package builtin // fileread_image.go -- FileRead 工具的图片处理逻辑. // // 职责: // - 通过 magic bytes 检测图片实际格式(不信任扩展名) // - 解码图片尺寸(宽/高) // - 构建 ImageResult 供调用方获取结构化图片数据 // // 设计决策: // 反向思考:为什么不用 sharp 或 vips 这样的外部库做缩放? // - Go 没有 sharp 对应物(libvips 的 Go 绑定需要 cgo + 系统库) // - 引入 cgo 会大幅增加编译复杂度和交叉编译难度 // - Go 标准库 image 包足够做格式检测和尺寸获取 // - 对于缩放,采用"小图直传,大图只报信息"策略(见 fileread.go handleImageFile) // // 精妙之处(CLEVER): 图片格式检测用 magic bytes 而非文件扩展名. // 用户可能把 .jpg 文件重命名为 .png(或反之),magic bytes 不会骗人. // 这与原 TS 项目的 detectImageFormatFromBuffer 逻辑一致. import ( "bytes" "image" "io" "os" ) // ImageResult is the structured image payload stored in tools.Result.Data // so callers can access parsed image bytes. // // Field consumption: // - MediaType / Base64 are consumed by the vision wire (engine.go builds // array-form tool_result for Anthropic messages API, see commit // 087393b). These are the fields the model actually receives. // - Width / Height are an external type-assertion surface: consumers // that do `if img, ok := res.Data.(*ImageResult); ok` can read them // for logging / validation / UI rendering. The Anthropic tool_result // wire itself does not consume dimensions. Retained as a structured // field rather than a free-form Meta map because they are canonical // image attributes with a stable type. // // ImageResult 是存于 tools.Result.Data 的结构化图片载荷, 让调用方获取 // 解析后的图片数据. // // 字段消费形态: // - MediaType / Base64 由 vision wire 消费 (engine.go 为 Anthropic // messages API 构造 array-form tool_result, 见 commit 087393b). 这是 // 模型实际收到的字段. // - Width / Height 是外部 type-assert 表面: 消费者经 // `if img, ok := res.Data.(*ImageResult); ok` 后读取, 用于日志 / // 校验 / UI 渲染. Anthropic tool_result wire 本身不消费尺寸. 保留为 // 结构化字段而非 free-form Meta map, 因其是规范的图片属性, 有稳定类型. type ImageResult struct { MediaType string // "image/png", "image/jpeg", "image/gif", "image/webp" Base64 string // base64 编码的图片数据 Width int // pull API, 图片宽度 (外部 type-assert 消费, vision wire 不读) Height int // pull API, 图片高度 (外部 type-assert 消费, vision wire 不读) } // imageDimensions 存储图片尺寸. type imageDimensions struct { Width int Height int } // ───────────────────────────────────────────────────────────────────── // Magic Bytes 图片格式检测 // ───────────────────────────────────────────────────────────────────── // PNG magic: 89 50 4E 47 0D 0A 1A 0A var pngMagic = []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} // JPEG magic: FF D8 FF var jpegMagic = []byte{0xFF, 0xD8, 0xFF} // GIF magic: "GIF87a" 或 "GIF89a" var gif87Magic = []byte("GIF87a") var gif89Magic = []byte("GIF89a") // WEBP magic: "RIFF" + 4 bytes size + "WEBP" var riffMagic = []byte("RIFF") var webpMagic = []byte("WEBP") // TIFF magic: "II\x2A\x00"(小端)或 "MM\x00\x2A"(大端) var tiffLEMagic = []byte{0x49, 0x49, 0x2A, 0x00} // "II" + 42(LE) var tiffBEMagic = []byte{0x4D, 0x4D, 0x00, 0x2A} // "MM" + 42(BE) // HEIC/HEIF magic: 字节 4-7 为 "ftyp",字节 8-11 为品牌标识 // 常见品牌: heic(iPhone), heix(iPhone HDR), mif1(通用 HEIF) var ftypMagic = []byte("ftyp") // heicBrands 是 HEIC/HEIF/AVIF 的 ISO 品牌标识符集合(ISOBMFF 家族). // 精妙之处(CLEVER): 用 map[string]struct{} 而非 []string, // O(1) 查找 vs O(n) 遍历--对高频调用的 magic 检测更优. // // AVIF(AV1 Image File Format)与 HEIC 同为 ISOBMFF 容器, // 都用 libheif 解码(libheif-plugin-aomdec 支持 AVIF), // 因此一并列入统一走 correctHEICToJPEG 路径. var heicBrands = map[string]struct{}{ // HEIC(Apple iPhone 默认格式,HEVC 编码) "heic": {}, "heix": {}, "hevc": {}, "hevx": {}, "heim": {}, "heis": {}, "hevm": {}, "hevs": {}, // HEIF 通用 "mif1": {}, "msf1": {}, // AVIF(AV1 编码,现代浏览器默认,Chrome/Firefox 支持) "avif": {}, "avis": {}, // MIAF(多图像应用格式,HEIF 子规范) "miaf": {}, } // detectImageMediaType 通过 magic bytes 检测图片格式. // 返回 MIME 类型字符串,如 "image/png". // 如果无法识别,默认返回 "image/png". // // 历史包袱(LEGACY): 默认返回 "image/png" 而不是报错, // 是因为 API 层需要一个有效的 media_type. // 原 TS 项目也是这样处理的(detectImageFormatFromBuffer 默认返回 'image/png'). func detectImageMediaType(data []byte) string { if len(data) < 4 { return "image/png" // 默认 } // PNG if len(data) >= 8 && bytes.Equal(data[:8], pngMagic) { return "image/png" } // JPEG if bytes.HasPrefix(data, jpegMagic) { return "image/jpeg" } // GIF if len(data) >= 6 && (bytes.Equal(data[:6], gif87Magic) || bytes.Equal(data[:6], gif89Magic)) { return "image/gif" } // WEBP: "RIFF" + 4 bytes + "WEBP" if len(data) >= 12 && bytes.Equal(data[:4], riffMagic) && bytes.Equal(data[8:12], webpMagic) { return "image/webp" } // TIFF: "II\x2A\x00" 或 "MM\x00\x2A" if bytes.HasPrefix(data, tiffLEMagic) || bytes.HasPrefix(data, tiffBEMagic) { return "image/tiff" } // HEIC/HEIF: 字节 4-7 = "ftyp",字节 8-11 = 品牌 if len(data) >= 12 && bytes.Equal(data[4:8], ftypMagic) { brand := string(data[8:12]) if _, ok := heicBrands[brand]; ok { return "image/heic" } } return "image/png" // 默认回退 } // ───────────────────────────────────────────────────────────────────── // 图片尺寸获取 // ───────────────────────────────────────────────────────────────────── // decodeImageDimensions 从图片数据中解码尺寸信息. // 使用 Go 标准库的 image.DecodeConfig,只解码头部不加载整个图片. // // 精妙之处(CLEVER): DecodeConfig 只读取图片头部的最少字节即可获取尺寸, // 不需要解码整个图片到内存中.对于 10MB 的 PNG,只需读几百字节. // 但需要对应格式的解码器已注册(通过 _ "image/png" 等 import 实现). // // 历史包袱(LEGACY): webp 格式在 Go 标准库中没有解码器, // DecodeConfig 会返回错误.这种情况下返回 nil(无尺寸信息). // 可以引入 golang.org/x/image/webp 但我们避免外部依赖. func decodeImageDimensions(data []byte) *imageDimensions { reader := bytes.NewReader(data) config, _, err := image.DecodeConfig(reader) if err != nil { return nil } return &imageDimensions{ Width: config.Width, Height: config.Height, } } // getImageDimensions 从已打开的文件中获取图片尺寸. // 会 seek 回当前位置,不影响调用方的读取位置. func getImageDimensions(f *os.File) *imageDimensions { // 保存当前位置 currentPos, err := f.Seek(0, io.SeekCurrent) if err != nil { return nil } // 确保 seek 回原位 defer func() { _, _ = f.Seek(currentPos, io.SeekStart) }() // seek 到文件开头 if _, err := f.Seek(0, io.SeekStart); err != nil { return nil } config, _, err := image.DecodeConfig(f) if err != nil { return nil } return &imageDimensions{ Width: config.Width, Height: config.Height, } }