// fileread_tiff_test.go -- TIFF 解码与方向校正测试. package builtin import ( "bytes" "encoding/binary" "image" "image/color" "image/jpeg" "testing" ) // ───────────────────────────────────────────────────────────────────── // 辅助:构建最小 TIFF 字节流 // ───────────────────────────────────────────────────────────────────── // buildMinimalTIFF 构建一个最小合法的 TIFF 文件字节流. // 无压缩 RGB,宽×高像素,包含指定 Orientation. func buildMinimalTIFF(t *testing.T, width, height int, orient exifOrientation) []byte { t.Helper() // 构建像素数据(渐变色,便于验证旋转正确性) pixels := make([]byte, width*height*3) for y := 0; y < height; y++ { for x := 0; x < width; x++ { off := (y*width + x) * 3 pixels[off] = uint8(x * 64) // R pixels[off+1] = uint8(y * 64) // G pixels[off+2] = 128 // B } } var buf bytes.Buffer bo := binary.LittleEndian // TIFF 头:II + 42 + IFD0 offset buf.Write([]byte("II")) _ = binary.Write(&buf, bo, uint16(42)) // IFD0 在头部(8字节)之后,像素数据再之后 // 结构:header(8) + pixels(w*h*3) + IFD0 pixelOffset := uint32(8) ifd0Offset := pixelOffset + uint32(len(pixels)) _ = binary.Write(&buf, bo, ifd0Offset) // 像素数据 buf.Write(pixels) // IFD0:11 个条目 tags := []struct { tag uint16 dataType uint16 count uint32 value uint32 }{ {tiffTagImageWidth, 4, 1, uint32(width)}, {tiffTagImageLength, 4, 1, uint32(height)}, {tiffTagBitsPerSample, 3, 1, 8}, {tiffTagCompression, 3, 1, tiffCompNone}, {tiffTagPhotometric, 3, 1, tiffPhotoRGB}, {tiffTagStripOffsets, 4, 1, pixelOffset}, {0x0112, 3, 1, uint32(orient)}, // Orientation {tiffTagSamplesPerPixel, 3, 1, 3}, {tiffTagRowsPerStrip, 4, 1, uint32(height)}, {tiffTagStripByteCounts, 4, 1, uint32(len(pixels))}, {tiffTagPlanarConfig, 3, 1, 1}, } _ = binary.Write(&buf, bo, uint16(len(tags))) for _, tag := range tags { _ = binary.Write(&buf, bo, tag.tag) _ = binary.Write(&buf, bo, tag.dataType) _ = binary.Write(&buf, bo, tag.count) _ = binary.Write(&buf, bo, tag.value) } _ = binary.Write(&buf, bo, uint32(0)) // 无下一 IFD return buf.Bytes() } // ───────────────────────────────────────────────────────────────────── // parseTIFFOrientation 测试 // ───────────────────────────────────────────────────────────────────── func TestParseTIFFOrientation_Normal(t *testing.T) { data := buildMinimalTIFF(t, 4, 2, exifOrientNormal) got := parseTIFFOrientation(data) if got != exifOrientNormal { t.Errorf("got %d, want %d", got, exifOrientNormal) } } func TestParseTIFFOrientation_Rotate90CW(t *testing.T) { data := buildMinimalTIFF(t, 4, 2, exifOrientRotate90CW) got := parseTIFFOrientation(data) if got != exifOrientRotate90CW { t.Errorf("got %d, want %d", got, exifOrientRotate90CW) } } func TestParseTIFFOrientation_AllValues(t *testing.T) { for _, orient := range []exifOrientation{1, 2, 3, 4, 5, 6, 7, 8} { data := buildMinimalTIFF(t, 4, 2, orient) got := parseTIFFOrientation(data) if got != orient { t.Errorf("orient=%d: got %d", orient, got) } } } func TestParseTIFFOrientation_InvalidData(t *testing.T) { got := parseTIFFOrientation([]byte{0x00, 0x01}) if got != 1 { t.Errorf("无效数据应返回 1,got %d", got) } } // ───────────────────────────────────────────────────────────────────── // decodeTIFF 测试 // ───────────────────────────────────────────────────────────────────── func TestDecodeTIFF_UncompressedRGB(t *testing.T) { data := buildMinimalTIFF(t, 4, 2, exifOrientNormal) img, err := decodeTIFF(data) if err != nil { t.Fatalf("decodeTIFF 失败: %v", err) } bounds := img.Bounds() if bounds.Max.X != 4 || bounds.Max.Y != 2 { t.Errorf("期望 4×2,got %d×%d", bounds.Max.X, bounds.Max.Y) } } func TestDecodeTIFF_InvalidMagic(t *testing.T) { _, err := decodeTIFF([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}) if err == nil { t.Error("无效 magic 应返回 error") } } func TestDecodeTIFF_TooShort(t *testing.T) { _, err := decodeTIFF([]byte{0x49, 0x49}) if err == nil { t.Error("太短应返回 error") } } // ───────────────────────────────────────────────────────────────────── // correctTIFFImage 测试 // ───────────────────────────────────────────────────────────────────── func TestCorrectTIFFImage_NormalOutputJPEG(t *testing.T) { data := buildMinimalTIFF(t, 4, 2, exifOrientNormal) result, mt := correctTIFFImage(data) // 应输出 JPEG if mt != "image/jpeg" { t.Errorf("期望 image/jpeg,got %s", mt) } // 验证是合法 JPEG _, _, err := image.Decode(bytes.NewReader(result)) if err != nil { t.Fatalf("输出不是合法 JPEG: %v", err) } } func TestCorrectTIFFImage_Rotate90CW_DimensionSwapped(t *testing.T) { // 4×2,Orientation=6 (顺时针90°) → 输出应为 2×4 data := buildMinimalTIFF(t, 4, 2, exifOrientRotate90CW) result, mt := correctTIFFImage(data) if mt != "image/jpeg" { t.Errorf("期望 image/jpeg,got %s", mt) } img, _, err := image.Decode(bytes.NewReader(result)) if err != nil { t.Fatalf("输出解码失败: %v", err) } b := img.Bounds() if b.Max.X != 2 || b.Max.Y != 4 { t.Errorf("Rotate90CW 后应为 2×4,got %d×%d", b.Max.X, b.Max.Y) } } func TestCorrectTIFFImage_InvalidData_ReturnOriginal(t *testing.T) { garbage := []byte{0x00, 0x01, 0x02} result, mt := correctTIFFImage(garbage) if !bytes.Equal(result, garbage) { t.Error("无效数据应原样返回") } if mt != "image/tiff" { t.Errorf("期望 image/tiff,got %s", mt) } } // ───────────────────────────────────────────────────────────────────── // tiffUnpackBits 测试 // ───────────────────────────────────────────────────────────────────── func TestTiffUnpackBits_Literal(t *testing.T) { // n=2 → 复制后续 3 个字节 src := []byte{0x02, 0xAA, 0xBB, 0xCC} got, err := tiffUnpackBits(src) if err != nil { t.Fatalf("unexpected error: %v", err) } want := []byte{0xAA, 0xBB, 0xCC} if !bytes.Equal(got, want) { t.Errorf("literal: got %v, want %v", got, want) } } func TestTiffUnpackBits_Repeat(t *testing.T) { // n=-3(0xFD)→ 重复下一个字节 4 次(1-(-3)=4) src := []byte{0xFD, 0x42} got, err := tiffUnpackBits(src) if err != nil { t.Fatalf("unexpected error: %v", err) } want := []byte{0x42, 0x42, 0x42, 0x42} if !bytes.Equal(got, want) { t.Errorf("repeat: got %v, want %v", got, want) } } func TestTiffUnpackBits_Noop(t *testing.T) { // n=-128(0x80)→ 无操作,后接 n=0x00(字面量1字节0xFF) src := []byte{0x80, 0x00, 0xFF} got, err := tiffUnpackBits(src) if err != nil { t.Fatalf("unexpected error: %v", err) } want := []byte{0xFF} if !bytes.Equal(got, want) { t.Errorf("noop: got %v, want %v", got, want) } } // ───────────────────────────────────────────────────────────────────── // detectImageMediaType TIFF/HEIC 测试 // ───────────────────────────────────────────────────────────────────── func TestDetectImageMediaType_TIFF_LE(t *testing.T) { data := []byte{0x49, 0x49, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x08} got := detectImageMediaType(data) if got != "image/tiff" { t.Errorf("LE TIFF: got %s", got) } } func TestDetectImageMediaType_TIFF_BE(t *testing.T) { data := []byte{0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08} got := detectImageMediaType(data) if got != "image/tiff" { t.Errorf("BE TIFF: got %s", got) } } func TestDetectImageMediaType_HEIC(t *testing.T) { // ftyp box: size(4) + "ftyp" + "heic" data := make([]byte, 12) binary.BigEndian.PutUint32(data[0:4], 24) // box size copy(data[4:8], "ftyp") copy(data[8:12], "heic") got := detectImageMediaType(data) if got != "image/heic" { t.Errorf("HEIC: got %s", got) } } // ───────────────────────────────────────────────────────────────────── // tiffUndoHorizontalDiff 测试 // ───────────────────────────────────────────────────────────────────── func TestTiffUndoHorizontalDiff(t *testing.T) { // 一行 RGB 差分数据: [R0,G0,B0, dR1,dG1,dB1] // 解码后: [R0,G0,B0, R0+dR1, G0+dG1, B0+dB1] pixels := []byte{10, 20, 30, 5, 3, 1} tiffUndoHorizontalDiff(pixels, 6, 3) want := []byte{10, 20, 30, 15, 23, 31} if !bytes.Equal(pixels, want) { t.Errorf("got %v, want %v", pixels, want) } } // ───────────────────────────────────────────────────────────────────── // TIFF 灰度图测试 // ───────────────────────────────────────────────────────────────────── func buildGrayscaleTIFF(t *testing.T, width, height int) []byte { t.Helper() pixels := make([]byte, width*height) for i := range pixels { pixels[i] = uint8(i * 10) } var buf bytes.Buffer bo := binary.LittleEndian buf.Write([]byte("II")) _ = binary.Write(&buf, bo, uint16(42)) pixelOffset := uint32(8) ifd0Offset := pixelOffset + uint32(len(pixels)) _ = binary.Write(&buf, bo, ifd0Offset) buf.Write(pixels) tags := []struct { tag, dt uint16 cnt, val uint32 }{ {tiffTagImageWidth, 4, 1, uint32(width)}, {tiffTagImageLength, 4, 1, uint32(height)}, {tiffTagBitsPerSample, 3, 1, 8}, {tiffTagCompression, 3, 1, tiffCompNone}, {tiffTagPhotometric, 3, 1, tiffPhotoBlackIsZero}, {tiffTagStripOffsets, 4, 1, pixelOffset}, {tiffTagSamplesPerPixel, 3, 1, 1}, {tiffTagRowsPerStrip, 4, 1, uint32(height)}, {tiffTagStripByteCounts, 4, 1, uint32(len(pixels))}, } _ = binary.Write(&buf, bo, uint16(len(tags))) for _, tag := range tags { _ = binary.Write(&buf, bo, tag.tag) _ = binary.Write(&buf, bo, tag.dt) _ = binary.Write(&buf, bo, tag.cnt) _ = binary.Write(&buf, bo, tag.val) } _ = binary.Write(&buf, bo, uint32(0)) return buf.Bytes() } func TestDecodeTIFF_Grayscale(t *testing.T) { data := buildGrayscaleTIFF(t, 4, 2) img, err := decodeTIFF(data) if err != nil { t.Fatalf("grayscale decodeTIFF 失败: %v", err) } bounds := img.Bounds() if bounds.Max.X != 4 || bounds.Max.Y != 2 { t.Errorf("尺寸错误: %d×%d", bounds.Max.X, bounds.Max.Y) } // 验证 (0,0) 是黑色(第一个像素值=0) r, g, b, _ := img.At(0, 0).RGBA() if r != 0 || g != 0 || b != 0 { t.Errorf("(0,0) 应为黑色,got (%d,%d,%d)", r, g, b) } } // ───────────────────────────────────────────────────────────────────── // 全流程:TIFF → correctTIFFImage → JPEG 像素验证 // ───────────────────────────────────────────────────────────────────── func TestCorrectTIFFImage_PixelContent(t *testing.T) { // 构建 2×1 TIFF:左像素红色(255,0,0),右像素蓝色(0,0,255) pixels := []byte{255, 0, 0, 0, 0, 255} // R G B | R G B var buf bytes.Buffer bo := binary.LittleEndian buf.Write([]byte("II")) _ = binary.Write(&buf, bo, uint16(42)) pixelOffset := uint32(8) ifd0Offset := pixelOffset + uint32(len(pixels)) _ = binary.Write(&buf, bo, ifd0Offset) buf.Write(pixels) tags := []struct { tag, dt uint16 cnt, val uint32 }{ {tiffTagImageWidth, 4, 1, 2}, {tiffTagImageLength, 4, 1, 1}, {tiffTagBitsPerSample, 3, 1, 8}, {tiffTagCompression, 3, 1, tiffCompNone}, {tiffTagPhotometric, 3, 1, tiffPhotoRGB}, {tiffTagStripOffsets, 4, 1, pixelOffset}, {tiffTagSamplesPerPixel, 3, 1, 3}, {tiffTagRowsPerStrip, 4, 1, 1}, {tiffTagStripByteCounts, 4, 1, 6}, } _ = binary.Write(&buf, bo, uint16(len(tags))) for _, tag := range tags { _ = binary.Write(&buf, bo, tag.tag) _ = binary.Write(&buf, bo, tag.dt) _ = binary.Write(&buf, bo, tag.cnt) _ = binary.Write(&buf, bo, tag.val) } _ = binary.Write(&buf, bo, uint32(0)) result, mt := correctTIFFImage(buf.Bytes()) if mt != "image/jpeg" { t.Fatalf("期望 image/jpeg,got %s", mt) } // 解码并检查尺寸 img, err := jpeg.Decode(bytes.NewReader(result)) if err != nil { t.Fatalf("输出 JPEG 解码失败: %v", err) } b := img.Bounds() if b.Max.X != 2 || b.Max.Y != 1 { t.Errorf("尺寸错误: %d×%d", b.Max.X, b.Max.Y) } // JPEG 有损压缩会改变颜色,只验证左像素 R 分量 > 右像素 R 分量 // (左=红色,右=蓝色,R通道上左 > 右是基本保证) _ = color.NRGBA{} // 确保 color 包被使用 lr, _, _, _ := img.At(0, 0).RGBA() rr, _, _, _ := img.At(1, 0).RGBA() if lr <= rr { t.Errorf("左像素(红)R分量应 > 右像素(蓝)R分量,got left=%d right=%d", lr>>8, rr>>8) } }