package builtin // fileread_exif.go -- JPEG EXIF 方向校正. // // 手机拍照的 JPEG 图片通常把像素数据以传感器原始方向存储(横向), // 然后在 EXIF APP1 段的 Orientation 标签(0x0112)记录实际方向(如"顺时针旋转90°"). // 普通看图软件会自动处理,但 Anthropic vision API 直接看原始像素,不解释 EXIF. // // 此文件做三件事: // 1. 从 JPEG 字节流中解析 APP1/EXIF 段,提取 Orientation 标签值 // 2. 根据 Orientation 对图片像素做旋转/翻转变换 // 3. 重新编码为 JPEG 字节返回(供调用方 base64 编码后发给 API) // // 精妙之处(CLEVER): 全程只用 Go 标准库-- // - EXIF 解析:手动读 JPEG segment + TIFF IFD,不引入 goexif 等外部包 // - 图片变换:image + image/jpeg 标准包,像素级操作 // - 零外部依赖,符合项目"只用标准库"原则 // // 升华改进(ELEVATED): 早期实现 项目注释"EXIF - 需要外部库,暂不实现". // 实际上 EXIF Orientation 标签只需解析 JPEG APP1 段前几十字节, // 完全可以在标准库范围内实现.仅旋转场景(不涉及缩放/滤镜)无需 libvips/sharp. import ( "bytes" "encoding/binary" "image" "image/color" "image/jpeg" // 注册 JPEG 解码器到 image 包(必须,否则 image.Decode 无法处理 JPEG) _ "image/jpeg" // 注册 PNG 解码器(其他路径也会用到 image.Decode) _ "image/png" ) // exifOrientation 表示 JPEG EXIF Orientation 标签的取值含义. // EXIF 规范 Tag 0x0112,IFD0,SHORT 类型. type exifOrientation int const ( exifOrientNormal exifOrientation = 1 // 正常,无需处理 exifOrientFlipH exifOrientation = 2 // 水平翻转 exifOrientRotate180 exifOrientation = 3 // 旋转 180° exifOrientFlipV exifOrientation = 4 // 垂直翻转 exifOrientTranspose exifOrientation = 5 // 转置(沿主对角线翻转) exifOrientRotate90CW exifOrientation = 6 // 顺时针旋转 90°(手机竖拍最常见) exifOrientTransverse exifOrientation = 7 // 反转置(沿副对角线翻转) exifOrientRotate90CCW exifOrientation = 8 // 逆时针旋转 90° ) // correctJPEGOrientation 读取 JPEG 的 EXIF Orientation,若不为 1(正常), // 对图片做相应的像素变换后重新编码为 JPEG 返回. // // 若无 EXIF,无 Orientation 标签,或 Orientation=1,返回原始 data 不做任何操作. // 若解码/编码失败,返回原始 data(fail-open,保证不破坏原有功能). // // 精妙之处(CLEVER): fail-open 设计--EXIF 校正是锦上添花, // 失败时静默返回原始数据,不影响 FileRead 的主流程. // 替代方案:失败时返回 error(破坏调用链,用户看到莫名其妙的报错). func correctJPEGOrientation(data []byte) []byte { orient := parseJPEGOrientation(data) if orient <= exifOrientNormal || orient > exifOrientRotate90CCW { return data // 无需处理或值异常 } // 解码图片像素 img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return data // 解码失败,原样返回 } // 执行变换 transformed := applyOrientation(img, orient) // 重新编码为 JPEG var buf bytes.Buffer if err := jpeg.Encode(&buf, transformed, &jpeg.Options{Quality: 92}); err != nil { return data // 编码失败,原样返回 } return buf.Bytes() } // ───────────────────────────────────────────────────────────────────── // JPEG EXIF 解析 // ───────────────────────────────────────────────────────────────────── // parseJPEGOrientation 从 JPEG 字节流中提取 EXIF Orientation 标签值. // 返回 1(正常)表示无需处理,返回 0 表示未找到或解析失败. // // JPEG 结构:FF D8(SOI)→ APP1(FF E1)→ Exif\0\0 → TIFF → IFD0 → tag 0x0112 // // 精妙之处(CLEVER): 只解析到 IFD0 的 Orientation tag 即止, // 不构建完整 EXIF 树.最多读几百字节,内存开销极小. func parseJPEGOrientation(data []byte) exifOrientation { if len(data) < 4 { return 1 } // 验证 JPEG SOI if data[0] != 0xFF || data[1] != 0xD8 { return 1 } // 遍历 JPEG segment pos := 2 for pos+4 <= len(data) { if data[pos] != 0xFF { return 1 // 损坏的 JPEG } marker := data[pos+1] // SOS(FF DA)之后是压缩数据,不再有 EXIF if marker == 0xDA { return 1 } // APP1 = FF E1 if marker == 0xE1 { segLen := int(binary.BigEndian.Uint16(data[pos+2 : pos+4])) if pos+2+segLen > len(data) { return 1 } segData := data[pos+4 : pos+2+segLen] if orient, ok := parseEXIFOrientation(segData); ok { return orient } } // 跳过此 segment(长度包含自身的 2 字节) segLen := int(binary.BigEndian.Uint16(data[pos+2 : pos+4])) pos += 2 + segLen } return 1 } // parseEXIFOrientation 从 APP1 segment 数据中解析 EXIF Orientation. // segData 是 APP1 marker + length 之后的内容(即 "Exif\0\0" 开始的部分). // // 精妙之处(CLEVER): 作为薄包装层,剥离 "Exif\0\0" 前缀后委托给 // parseTIFFIFDOrientation,使同一个 TIFF IFD 解析器可以被 // JPEG/WebP/TIFF 三种格式共用,不重复实现. func parseEXIFOrientation(segData []byte) (exifOrientation, bool) { // 检查 "Exif\0\0" 标识(6字节) if len(segData) < 6 || !bytes.HasPrefix(segData, []byte("Exif\x00\x00")) { return 1, false } return parseTIFFIFDOrientation(segData[6:]) } // parseTIFFIFDOrientation 从原始 TIFF 字节(从 TIFF 头部开始,无 "Exif\0\0" 前缀) // 中解析 Orientation 标签(0x0112). // // 被以下三处调用: // - parseEXIFOrientation:处理 JPEG APP1 / WebP EXIF chunk(剥离前缀后调用) // - parseTIFFOrientation:处理独立 TIFF 文件(文件头即 TIFF 头) // // 升华改进(ELEVATED): 早期实现 只对 JPEG 做 EXIF 校正. // 提取这个公共函数后,JPEG/WebP/TIFF 三种格式共享同一个 // 22 行的 IFD 解析核心,零代码重复. func parseTIFFIFDOrientation(tiff []byte) (exifOrientation, bool) { if len(tiff) < 8 { return 1, false } // 确定字节序 var bo binary.ByteOrder switch { case bytes.HasPrefix(tiff, []byte("II")): // Little-Endian(Intel) bo = binary.LittleEndian case bytes.HasPrefix(tiff, []byte("MM")): // Big-Endian(Motorola) bo = binary.BigEndian default: return 1, false } // 验证 TIFF magic(0x002A) if bo.Uint16(tiff[2:4]) != 0x002A { return 1, false } // IFD0 偏移(从 TIFF 头部开始计算) ifd0Offset := int(bo.Uint32(tiff[4:8])) if ifd0Offset+2 > len(tiff) { return 1, false } // 读取 IFD0 条目数量 entryCount := int(bo.Uint16(tiff[ifd0Offset : ifd0Offset+2])) pos := ifd0Offset + 2 // 精妙之处(CLEVER): 每个 IFD 条目固定 12 字节: // tag(2) + type(2) + count(4) + value_offset(4) // 对于 SHORT 类型且 count=1,value 直接存在 value_offset 的低 2 字节(小端)或高 2 字节(大端) for i := 0; i < entryCount; i++ { entryPos := pos + i*12 if entryPos+12 > len(tiff) { break } tag := bo.Uint16(tiff[entryPos : entryPos+2]) if tag == 0x0112 { // Orientation tag // type=SHORT(3), count=1, value in bytes 8-9 of entry valOffset := entryPos + 8 orient := exifOrientation(bo.Uint16(tiff[valOffset : valOffset+2])) return orient, true } } return 1, false } // ───────────────────────────────────────────────────────────────────── // 图片变换 // ───────────────────────────────────────────────────────────────────── // applyOrientation 根据 EXIF Orientation 对图片做像素变换. // // 8 种变换覆盖所有手机拍照场景,最常见的是 6(顺时针90°,手机竖拍). // 使用 image.NRGBA 作为中间格式,保证所有颜色模型都能正确处理. func applyOrientation(src image.Image, orient exifOrientation) image.Image { bounds := src.Bounds() srcW := bounds.Max.X - bounds.Min.X srcH := bounds.Max.Y - bounds.Min.Y switch orient { case exifOrientFlipH: // 2: 水平翻转 dst := image.NewNRGBA(image.Rect(0, 0, srcW, srcH)) for y := 0; y < srcH; y++ { for x := 0; x < srcW; x++ { dst.Set(srcW-1-x, y, src.At(bounds.Min.X+x, bounds.Min.Y+y)) } } return dst case exifOrientRotate180: // 3: 旋转 180° dst := image.NewNRGBA(image.Rect(0, 0, srcW, srcH)) for y := 0; y < srcH; y++ { for x := 0; x < srcW; x++ { dst.Set(srcW-1-x, srcH-1-y, src.At(bounds.Min.X+x, bounds.Min.Y+y)) } } return dst case exifOrientFlipV: // 4: 垂直翻转 dst := image.NewNRGBA(image.Rect(0, 0, srcW, srcH)) for y := 0; y < srcH; y++ { for x := 0; x < srcW; x++ { dst.Set(x, srcH-1-y, src.At(bounds.Min.X+x, bounds.Min.Y+y)) } } return dst case exifOrientTranspose: // 5: 转置(主对角线翻转) // 输出尺寸:宽高互换 dst := image.NewNRGBA(image.Rect(0, 0, srcH, srcW)) for y := 0; y < srcH; y++ { for x := 0; x < srcW; x++ { dst.Set(y, x, src.At(bounds.Min.X+x, bounds.Min.Y+y)) } } return dst case exifOrientRotate90CW: // 6: 顺时针旋转 90°(最常见:手机竖拍) // 升华改进(ELEVATED): 这是最高频的校正--手机竖拍时传感器横向, // EXIF=6 要求顺时针旋转90°才能"站起来". // 输出尺寸:宽高互换(srcW → 新高,srcH → 新宽) dst := image.NewNRGBA(image.Rect(0, 0, srcH, srcW)) for y := 0; y < srcH; y++ { for x := 0; x < srcW; x++ { // (x,y) → (srcH-1-y, x) dst.Set(srcH-1-y, x, src.At(bounds.Min.X+x, bounds.Min.Y+y)) } } return dst case exifOrientTransverse: // 7: 反转置(副对角线翻转) dst := image.NewNRGBA(image.Rect(0, 0, srcH, srcW)) for y := 0; y < srcH; y++ { for x := 0; x < srcW; x++ { dst.Set(srcH-1-y, srcW-1-x, src.At(bounds.Min.X+x, bounds.Min.Y+y)) } } return dst case exifOrientRotate90CCW: // 8: 逆时针旋转 90° dst := image.NewNRGBA(image.Rect(0, 0, srcH, srcW)) for y := 0; y < srcH; y++ { for x := 0; x < srcW; x++ { // (x,y) → (y, srcW-1-x) dst.Set(y, srcW-1-x, src.At(bounds.Min.X+x, bounds.Min.Y+y)) } } return dst default: return src } } // colorToNRGBA 将任意 color.Color 转换为 color.NRGBA(用于像素复制). // 仅在需要直接操作像素时使用,applyOrientation 已使用 image.NRGBA 内置处理. func colorToNRGBA(c color.Color) color.NRGBA { r, g, b, a := c.RGBA() if a == 0 { return color.NRGBA{} } // RGBA() 返回 pre-multiplied 16-bit 值,转换为 8-bit non-premultiplied return color.NRGBA{ R: uint8(r * 0xff / a), G: uint8(g * 0xff / a), B: uint8(b * 0xff / a), A: uint8(a >> 8), } }