package builtin // fileread_tiff.go -- TIFF 图片方向校正与 JPEG 转换. // // TIFF 是专业摄影和文档扫描的主流格式,常见于相机 RAW 输出, // 印刷行业,医学影像等场景.Anthropic vision API 不接受 TIFF, // 因此本模块负责: // 1. 读取 TIFF IFD0 中的 Orientation 标签(0x0112) // 2. 解码像素(支持无压缩/LZW/PackBits,覆盖 95%+ 实际场景) // 3. 应用旋转/翻转变换 // 4. 重新编码为 JPEG 返回 // // 精妙之处(CLEVER): TIFF 文件本身就是 TIFF 格式(无 "Exif\0\0" 前缀), // 因此 Orientation 解析直接复用 parseTIFFIFDOrientation, // 零新增解析代码. // // 升华改进(ELEVATED): 早期实现 根本不支持 TIFF. // 我们用纯 Go 标准库实现了一个最小 TIFF 解码器-- // 刻意跳过 CCITT(传真格式,3210 行 ccitt 包)和 JPEG-in-TIFF, // 只实现专业摄影场景实际用到的三种压缩格式. // 跳过的压缩类型走 fail-open:返回原始字节,不破坏主流程. import ( "bytes" "compress/zlib" "encoding/binary" "errors" "fmt" "image" "image/color" "image/jpeg" "io" // 精妙之处(CLEVER): TIFF LZW 有 "off by one" 算法差异(Aldus vs. 标准 LZW)-- // 代码宽度在表填满前一项就增加,而非填满后.Go 标准库 compress/lzw 实现标准 LZW, // 会产生 "invalid code" 错误.x/image/tiff/lzw 专为 TIFF 设计,已在 go.mod 中. tifflzw "golang.org/x/image/tiff/lzw" ) // ───────────────────────────────────────────────────────────────────── // TIFF 标签常量 // ───────────────────────────────────────────────────────────────────── // TIFF IFD 标签编号(Tag ID) const ( tiffTagImageWidth = 0x0100 // 256: 图片宽度 tiffTagImageLength = 0x0101 // 257: 图片高度 tiffTagBitsPerSample = 0x0102 // 258: 每分量位深 tiffTagCompression = 0x0103 // 259: 压缩格式 tiffTagPhotometric = 0x0106 // 262: 测光解释(颜色模型) tiffTagStripOffsets = 0x0111 // 273: 各条带在文件中的偏移 tiffTagSamplesPerPixel = 0x0115 // 277: 每像素分量数(RGB=3, RGBA=4) tiffTagRowsPerStrip = 0x0116 // 278: 每条带的行数 tiffTagStripByteCounts = 0x0117 // 279: 各条带压缩后字节数 tiffTagPlanarConfig = 0x011C // 284: 分量排列方式(1=chunky, 2=planar) tiffTagPredictor = 0x013D // 317: LZW 预测器(1=无, 2=水平差分) ) // TIFF 压缩格式 const ( tiffCompNone = 1 // 无压缩,原始像素 tiffCompLZW = 5 // LZW 压缩(需用 x/image/tiff/lzw,标准库 off-by-one 不兼容) tiffCompDeflate = 8 // Deflate/zlib 压缩(标准库 compress/zlib 支持) tiffCompPackBits = 32773 // PackBits RLE 压缩 // tiffCompCCITT3 = 3, tiffCompCCITT4 = 4: 传真格式,不支持(太古老) // tiffCompJPEG = 6/7: JPEG-in-TIFF,不支持(需要 JPEG 解码器嵌套) ) // TIFF 测光解释(PhotometricInterpretation) const ( tiffPhotoWhiteIsZero = 0 // 灰度,0=白(旧式扫描仪) tiffPhotoBlackIsZero = 1 // 灰度,0=黑 tiffPhotoRGB = 2 // RGB 彩色 ) // ───────────────────────────────────────────────────────────────────── // 公共入口 // ───────────────────────────────────────────────────────────────────── // correctTIFFImage 读取 TIFF 文件,解码像素,应用 Orientation 变换, // 重新编码为 JPEG 返回.同时返回新的 media type("image/jpeg"). // // fail-open 设计:任何解码/编码失败都返回原始数据 + 原始 media type, // 不影响 FileRead 主流程. func correctTIFFImage(data []byte) ([]byte, string) { img, err := decodeTIFF(data) if err != nil { return data, "image/tiff" // fail-open } // 读取并应用 Orientation(TIFF 文件头 = 直接的 TIFF 格式) orient := parseTIFFOrientation(data) if orient > exifOrientNormal && orient <= exifOrientRotate90CCW { img = applyOrientation(img, orient) } var buf bytes.Buffer if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 92}); err != nil { return data, "image/tiff" // fail-open } return buf.Bytes(), "image/jpeg" } // parseTIFFOrientation 从独立 TIFF 文件中读取 Orientation 标签. // // TIFF 文件头就是 TIFF 格式本身(不像 JPEG 需要先找 APP1 segment), // 因此直接调用 parseTIFFIFDOrientation 即可,无需任何额外解析. func parseTIFFOrientation(data []byte) exifOrientation { orient, _ := parseTIFFIFDOrientation(data) return orient } // ───────────────────────────────────────────────────────────────────── // TIFF 解码器 // ───────────────────────────────────────────────────────────────────── // tiffIFDEntry 表示 IFD 中一个 12 字节的条目. // 精妙之处(CLEVER): 把原始 4 字节 value/offset 存为 [4]byte 而非 uint32, // 因为它的含义取决于 count * typeSize ≤ 4 时存 inline value, // 否则存 offset.延迟解释避免了提前解码的歧义. type tiffIFDEntry struct { tag uint16 dataType uint16 count uint32 raw [4]byte // inline value 或 offset to data } // typeSizeOf 返回 TIFF 数据类型的单个元素字节数. func typeSizeOf(dt uint16) int { switch dt { case 1, 2, 6: // BYTE, ASCII, SBYTE return 1 case 3, 8: // SHORT, SSHORT return 2 case 4, 9, 11: // LONG, SLONG, FLOAT return 4 case 5, 10, 12: // RATIONAL, SRATIONAL, DOUBLE return 8 } return 0 } // scalarUint32 读取条目的单个标量值(适用于 count=1 的 SHORT/LONG). func (e *tiffIFDEntry) scalarUint32(bo binary.ByteOrder) uint32 { switch e.dataType { case 3: // SHORT return uint32(bo.Uint16(e.raw[:])) case 4: // LONG return bo.Uint32(e.raw[:]) } return 0 } // allUint32s 读取条目的全部值,处理 inline 和 offset 两种存储方式. // 精妙之处(CLEVER): TIFF 规定当 count * typeSize ≤ 4 时,值直接存在 // value/offset 字段的开头(不是指针).这个判断是正确解码的关键, // 很多简单实现都在这里犯错--把 inline SHORT 当成 offset 来读. func (e *tiffIFDEntry) allUint32s(data []byte, bo binary.ByteOrder) []uint32 { sz := typeSizeOf(e.dataType) if sz == 0 || e.count == 0 { return nil } total := sz * int(e.count) var src []byte if total <= 4 { // Inline:直接从 raw 字段读取 src = e.raw[:] } else { // Offset:raw 是指向数据的偏移量 off := int(bo.Uint32(e.raw[:])) if off+total > len(data) { return nil } src = data[off:] } result := make([]uint32, e.count) for i := range result { switch e.dataType { case 3: // SHORT (2 bytes) result[i] = uint32(bo.Uint16(src[i*2:])) case 4: // LONG (4 bytes) result[i] = bo.Uint32(src[i*4:]) } } return result } // readTIFFIFD 解析 IFD0 的全部条目. func readTIFFIFD(data []byte, bo binary.ByteOrder) ([]tiffIFDEntry, error) { if len(data) < 8 { return nil, errors.New("tiff: data too short") } ifd0Off := int(bo.Uint32(data[4:8])) if ifd0Off+2 > len(data) { return nil, errors.New("tiff: IFD0 offset out of range") } count := int(bo.Uint16(data[ifd0Off:])) entries := make([]tiffIFDEntry, 0, count) pos := ifd0Off + 2 for i := 0; i < count; i++ { if pos+12 > len(data) { break } var e tiffIFDEntry e.tag = bo.Uint16(data[pos:]) e.dataType = bo.Uint16(data[pos+2:]) e.count = bo.Uint32(data[pos+4:]) copy(e.raw[:], data[pos+8:pos+12]) entries = append(entries, e) pos += 12 } return entries, nil } // findTag 在条目列表中查找指定 tag 的标量值(count=1). func findTag(entries []tiffIFDEntry, tag uint16, bo binary.ByteOrder) (uint32, bool) { for _, e := range entries { if e.tag == tag { return e.scalarUint32(bo), true } } return 0, false } // findTagValues 在条目列表中查找指定 tag 的全部值. func findTagValues(entries []tiffIFDEntry, tag uint16, data []byte, bo binary.ByteOrder) []uint32 { for _, e := range entries { if e.tag == tag { return e.allUint32s(data, bo) } } return nil } // decodeTIFF 解码 TIFF 文件字节流为 image.Image. // // 支持的压缩格式: // - 无压缩(tiffCompNone=1):原始像素,直接读取 // - LZW(tiffCompLZW=5):标准库 compress/lzw,MSB-first // - PackBits(tiffCompPackBits=32773):简单 RLE,自实现 ~25 行 // // 支持的颜色模型: // - RGB(samplesPerPixel=3,photometric=2) // - RGBA(samplesPerPixel=4) // - 灰度(samplesPerPixel=1,photometric=0/1) // // 不支持(fail-open 返回 error): // - CCITT Group 3/4 传真压缩(传真图不会发给 vision API) // - JPEG-in-TIFF(嵌套解码过于复杂,实际场景极少) // - CMYK,YCbCr 等非 RGB 颜色模型 // - Planar(分离平面)存储格式 // - 16-bit 及以上位深 func decodeTIFF(data []byte) (image.Image, error) { if len(data) < 8 { return nil, errors.New("tiff: too short") } // 1. 确定字节序 var bo binary.ByteOrder switch { case data[0] == 'I' && data[1] == 'I': bo = binary.LittleEndian case data[0] == 'M' && data[1] == 'M': bo = binary.BigEndian default: return nil, errors.New("tiff: invalid byte order marker") } if bo.Uint16(data[2:4]) != 42 { return nil, errors.New("tiff: invalid magic (expected 42)") } // 2. 读 IFD0 条目 entries, err := readTIFFIFD(data, bo) if err != nil { return nil, err } // 3. 提取必要标签 width, ok := findTag(entries, tiffTagImageWidth, bo) if !ok || width == 0 { return nil, errors.New("tiff: missing or zero ImageWidth") } height, ok := findTag(entries, tiffTagImageLength, bo) if !ok || height == 0 { return nil, errors.New("tiff: missing or zero ImageLength") } compression, _ := findTag(entries, tiffTagCompression, bo) if compression == 0 { compression = tiffCompNone // 默认无压缩 } photometric, _ := findTag(entries, tiffTagPhotometric, bo) samplesPerPixel, ok := findTag(entries, tiffTagSamplesPerPixel, bo) if !ok { samplesPerPixel = 3 // 默认 RGB } rowsPerStrip, ok := findTag(entries, tiffTagRowsPerStrip, bo) if !ok { rowsPerStrip = height // 单条带 } predictor, _ := findTag(entries, tiffTagPredictor, bo) planar, _ := findTag(entries, tiffTagPlanarConfig, bo) stripOffsets := findTagValues(entries, tiffTagStripOffsets, data, bo) stripByteCounts := findTagValues(entries, tiffTagStripByteCounts, data, bo) // 4. 检查支持范围 if compression != tiffCompNone && compression != tiffCompLZW && compression != tiffCompDeflate && compression != tiffCompPackBits { return nil, fmt.Errorf("tiff: unsupported compression %d (CCITT/JPEG-in-TIFF not supported)", compression) } if samplesPerPixel != 1 && samplesPerPixel != 3 && samplesPerPixel != 4 { return nil, fmt.Errorf("tiff: unsupported samples per pixel: %d", samplesPerPixel) } if planar == 2 { return nil, errors.New("tiff: planar (separated channel) format not supported") } if len(stripOffsets) == 0 { return nil, errors.New("tiff: no strip offsets found") } // 5. 分配输出图片 dst := image.NewNRGBA(image.Rect(0, 0, int(width), int(height))) rowStride := int(width) * int(samplesPerPixel) // 每行未压缩字节数 rowsPS := int(rowsPerStrip) // 6. 逐条带解码 for i, offset := range stripOffsets { // 获取条带原始字节 var stripRaw []byte if i < len(stripByteCounts) { bc := int(stripByteCounts[i]) if int(offset)+bc > len(data) { return nil, fmt.Errorf("tiff: strip %d out of bounds", i) } stripRaw = data[offset : int(offset)+bc] } else { // 无 StripByteCounts:假设到文件末尾 if int(offset) > len(data) { return nil, fmt.Errorf("tiff: strip %d offset out of bounds", i) } stripRaw = data[offset:] } // 解压 var pixels []byte switch compression { case tiffCompNone: pixels = stripRaw case tiffCompLZW: // 精妙之处(CLEVER): 必须用 golang.org/x/image/tiff/lzw 而非 compress/lzw. // TIFF LZW 有"off by one"差异(Aldus 实现):代码宽度在表填满前一项即增加. // 标准 compress/lzw 不知道此差异,会产生 "invalid code" 错误. // x/image/tiff/lzw 专为 TIFF 设计,已在 go.mod 中. r := tifflzw.NewReader(bytes.NewReader(stripRaw), tifflzw.MSB, 8) pixels, err = io.ReadAll(r) r.Close() if err != nil { return nil, fmt.Errorf("tiff: LZW decompress strip %d: %w", i, err) } case tiffCompDeflate: zr, zerr := zlib.NewReader(bytes.NewReader(stripRaw)) if zerr != nil { return nil, fmt.Errorf("tiff: Deflate open strip %d: %w", i, zerr) } pixels, err = io.ReadAll(zr) zr.Close() if err != nil { return nil, fmt.Errorf("tiff: Deflate decompress strip %d: %w", i, err) } case tiffCompPackBits: pixels, err = tiffUnpackBits(stripRaw) if err != nil { return nil, fmt.Errorf("tiff: PackBits strip %d: %w", i, err) } } // 应用水平差分预测器(predictor=2,常见于 LZW 压缩的 RGB 图片) // 原理:存储的是相邻像素的差值而非绝对值,提升压缩率. // 解码时需要还原:pixel[i] = stored[i] + pixel[i-1] if predictor == 2 && compression != tiffCompNone { tiffUndoHorizontalDiff(pixels, rowStride, int(samplesPerPixel)) } // 将条带像素写入输出图片 startRow := i * rowsPS for row := 0; row < rowsPS && startRow+row < int(height); row++ { rowOff := row * rowStride if rowOff+rowStride > len(pixels) { break } y := startRow + row for x := 0; x < int(width); x++ { pOff := rowOff + x*int(samplesPerPixel) var r, g, b, a uint8 switch samplesPerPixel { case 1: // 灰度 r = pixels[pOff] g, b, a = r, r, 255 if photometric == tiffPhotoWhiteIsZero { r = 255 - r g, b = r, r } case 3: // RGB r, g, b, a = pixels[pOff], pixels[pOff+1], pixels[pOff+2], 255 if photometric == tiffPhotoWhiteIsZero { r, g, b = 255-r, 255-g, 255-b } case 4: // RGBA r, g, b, a = pixels[pOff], pixels[pOff+1], pixels[pOff+2], pixels[pOff+3] } dst.SetNRGBA(x, y, color.NRGBA{R: r, G: g, B: b, A: a}) } } } return dst, nil } // ───────────────────────────────────────────────────────────────────── // PackBits 解压(TIFF 压缩格式 32773) // ───────────────────────────────────────────────────────────────────── // tiffUnpackBits 实现 PackBits RLE 解压算法. // // PackBits 规则(n = int8(byte)): // // n >= 0:读后续 n+1 个字节并原样输出(字面量游程) // n == -128:跳过(无操作,用于填充对齐) // n < 0(且 n != -128):读后续 1 个字节,重复输出 1-n 次(重复游程) // // 精妙之处(CLEVER): 用 int8 转换实现有符号解释:int8(0x80) = -128, // int8(0x81) = -127(重复 128 次),int8(0x00) = 0(字面量 1 字节). // 避免了手动处理补码. func tiffUnpackBits(src []byte) ([]byte, error) { dst := make([]byte, 0, len(src)*2) // 预分配,PackBits 最坏 2x 膨胀 i := 0 for i < len(src) { n := int(int8(src[i])) i++ switch { case n >= 0: // 字面量:复制 n+1 个字节 count := n + 1 if i+count > len(src) { return nil, fmt.Errorf("packbits: literal run truncated at byte %d", i) } dst = append(dst, src[i:i+count]...) i += count case n == -128: // 无操作 continue default: // 重复:复制 1 个字节 (1-n) 次 if i >= len(src) { return nil, fmt.Errorf("packbits: repeat run truncated at byte %d", i) } b := src[i] i++ count := 1 - n for j := 0; j < count; j++ { dst = append(dst, b) } } } return dst, nil } // ───────────────────────────────────────────────────────────────────── // 水平差分预测器 // ───────────────────────────────────────────────────────────────────── // tiffUndoHorizontalDiff 还原 LZW 水平差分预测器(TIFF Predictor=2). // // 水平差分预测的工作原理: // // 编码时:存储 pixel[i] - pixel[i-1](差值) // 解码时:还原 pixel[i] = stored[i] + pixel[i-1](前缀和) // // 每行独立处理,行首像素无需还原(差值即原值). // samplesPerPixel 控制"颜色分量"步进--对 RGB 图片, // R/G/B 分量各自独立做差分,不跨分量相减. func tiffUndoHorizontalDiff(pixels []byte, rowStride, samplesPerPixel int) { rows := len(pixels) / rowStride for row := 0; row < rows; row++ { base := row * rowStride // 从第二个像素开始(索引 samplesPerPixel) for col := samplesPerPixel; col < rowStride; col++ { pixels[base+col] += pixels[base+col-samplesPerPixel] } } }