// fileread_heic.go -- HEIC/HEIF/AVIF 图片解码与 JPEG 转换(CGO + libheif). // // HEIC(High Efficiency Image Container)是 Apple 于 iOS 11(2017)起 // 设定的默认相机格式.基于 ISOBMFF 容器 + HEVC(H.265)编解码. // Anthropic vision API 不接受 HEIC,必须先转换为 JPEG. // // 技术路线选择(ELEVATED): // - 纯 Go 实现 HEVC intra 解码:~5000-8000 行,数周工作量 // - 内联 C 源码(libde265/libheif):数万行 C++,构建系统复杂 // - CGO + 系统 libheif.so:~200 行 Go,零 C 代码维护 // → 选 CGO + libheif.libheif 已随 apt 安装,是 Linux 发行版标准组件. // "零外部依赖"原则针对 Go 包(go.mod),不妨碍使用系统 codec 库. // 类比:go-sqlite3,bimg + libvips 也是此模式. // // EXIF Orientation 处理(ELEVATED): // libheif 只自动处理 ISOBMFF 级别的方向变换(irot/imir box), // 不处理 EXIF metadata 中的 Orientation 标签. // 早期方案代码注释"libheif 自动应用 EXIF orientation"--这是错的! // 真实测试表明:纯 EXIF orientation 的 AVIF/HEIC 文件,libheif 不旋转. // // 正确策略: // 1. 比较 ispe(物理像素)和 logical(逻辑)尺寸 // 2. 若尺寸相同:libheif 没有应用 ISOBMFF 变换 → 读 EXIF 手动旋转 // 3. 若尺寸不同:libheif 已应用 ISOBMFF 变换(irot/imir) → 跳过 // // 构建要求: // apt-get install libheif-dev libde265-dev // CGO_ENABLED=1(默认开启) // // 部署要求: // apt-get install libheif1 libde265-0 // (Ubuntu/Debian 标准仓库均有) //go:build libheif && cgo package builtin /* #cgo LDFLAGS: -lheif #include #include */ import "C" import ( "bytes" "errors" "fmt" "image" "image/color" "image/jpeg" "unsafe" ) // correctHEICToJPEG 解码 HEIC/HEIF/AVIF 文件,校正方向,重新编码为 JPEG. // // fail-open:任何 CGO 调用失败都返回原始数据 + 原始 media type, // 不影响 FileRead 主流程. func correctHEICToJPEG(data []byte) ([]byte, string) { img, err := decodeHEIC(data) if err != nil { return data, "image/heic" // fail-open } var buf bytes.Buffer if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 92}); err != nil { return data, "image/heic" // fail-open } return buf.Bytes(), "image/jpeg" } // decodeHEIC 通过 libheif 将 HEIC/HEIF/AVIF 字节解码为 image.Image. // // 方向处理策略(两阶段): // // 阶段1:libheif 解码时自动应用 ISOBMFF irot/imir 变换 // → 通过比较 ispe(物理)和 logical 尺寸来检测是否已旋转 // // 阶段2:若 libheif 未旋转(ispe == logical),读 EXIF Orientation // → 用我们的 applyOrientation 手动旋转 // // 精妙之处(CLEVER): 两阶段检测避免了"双重旋转": // // iPhone 照片通常既有 irot box 又有 EXIF orientation,二者一致. // 如果只看 EXIF 而 libheif 已经旋了 irot,就会旋转两次→画面错误. // ispe vs logical 比较是判断 libheif 是否已旋的唯一可靠方式. func decodeHEIC(data []byte) (image.Image, error) { if len(data) == 0 { return nil, errors.New("heic: empty data") } // 分配 heif_context(解码会话) ctx := C.heif_context_alloc() if ctx == nil { return nil, errors.New("heic: heif_context_alloc failed") } defer C.heif_context_free(ctx) // 从内存读取文件 // 精妙之处(CLEVER): without_copy 变体--libheif 直接引用 Go 切片内存, // 避免 MB 级拷贝.CGO 调用期间 Go GC 不会移动该内存(CGO 规则保证). ptr := unsafe.Pointer(&data[0]) herr := C.heif_context_read_from_memory_without_copy(ctx, ptr, C.size_t(len(data)), nil) if herr.code != C.heif_error_Ok { return nil, fmt.Errorf("heic: read failed: %s", C.GoString(herr.message)) } // 获取主图片 handle var handle *C.struct_heif_image_handle herr = C.heif_context_get_primary_image_handle(ctx, &handle) if herr.code != C.heif_error_Ok { return nil, fmt.Errorf("heic: get primary handle failed: %s", C.GoString(herr.message)) } defer C.heif_image_handle_release(handle) // 阶段1:读取 ispe(物理像素)和 logical(含 ISOBMFF 变换后)尺寸 ispeW := int(C.heif_image_handle_get_ispe_width(handle)) ispeH := int(C.heif_image_handle_get_ispe_height(handle)) logW := int(C.heif_image_handle_get_width(handle)) logH := int(C.heif_image_handle_get_height(handle)) // 解码为 RGBA(libheif 自动应用 ISOBMFF irot/imir 变换) var cimg *C.struct_heif_image herr = C.heif_decode_image( handle, &cimg, C.heif_colorspace_RGB, C.heif_chroma_interleaved_RGBA, nil, ) if herr.code != C.heif_error_Ok { return nil, fmt.Errorf("heic: decode failed: %s", C.GoString(herr.message)) } defer C.heif_image_release(cimg) w, h := logW, logH if w <= 0 || h <= 0 { return nil, fmt.Errorf("heic: invalid dimensions %dx%d", w, h) } // 获取像素平面(RGBA interleaved) var stride C.int plane := C.heif_image_get_plane_readonly(cimg, C.heif_channel_interleaved, &stride) if plane == nil { return nil, errors.New("heic: get plane failed") } // 拷贝像素到 Go 内存 // 精妙之处(CLEVER): CGO 规则禁止在 C 函数返回后保留指向 C 内存的 Go 指针. // C.GoBytes 执行必要的拷贝,之后 cimg defer release 可以安全释放 C 内存. strideInt := int(stride) totalBytes := h * strideInt rawBytes := C.GoBytes(unsafe.Pointer(plane), C.int(totalBytes)) // 构建 Go image.NRGBA dst := image.NewNRGBA(image.Rect(0, 0, w, h)) for y := 0; y < h; y++ { srcRow := rawBytes[y*strideInt:] for x := 0; x < w; x++ { off := x * 4 dst.SetNRGBA(x, y, color.NRGBA{ R: srcRow[off], G: srcRow[off+1], B: srcRow[off+2], A: srcRow[off+3], }) } } // 阶段2:若 libheif 未应用 ISOBMFF 旋转(ispe == logical), // 检查 EXIF Orientation 并手动旋转. // // 升华改进(ELEVATED): 这修复了早期方案代码的错误假设. // libheif 只处理 irot/imir box,不处理 EXIF metadata 的 Orientation 标签. // 纯 EXIF orientation 的文件(部分 AVIF,非 Apple 编码的 HEIC) // 必须手动读 EXIF 并旋转,否则 vision API 看到的是侧倒的图片. if ispeW == logW && ispeH == logH { // libheif 未旋转(无 irot/imir box 或已忽略)→ 检查 EXIF orient := readHEICExifOrientation(handle) if orient > exifOrientNormal && orient <= exifOrientRotate90CCW { result := applyOrientation(dst, orient) if rotated, ok := result.(*image.NRGBA); ok { return rotated, nil } // applyOrientation 可能返回非 NRGBA(不应发生) return result, nil } } return dst, nil } // readHEICExifOrientation 从 libheif image handle 中读取 EXIF Orientation 标签. // // HEIF 规范(ISO 23008-12)定义 EXIF metadata item 格式: // // [4字节 BE uint32 headerOffset] [headerOffset字节的任意内容] [TIFF header...] // // 注意:headerOffset 不总是0!libavif 等工具生成的 AVIF 文件常用 headerOffset=10, // payload 内容为 [00 00 00 00 45 78 69 66 00 00](padding + "Exif\0\0"). // 因此 TIFF header 在 buf[4+headerOffset:] 而非 buf[4:]. // // 兼容策略(针对不同编码器): // // 主路径:读 headerOffset → buf[4+headerOffset:] 解析 TIFF // 兜底1:带 "Exif\0\0" 前缀(某些非标准编码器直接放 JPEG-style EXIF) // 兜底2:直接 TIFF(极少数编码器) func readHEICExifOrientation(handle *C.struct_heif_image_handle) exifOrientation { // 查询 "Exif" 类型的 metadata block 数量 exifType := C.CString("Exif") defer C.free(unsafe.Pointer(exifType)) n := int(C.heif_image_handle_get_number_of_metadata_blocks(handle, exifType)) if n == 0 { return 1 // 无 EXIF } // 获取第一个 EXIF block 的 ID ids := make([]C.heif_item_id, n) C.heif_image_handle_get_list_of_metadata_block_IDs(handle, exifType, &ids[0], C.int(n)) // 读取 EXIF 数据 exifSize := int(C.heif_image_handle_get_metadata_size(handle, ids[0])) if exifSize <= 4 { return 1 } exifBuf := make([]byte, exifSize) herr := C.heif_image_handle_get_metadata(handle, ids[0], unsafe.Pointer(&exifBuf[0])) if herr.code != C.heif_error_Ok { return 1 } // HEIF 规范:EXIF metadata item 的前4字节是 big-endian uint32, // 表示 TIFF header 在"EXIF payload"中的偏移量(从第5字节算起). // // 升华改进(ELEVATED): 原代码固定跳4字节(假设 headerOffset=0), // 但真实 AVIF 文件(如 libavif 生成的)headerOffset=10: // buf = [00 00 00 0a] [00 00 00 00 45 78 69 66 00 00] [49 49 2a 00 ...] // ^^^^^^^^^^^^^ ← offset=10 ← TIFF 在 4+10=14 // 早期方案代码跳4字节后看到 [00 00...],无法识别 TIFF → orientation 返回1. // 正确做法:读 BE uint32,跳过 4+headerOffset 字节才到 TIFF header. // // 同时保留多种格式的兼容兜底(某些旧编码器可能用不同格式). if exifSize >= 4 { // 读 big-endian uint32 header offset headerOff := int(uint32(exifBuf[0])<<24 | uint32(exifBuf[1])<<16 | uint32(exifBuf[2])<<8 | uint32(exifBuf[3])) tiffStart := 4 + headerOff if tiffStart < exifSize { if orient, ok := parseTIFFIFDOrientation(exifBuf[tiffStart:]); ok { return orient } } } // 兜底1:带 "Exif\0\0" 前缀格式(某些非标准编码器) if orient, ok := parseEXIFOrientation(exifBuf); ok { return orient } // 兜底2:直接 TIFF(无任何前缀,极少数编码器) if orient, ok := parseTIFFIFDOrientation(exifBuf); ok { return orient } return 1 }