Skip to content

15-minute quickstart

Copy page

This exercise turns the classic “read → process → export” workflow into one comparable task across four interfaces: shrink input.jpg to fit within 640 × 640, never enlarge it, and write output.webp at quality 82.

To run the example, name your own JPEG input.jpg; different dimensions and image content will produce different output measurements.

Gate of the Orient outlined in red lights above a park walkway at night
Input: JPEG1938 × 1292, 610,191 bytes
The same night scene resized to fit within 640 pixels and encoded as WebP
Reference output: WebP640 × 427, 36,172 bytes, quality 82

The reference output was generated with govips v2.19.0 and libvips 8.17.3.

Run command

go run . input.jpg output/

Complete source

package main

import (
	"fmt"
	"os"

	"github.com/davidbyttow/govips/v2/vips"
	"github.com/webp-sh/vips-handbook/examples/internal/recipeutil"
)

func main() {
	if err := run(os.Args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run(args []string) error {
	if len(args) != 3 {
		return fmt.Errorf("usage: quickstart-thumbnail <input.jpg> <output-directory>")
	}
	inputPath, outputDir := args[1], args[2]
	if err := os.MkdirAll(outputDir, 0o755); err != nil {
		return err
	}

	if err := vips.Startup(nil); err != nil {
		return fmt.Errorf("start libvips: %w", err)
	}
	defer vips.Shutdown()

	image, err := vips.NewThumbnailFromFile(inputPath, 640, 640, vips.InterestingNone)
	if err != nil {
		return fmt.Errorf("create thumbnail: %w", err)
	}
	defer image.Close()

	output, err := recipeutil.ExportWebp(image, outputDir, "output.webp", 82, map[string]any{
		"width": 640, "height": 640, "crop": "none", "withoutEnlargement": true,
	})
	if err != nil {
		return err
	}
	if output.Width > 640 || output.Height > 640 {
		return fmt.Errorf("unexpected thumbnail dimensions: %dx%d", output.Width, output.Height)
	}
	if err := recipeutil.WriteResult(outputDir, "quickstart-thumbnail", inputPath, []recipeutil.Output{output}); err != nil {
		return err
	}

	fmt.Printf("quickstart-thumbnail: %dx%d, %d bytes\n", output.Width, output.Height, output.Size)
	return nil
}

At minimum, confirm that output.webp exists and can be opened again; neither dimension is greater than 640; an input smaller than the target was not enlarged; and the output is WebP data, not a renamed file.

  • Format: WEBP
  • Dimensions: 640 × 427
  • Bands: 3
  • Alpha: No
  • File size: 36,172 bytes
  • Generated with: govips v2.19.0 / libvips 8.17.3

The four snippets target the same product behavior, but they are not line-for-line equivalents. govips and pyvips call thumbnail operations directly, sharp expresses the boundary with resize({ fit: 'inside' }), and the CLI passes saver options through the filename.

The same 1938 × 1292 JPEG was fit inside 640 pixels without enlargement, as WebP quality 82. Results from the 2026-10-12 run:

Interface libvips Output size Bytes
govips v2.19.0 8.17.3 640 × 427 36,172
sharp 0.35.5 8.18.7 640 × 427 35,924
pyvips 3.2.0 8.17.3 640 × 427 39,228
vips CLI 8.17.3 8.17.3 640 × 427 39,228

Dimensions, format, and the no-enlargement rule line up. Byte size does not. sharp’s libvips is one minor version ahead of the other three. pyvips and the CLI share 8.17.3 and the same byte size, which still differs slightly from the govips thumbnail path.

Continue to build a complete image pipeline and make input checks, orientation normalization, encoding, and result verification explicit stages.