跳转到内容

排查 out of order read

复制本页

out of order read 的含义不是“文件里的像素顺序错了”,而是当前 Pipeline 请求像素的顺序超出了加载器承诺的访问模式。最常见的情况是:输入按顺序模式打开,但后续操作需要回头读取、从底部开始读取,或者消费同一条顺序 Pipeline 两次。

  • 垂直翻转、90°/180°/270° 旋转或 Smart Crop 在导出时报告 out of order read。
  • 第一次导出成功,第二次从同一个顺序图像导出失败。
  • 变换调用本身没有返回错误,直到 Export* 或 SaveToWriter* 才失败。
  • 换成默认加载方式后错误消失,但内存或临时磁盘用量上升。

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 留在顺序路径;需要转置或反向读取的方向会先实体化。更复杂的自定义变换仍应由调用方在加载前决定访问策略。

  1. 用同一输入和导出参数重复失败路径,确认不再出现 out of order read。
  2. 检查输出尺寸、方向和像素结果,避免只以“没有报错”为验收标准。
  3. 记录峰值 RSS、临时磁盘使用和失败位置;实体化可能把访问错误变成资源压力。
  4. 若需要多个输出,确认它们读取的是同一份已实体化图像,或分别重新加载输入,而不是重复消费原顺序流。
  • 本页失败路径已在 govips v2.19.0 与 libvips 8.17.3 上复现;默认实体化路径生成了有效的 1938×1292 JPEG。
  • 具体错误前缀可能来自 JPEG、PNG 或其他加载器,不应只匹配 VipsJpeg。
  • sharp、pyvips 与 vips CLI 暴露访问策略的方式不同;本页不声称它们与 govips 具有相同默认值。