package builtin // fileread_webp.go -- WebP 图片 EXIF 方向校正. // // WebP 是 Google 推出的 Web 图片格式,Anthropic vision API 原生支持. // 带有 EXIF 方向标签的 WebP 图片(通常来自处理过的手机照片) // 如不校正,vision API 会看到方向错误的图片. // // WebP 容器格式(RIFF): // // RIFF header (12B): "RIFF" + size(4) + "WEBP" // Extended WebP: VP8X chunk → 可能含 EXIF chunk // EXIF chunk: chunk ID "EXIF" + size(4) + TIFF data // // 精妙之处(CLEVER): RIFF 容器解析极其简单(chunk ID + 4字节长度), // 只需 ~70 行就能准确找到 EXIF chunk,而无需理解 VP8/VP8L 编解码逻辑. // EXIF chunk 内的数据与 JPEG APP1 中的格式完全一致, // 直接复用 parseTIFFIFDOrientation/parseEXIFOrientation 即可. // // 升华改进(ELEVATED): 像素解码使用 golang.org/x/image/webp(已在 go.mod), // 这是 VP8/VP8L 解码的最小可行实现(~3800 行), // 比 libwebp C 库(CGO)更简洁,比自写 VP8 解码器(同等行数)少维护成本. // 解码后重新编码为 JPEG 输出(vision API 接受两种格式,JPEG 压缩更可控). import ( "bytes" "encoding/binary" "image/jpeg" "golang.org/x/image/webp" ) // ───────────────────────────────────────────────────────────────────── // 公共入口 // ───────────────────────────────────────────────────────────────────── // correctWebPOrientation 从 WebP 文件解析 EXIF Orientation, // 若不为 1(正常),对图片做相应变换后重新编码为 JPEG 返回. // // 返回值:校正后的字节数据 + media type. // - Orientation=1 或无 EXIF:返回原始 data + "image/webp"(不变) // - 解码/编码失败:返回原始 data + "image/webp"(fail-open) // - 校正成功:返回 JPEG 数据 + "image/jpeg" // // 精妙之处(CLEVER): 输出 JPEG 而非 WebP 是因为: // 1. Go 标准库无 WebP 编码器(x/image/webp 只有解码) // 2. Anthropic API 同等接受 JPEG 和 WebP // 3. JPEG 编码质量(92)对 vision 任务完全足够 func correctWebPOrientation(data []byte) ([]byte, string) { orient := parseWebPOrientation(data) if orient <= exifOrientNormal || orient > exifOrientRotate90CCW { return data, "image/webp" // 无需处理 } // 解码像素(使用 x/image/webp) img, err := webp.Decode(bytes.NewReader(data)) if err != nil { return data, "image/webp" // fail-open } // 应用变换 transformed := applyOrientation(img, orient) // 重新编码为 JPEG var buf bytes.Buffer if err := jpeg.Encode(&buf, transformed, &jpeg.Options{Quality: 92}); err != nil { return data, "image/webp" // fail-open } return buf.Bytes(), "image/jpeg" } // ───────────────────────────────────────────────────────────────────── // RIFF 容器解析 // ───────────────────────────────────────────────────────────────────── // parseWebPOrientation 解析 WebP RIFF 容器,找到 EXIF chunk 并提取 Orientation. // // WebP RIFF 结构: // // [0:4] "RIFF" // [4:8] 文件总大小 - 8(小端) // [8:12] "WEBP" // [12:] chunks,每个 chunk 格式:ID(4) + size(4) + data(size) // // 只有 Extended WebP(VP8X chunk)才有 EXIF. // Simple VP8 和 VP8L 无 EXIF,函数直接返回 1. func parseWebPOrientation(data []byte) exifOrientation { // 验证 RIFF/WEBP 头 if len(data) < 12 { return 1 } if !bytes.Equal(data[:4], []byte("RIFF")) { return 1 } if !bytes.Equal(data[8:12], []byte("WEBP")) { return 1 } // 遍历 chunks pos := 12 for pos+8 <= len(data) { chunkID := string(data[pos : pos+4]) chunkSize := int(binary.LittleEndian.Uint32(data[pos+4 : pos+8])) dataStart := pos + 8 if dataStart+chunkSize > len(data) { break // 数据截断 } if chunkID == "EXIF" { // 找到 EXIF chunk! // 精妙之处(CLEVER): WebP EXIF chunk 有两种编码方式: // 1. 带 "Exif\0\0" 前缀(部分 encoder 包含)→ parseEXIFOrientation // 2. 直接是 TIFF 数据(无前缀)→ parseTIFFIFDOrientation // 先尝试带前缀,失败再尝试无前缀,兼容两种编码器. exifData := data[dataStart : dataStart+chunkSize] if orient, ok := parseEXIFOrientation(exifData); ok { return orient } if orient, ok := parseTIFFIFDOrientation(exifData); ok { return orient } return 1 } // 跳到下一个 chunk(RIFF 规定 chunk 对齐到 2 字节边界) pos = dataStart + chunkSize if chunkSize%2 != 0 { pos++ // padding byte } } return 1 // 无 EXIF chunk(Simple VP8/VP8L 图片) }