Reader/Writer streaming I/O (govips v2.19.0)
govips v2.19.0 can connect an io.Reader directly to libvips and write encoded chunks to an io.Writer. This avoids copying the complete compressed file into a Go byte slice with io.ReadAll, and gives HTTP bodies, object-storage streams, and pipes a common interface.
Responsibilities of the three APIs
Section titled “Responsibilities of the three APIs”| API | Role | Access strategy |
|---|---|---|
LoadImageFromReader |
Create an ImageRef from any io.Reader |
Materialized by default; lazy with explicit AccessSequential |
SaveToWriter / typed variants |
Write encoded chunks to an io.Writer |
Codec-dependent; formats such as TIFF may need full buffering |
TranscodeStream |
Combine a Reader, optional auto-rotation, and a Writer | Selects sequential or materialized loading from orientation metadata |
These APIs are not present in govips v2.17.0. This page and its executable example are pinned to v2.19.0.
Executable sequential-stream example
Section titled “Executable sequential-stream example”The reference implementation explicitly selects AccessSequential and keeps the input Reader open until ImageRef.Close(). All four bindings produce the same product result. The sharp, pyvips, and CLI versions are comparison implementations; they do not imply identical Reader/Writer APIs.
Run command
go run . input.jpg output/Complete source
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/davidbyttow/govips/v2/vips"
"github.com/webp-sh/vips-handbook/examples/internal/recipeutil"
)
type countingWriter struct {
writer io.Writer
written int
}
func (w *countingWriter) Write(data []byte) (int, error) {
n, err := w.writer.Write(data)
w.written += n
return n, err
}
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: stream-basic <input.jpg> <output-directory>")
}
inputPath, outputDir := args[1], args[2]
if err := os.MkdirAll(outputDir, 0o755); err != nil {
return fmt.Errorf("create output directory: %w", err)
}
vips.LoggingSettings(nil, vips.LogLevelError)
if err := vips.Startup(nil); err != nil {
return fmt.Errorf("start libvips: %w", err)
}
defer vips.Shutdown()
input, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("open input: %w", err)
}
defer input.Close()
outputPath := filepath.Join(outputDir, "streamed.webp")
output, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("create output: %w", err)
}
counter := &countingWriter{writer: output}
result, streamErr := streamWebp(input, counter)
closeErr := output.Close()
if streamErr != nil {
return streamErr
}
if closeErr != nil {
return fmt.Errorf("close output: %w", closeErr)
}
result.Path = filepath.Base(outputPath)
result.Size = counter.written
if err := recipeutil.WriteResult(outputDir, "stream-basic", inputPath, []recipeutil.Output{result}); err != nil {
return err
}
fmt.Printf("stream-basic: %dx%d, %d bytes\n", result.Width, result.Height, result.Size)
return nil
}
func streamWebp(input io.Reader, output io.Writer) (recipeutil.Output, error) {
params := vips.NewImportParams()
params.Access.Set(vips.AccessSequential)
image, err := vips.LoadImageFromReader(input, params)
if err != nil {
return recipeutil.Output{}, fmt.Errorf("load sequential stream: %w", err)
}
defer image.Close()
export := vips.NewWebpExportParams()
export.Quality = 82
export.StripMetadata = true
if err := image.SaveToWriterWebp(output, export); err != nil {
return recipeutil.Output{}, fmt.Errorf("save WebP stream: %w", err)
}
return recipeutil.Output{
Parameters: map[string]any{
"access": "sequential",
"input": "io.Reader",
"output": "io.Writer",
"pipeline": "LoadImageFromReader → SaveToWriterWebp",
"quality": 82,
"metadata": "strip",
},
Format: "webp",
Width: image.Width(),
Height: image.Height(),
Bands: image.Bands(),
HasAlpha: image.HasAlpha(),
}, 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, 'streamed.webp');
const writeResult = await sharp(inputPath, { sequentialRead: true })
.webp({ quality: 82 })
.toFile(outputPath);
await writeFile(
path.join(outputDir, 'result.json'),
`${JSON.stringify({
recipeId: 'stream-basic',
generator: { sharp: sharp.versions.sharp, libvips: sharp.versions.vips },
input: path.basename(inputPath),
outputs: [{
path: 'streamed.webp', format: writeResult.format,
width: writeResult.width, height: writeResult.height, size: writeResult.size,
}],
}, null, 2)}\n`,
);
console.log(`stream-basic: ${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, "streamed.webp")
try:
image = pyvips.Image.new_from_file(input_path, access="sequential")
image.write_to_file(output_path, Q=82, keep="none")
result_path = os.path.join(output_dir, "result.json")
with open(result_path, "w") as f:
json.dump({
"recipeId": "stream-basic",
"generator": {"pyvips": pyvips.__version__,
"libvips": f"{pyvips.version(0)}.{pyvips.version(1)}.{pyvips.version(2)}"},
"input": os.path.basename(input_path),
"outputs": [{"path": "streamed.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"stream-basic: {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/streamed.webp"
# sequential access + WebP export
# The [access=sequential] hint tells libvips to read top-to-bottom
# without building a full random-access cache. This is the CLI
# equivalent of the govips/sharp/pyvips sequential modes.
vips copy "${INPUT}[access=sequential]" "$OUTPUT_WEBP[Q=82,keep=none]"
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="$(vips --version | sed 's/vips-//')"
cat > "$OUTPUT_DIR/result.json" << EOF
{
"recipeId": "stream-basic",
"generator": {
"vipsCli": "${LIBVIPS}",
"libvips": "${LIBVIPS}"
},
"input": "$(basename "$INPUT")",
"outputs": [{
"path": "streamed.webp",
"format": "webp",
"width": ${WIDTH},
"height": ${HEIGHT},
"bands": ${BANDS},
"hasAlpha": false,
"size": ${FILESIZE}
}]
}
EOF
echo "stream-basic: ${WIDTH}x${HEIGHT}, ${FILESIZE} bytes"Inspect the generated result
Section titled “Inspect the generated result”
Generated with govips v2.19.0 / libvips 8.17.3.
- Format: WEBP
- Dimensions: 1938 × 1292
- Bands: 3
- Alpha: No
- File size: 398,898 bytes
- Generated with: govips v2.19.0 / libvips 8.17.3
Automated tests cover propagation of Reader and Writer failures in the reference implementation. A sequential load normally consumes only enough bytes for the header. Truncation or a mid-stream failure can therefore surface later, when SaveToWriterWebp first consumes all pixels.
Sequential and materialized paths
Section titled “Sequential and materialized paths”The sequential path works for shrinking, cropping, flattening, colourspace conversion, horizontal flips, and most line-kernel operations. Operations that read rows out of order—90°/180°/270° rotation, vertical flip, and smart crop, for example—must materialize first or fail with out of order read.
params := vips.NewImportParams()params.Access.Set(vips.AccessSequential)
image, err := vips.LoadImageFromReader(input, params)if err != nil { return fmt.Errorf("load stream: %w", err)}defer image.Close()
if err := image.SaveToWriterWebp(output, vips.NewWebpExportParams()); err != nil { return fmt.Errorf("save stream: %w", err)}Use TranscodeStream when the pipeline must choose materialization from EXIF orientation:
err := vips.TranscodeStream(input, output, &vips.TranscodeOptions{ Format: vips.ImageTypeWEBP, AutoRotate: true, ExportParams: &vips.ExportParams{ Quality: 82, Interlaced: false, StripMetadata: true, },})if err != nil { return fmt.Errorf("transcode stream: %w", err)}TranscodeStream keeps orientations 1 and 2 on the sequential path. Orientations that transpose or reverse row order are materialized first.
Untrusted input and error boundaries
Section titled “Untrusted input and error boundaries”- Limit the complete request body at the HTTP layer.
SetPipeReadLimitonly limits header buffering for non-seekable Readers; it is not an upload-size limit. - In sequential mode, keep the Reader alive until
ImageRef.Close(). - Original Reader and Writer errors are wrapped and remain detectable with
errors.Is. - Progressive JPEG, interlaced PNG, HEIF decoding, and TIFF output can require additional buffering. Do not infer a memory ceiling from the API name alone.
- For cancellation in an explicit Load/Save flow, another goroutine can call
image.SetKill(true)and clear the flag after completion.TranscodeStreamdoes not currently accept acontext.Context.
Reproducible observation
Section titled “Reproducible observation”When comparing sequential and materialized paths, hold the input, libvips/govips versions, concurrency, and encoding parameters constant. Record peak RSS, exit status, output dimensions, and file size. A single-machine observation describes that environment only; it is not a universal performance claim.
- Read the execution model to understand demand-driven evaluation.
- Read production lifecycle management for
Startup,Shutdown, andImageRef.Close()rules.