15-minute quickstart
This exercise turns the older Chinese notes’ “read → process → export” outline into one comparable task for four bindings: shrink input.jpg to fit within 640 × 640, never enlarge it, and write output.webp at quality 82.
1. Prepare an input
Section titled “1. Prepare an input”To run the example, name your own JPEG input.jpg; different dimensions and image content will produce different output measurements.


The reference output was generated with govips v2.19.0 and libvips 8.17.3.
2. Choose a binding and run it
Section titled “2. Choose a binding and run it”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
}Run command
node index.mjs input.jpg output/Complete source
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import sharp from 'sharp';
const [inputPath, outputDir] = process.argv.slice(2);
if (!inputPath || !outputDir) {
console.error('Usage: node index.mjs <input.jpg> <output-directory>');
process.exit(2);
}
try {
await mkdir(outputDir, { recursive: true });
const outputPath = path.join(outputDir, 'output.webp');
const writeResult = await sharp(inputPath)
.resize(640, 640, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 82 })
.toFile(outputPath);
if (writeResult.width > 640 || writeResult.height > 640) {
throw new Error(`Unexpected thumbnail dimensions: ${writeResult.width}x${writeResult.height}`);
}
await writeFile(
path.join(outputDir, 'result.json'),
`${JSON.stringify({
recipeId: 'quickstart-thumbnail',
generator: { sharp: sharp.versions.sharp, libvips: sharp.versions.vips },
input: path.basename(inputPath),
outputs: [{
path: 'output.webp',
format: writeResult.format,
width: writeResult.width,
height: writeResult.height,
size: writeResult.size,
}],
}, null, 2)}\n`,
);
console.log(`quickstart-thumbnail: ${writeResult.width}x${writeResult.height}, ${writeResult.size} bytes`);
} catch (error) {
console.error(error);
process.exit(1);
}Run command
python3 main.py input.jpg output/Complete source
import sys
import json
import os
import pyvips
input_path, output_dir = sys.argv[1], sys.argv[2]
if not input_path or not output_dir:
print("Usage: python3 main.py <input.jpg> <output-directory>", file=sys.stderr)
raise SystemExit(2)
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, "output.webp")
try:
image = pyvips.Image.thumbnail(input_path, 640, height=640, size="down")
image.write_to_file(output_path, Q=82)
if image.width > 640 or image.height > 640:
raise ValueError(f"Unexpected thumbnail dimensions: {image.width}x{image.height}")
result_path = os.path.join(output_dir, "result.json")
with open(result_path, "w") as f:
json.dump({
"recipeId": "quickstart-thumbnail",
"generator": {
"pyvips": pyvips.__version__,
"libvips": f"{pyvips.version(0)}.{pyvips.version(1)}.{pyvips.version(2)}",
},
"input": os.path.basename(input_path),
"outputs": [{
"path": "output.webp",
"format": "webp",
"width": image.width,
"height": image.height,
"bands": image.bands,
"hasAlpha": image.hasalpha(),
"size": os.path.getsize(output_path),
}],
}, f, indent=2)
f.write("\n")
print(f"quickstart-thumbnail: {image.width}x{image.height}, {os.path.getsize(output_path)} bytes")
except pyvips.Error as error:
print(error, file=sys.stderr)
raise SystemExit(1)Run command
bash run.sh input.jpg output/Complete source
#!/usr/bin/env bash
set -euo pipefail
INPUT="$1"
OUTPUT_DIR="$2"
if [ $# -ne 2 ]; then
echo "Usage: bash run.sh <input.jpg> <output-directory>" >&2
exit 2
fi
mkdir -p "$OUTPUT_DIR"
OUTPUT_WEBP="$OUTPUT_DIR/output.webp"
vips thumbnail "$INPUT" "$OUTPUT_WEBP[Q=82]" 640 --height 640 --size down
IDATA="$(vipsheader --all "$OUTPUT_WEBP")"
WIDTH="$(echo "$IDATA" | grep '^width:' | awk '{print $2}')"
HEIGHT="$(echo "$IDATA" | grep '^height:' | awk '{print $2}')"
BANDS="$(echo "$IDATA" | grep '^bands:' | awk '{print $2}')"
FILESIZE="$(stat -c%s "$OUTPUT_WEBP")"
LIBVIPS_VER="$(vips --version | sed 's/vips-//')"
if [ "$WIDTH" -gt 640 ] || [ "$HEIGHT" -gt 640 ]; then
echo "Unexpected thumbnail dimensions: ${WIDTH}x${HEIGHT}" >&2
exit 1
fi
cat > "$OUTPUT_DIR/result.json" << EOF
{
"recipeId": "quickstart-thumbnail",
"generator": {
"vipsCli": "${LIBVIPS_VER}",
"libvips": "${LIBVIPS_VER}"
},
"input": "$(basename "$INPUT")",
"outputs": [{
"path": "output.webp",
"format": "webp",
"width": ${WIDTH},
"height": ${HEIGHT},
"bands": ${BANDS},
"hasAlpha": false,
"size": ${FILESIZE}
}]
}
EOF
echo "quickstart-thumbnail: ${WIDTH}x${HEIGHT}, ${FILESIZE} bytes"3. Inspect the result
Section titled “3. Inspect the result”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.
Continue to build a complete image pipeline and make input checks, orientation normalization, encoding, and result verification explicit stages.