排查 out of order read
out of order read 的含义不是“文件里的像素顺序错了”,而是当前 Pipeline 请求像素的顺序超出了加载器承诺的访问模式。最常见的情况是:输入按顺序模式打开,但后续操作需要回头读取、从底部开始读取,或者消费同一条顺序 Pipeline 两次。
- 垂直翻转、90°/180°/270° 旋转或 Smart Crop 在导出时报告
out of order read。 - 第一次导出成功,第二次从同一个顺序图像导出失败。
- 变换调用本身没有返回错误,直到
Export*或SaveToWriter*才失败。 - 换成默认加载方式后错误消失,但内存或临时磁盘用量上升。
原因:顺序图像只能消费一次
Section titled “原因:顺序图像只能消费一次”libvips 是需求驱动的:变换调用通常只构建 Pipeline,真正的像素读取发生在导出等下游操作请求结果时。因此,造成访问顺序冲突的变换可以先“成功”,错误却在编码阶段才出现。
VIPS_ACCESS_SEQUENTIAL 表示像素只能大体从上到下读取,并允许在读取位置后方保留少量缓冲。它适合单次、从上到下的处理,但不适合以下需求:
| Pipeline 行为 | 为什么需要随机访问 |
|---|---|
| 垂直翻转 | 输出第一行需要读取输入最后一行 |
| 90°/270° 旋转 | 输出扫描线对应输入的列,而不是连续的行 |
| 180° 旋转 | 需要反向读取行和像素 |
| Smart Crop / 内容感知分析 | 需要先查看多个区域,再决定输出窗口 |
| 从同一顺序图像导出两次 | 第一次导出已经消费了输入流 |
下面的程序显式请求 AccessSequential,随后做垂直翻转。govips v2.19.0 中,Flip 只建立新的 Pipeline;ExportJpeg 首次消费像素时才返回 VipsJpeg: out of order read。
package main
import ( "fmt" "log" "os"
"github.com/davidbyttow/govips/v2/vips")
func main() { if len(os.Args) != 2 { log.Fatalf("usage: %s INPUT", os.Args[0]) } if err := vips.Startup(nil); err != nil { log.Fatal(err) } defer vips.Shutdown()
input, err := os.Open(os.Args[1]) if err != nil { log.Fatal(err) } defer input.Close()
params := vips.NewImportParams() params.Access.Set(vips.AccessSequential) image, err := vips.LoadImageFromReader(input, params) if err != nil { log.Fatal(err) } defer image.Close()
if err := image.Flip(vips.DirectionVertical); err != nil { log.Fatal(err) } if _, _, err := image.ExportJpeg(vips.NewJpegExportParams()); err != nil { fmt.Printf("expected failure: %v\n", err) return } log.Fatal("expected an out-of-order error")}1. 随机访问操作使用默认实体化路径
Section titled “1. 随机访问操作使用默认实体化路径”如果 Pipeline 包含垂直翻转、旋转、Smart Crop 或多次导出,不要设置 AccessSequential。govips v2.19.0 的 LoadImageFromReader(input, nil) 会在返回前实体化图像:解码尺寸不超过阈值时放入内存,更大时使用临时文件。Reader 在函数返回后即可关闭。
package main
import ( "log" "os"
"github.com/davidbyttow/govips/v2/vips")
func main() { if len(os.Args) != 3 { log.Fatalf("usage: %s INPUT OUTPUT", os.Args[0]) } if err := vips.Startup(nil); err != nil { log.Fatal(err) } defer vips.Shutdown()
input, err := os.Open(os.Args[1]) if err != nil { log.Fatal(err) } image, err := vips.LoadImageFromReader(input, nil) closeErr := input.Close() if err != nil { log.Fatal(err) } if closeErr != nil { image.Close() log.Fatal(closeErr) } defer image.Close()
if err := image.Flip(vips.DirectionVertical); err != nil { log.Fatal(err) } buffer, _, err := image.ExportJpeg(vips.NewJpegExportParams()) if err != nil { log.Fatal(err) } if err := os.WriteFile(os.Args[2], buffer, 0o644); err != nil { log.Fatal(err) }}实体化是明确的资源权衡:它换来随机访问能力,但可能使用更多内存或临时磁盘。可通过 SetStreamDiscThreshold 与 SetStreamScratchDir 控制 govips 的落盘边界和位置。
2. 顺序路径只保留顺序安全的操作
Section titled “2. 顺序路径只保留顺序安全的操作”如果目标是单次格式转换、缩小、颜色空间转换或其他能从上到下完成的处理,可以继续使用 AccessSequential。此时 Reader 必须一直保持可用,直到 ImageRef.Close();不要在同一顺序图像上分叉出多个消费者。
3. EXIF 自动旋转可交给 TranscodeStream 决策
Section titled “3. EXIF 自动旋转可交给 TranscodeStream 决策”只需要“读取 → 可选自动旋转 → 导出”时,可使用 govips v2.19.0 的 TranscodeStream。它会让方向 1 和 2 留在顺序路径;需要转置或反向读取的方向会先实体化。更复杂的自定义变换仍应由调用方在加载前决定访问策略。
判断修复是否有效
Section titled “判断修复是否有效”- 用同一输入和导出参数重复失败路径,确认不再出现
out of order read。 - 检查输出尺寸、方向和像素结果,避免只以“没有报错”为验收标准。
- 记录峰值 RSS、临时磁盘使用和失败位置;实体化可能把访问错误变成资源压力。
- 若需要多个输出,确认它们读取的是同一份已实体化图像,或分别重新加载输入,而不是重复消费原顺序流。
受影响版本与边界
Section titled “受影响版本与边界”- 本页失败路径已在 govips v2.19.0 与 libvips 8.17.3 上复现;默认实体化路径生成了有效的 1938×1292 JPEG。
- 具体错误前缀可能来自 JPEG、PNG 或其他加载器,不应只匹配
VipsJpeg。 - sharp、pyvips 与 vips CLI 暴露访问策略的方式不同;本页不声称它们与 govips 具有相同默认值。