跳转到内容

Reader/Writer 流式 I/O(govips v2.19.0)

复制本页

govips v2.19.0 可以直接把 io.Reader 连接到 libvips,并把编码结果写入 io.Writer。这避免了先用 io.ReadAll 把完整压缩文件复制到 Go byte slice,也让 HTTP Body、对象存储流和管道能够使用同一套接口。

API 作用 访问策略
LoadImageFromReader 从任意 io.Reader 创建 ImageRef 默认实体化;显式 AccessSequential 时延迟读取
SaveToWriter / typed variants 把编码块直接写入 io.Writer 取决于编码器;TIFF 等格式可能需要完整缓冲
TranscodeStream 组合 Reader、可选自动旋转和 Writer 根据方向信息自动选择顺序或实体化路径

这些 API 在 govips v2.17.0 中不存在;本页和可执行示例均锁定到 v2.19.0。

Reference 实现显式设置 AccessSequential,并保持输入 Reader 打开直到 ImageRef.Close()。四个 binding 都生成同一产品结果;sharp、pyvips 与 CLI 在这里用于结果比较,并不代表它们拥有与 govips 完全相同的 Reader/Writer 接口。

运行命令

go run . input.jpg output/

完整源码

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
}
stream-basic output
WEBP1938 × 1292, 398,898 bytes, 3 bands

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

Reference 的 Reader 与 Writer 错误传播已经由自动化测试覆盖。顺序模式下,加载函数通常只读取足够的头部;截断或中途失败可能直到 SaveToWriterWebp 首次完整消费像素时才返回。

顺序路径适合缩小、裁剪、flatten、颜色空间转换、水平翻转和多数行卷积。90°/180°/270° 旋转、垂直翻转、Smart Crop 等需要乱序读取像素的操作必须先实体化,否则会得到 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)
}

如果业务需要根据 EXIF 方向决定是否实体化,可使用 TranscodeStream:

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 会让方向 1 和 2 留在顺序路径;需要转置或反向读取行的方向会先实体化。

  • 在 HTTP 层限制请求体总长度;SetPipeReadLimit 只限制非 seekable Reader 的头部缓冲,不等于上传大小限制。
  • 顺序模式必须让 Reader 存活到 ImageRef.Close()。
  • Reader 和 Writer 的原始错误会被包装,可以用 errors.Is 判断。
  • progressive JPEG、interlaced PNG、HEIF 解码和 TIFF 输出可能需要额外缓冲,不能仅凭 API 名称推断内存上限。
  • 需要取消显式 Load/Save 流程时,可以从另一个 goroutine 调用 image.SetKill(true);完成后应清除 kill flag。TranscodeStream 当前不接收 context.Context。

比较顺序与实体化路径时,应固定输入、libvips/govips 版本、并发度和编码参数,并同时记录峰值 RSS、退出码、输出尺寸与文件大小。单次本机结果只能描述该环境,不能推广为所有格式和机器的性能结论。