feat: add HEIC/HEIF image format support for Gemini channel (#4049)

* feat: add HEIC/HEIF image format support

Add detection, MIME type mapping, and dimension parsing for HEIC/HEIF
images via ISOBMFF ftyp brand inspection and ispe box parsing. Update
Gemini relay to accept these formats and refactor getImageConfig to
properly retry decoders using buffered data.

* fix: handle ISOBMFF extended sizes in HEIF dimension parser

parseHEIFDimensions now correctly handles boxSize==1 (64-bit extended
size) and boxSize==0 (box-to-EOF), preventing the parser from breaking
out of the loop when encountering these valid ISOBMFF box headers
before reaching the meta box.
This commit is contained in:
Calcium-Ion 2026-04-02 21:32:42 +08:00 committed by GitHub
commit 9816ad87e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 155 additions and 13 deletions

View File

@ -37,6 +37,8 @@ var geminiSupportedMimeTypes = map[string]bool{
"image/jpeg": true,
"image/jpg": true, // support old image/jpeg
"image/webp": true,
"image/heic": true,
"image/heif": true,
"text/plain": true,
"video/mov": true,
"video/mpeg": true,

View File

@ -104,6 +104,11 @@ func GetFileTypeFromUrl(c *gin.Context, url string, reason ...string) (string, e
return sniffed, nil
}
// Try HEIF/HEIC detection (Go standard library doesn't recognize it)
if heifMime := detectHEIF(readData); heifMime != "" {
return heifMime, nil
}
if _, format, err := image.DecodeConfig(bytes.NewReader(readData)); err == nil {
switch strings.ToLower(format) {
case "jpeg", "jpg":
@ -168,6 +173,10 @@ func GetMimeTypeByExtension(ext string) string {
return "image/gif"
case "jfif":
return "image/jpeg"
case "heic":
return "image/heic"
case "heif":
return "image/heif"
// Audio files
case "mp3":

View File

@ -3,6 +3,7 @@ package service
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"image"
_ "image/gif"
@ -275,6 +276,11 @@ func smartDetectMimeType(resp *http.Response, url string, fileBytes []byte) stri
}
return sniffed
}
// 4.5 尝试 HEIF/HEIC 检测Go 标准库不识别)
if heifMime := detectHEIF(fileBytes); heifMime != "" {
return heifMime
}
}
// 5. 尝试作为图片解码获取格式
@ -449,9 +455,118 @@ func decodeImageConfig(data []byte) (image.Config, string, error) {
return config, "webp", nil
}
// Try HEIF/HEIC: parse ISOBMFF ispe box for dimensions
if heifMime := detectHEIF(data); heifMime != "" {
formatName := "heif"
if heifMime == "image/heic" {
formatName = "heic"
}
if w, h, ok := parseHEIFDimensions(data); ok {
return image.Config{Width: w, Height: h}, formatName, nil
}
return image.Config{}, "", fmt.Errorf("failed to decode HEIF/HEIC image dimensions")
}
return image.Config{}, "", fmt.Errorf("failed to decode image config: unsupported format")
}
// detectHEIF checks ISOBMFF magic bytes to detect HEIC/HEIF files.
// Returns "image/heic", "image/heif", or "" if not recognized.
func detectHEIF(data []byte) string {
if len(data) < 12 {
return ""
}
// ISOBMFF: bytes[4:8] must be "ftyp"
if string(data[4:8]) != "ftyp" {
return ""
}
brand := string(data[8:12])
switch brand {
case "heic", "heix", "hevc", "hevx", "heim", "heis":
return "image/heic"
case "mif1", "msf1":
return "image/heif"
default:
return ""
}
}
// parseHEIFDimensions parses ISOBMFF box tree to find the ispe box
// and extract image width/height. Returns (width, height, ok).
func parseHEIFDimensions(data []byte) (int, int, bool) {
size := len(data)
if size < 12 {
return 0, 0, false
}
// Walk top-level boxes to find "meta"
offset := 0
for offset+8 <= size {
boxSize := int(binary.BigEndian.Uint32(data[offset : offset+4]))
boxType := string(data[offset+4 : offset+8])
headerLen := 8
if boxSize == 1 {
// 64-bit extended size
if offset+16 > size {
break
}
boxSize = int(binary.BigEndian.Uint64(data[offset+8 : offset+16]))
headerLen = 16
} else if boxSize == 0 {
// box extends to end of data
boxSize = size - offset
}
if boxSize < headerLen || offset+boxSize > size {
break
}
if boxType == "meta" {
// meta is a full box: 4 bytes version/flags after header
metaData := data[offset+headerLen : offset+boxSize]
if len(metaData) < 4 {
return 0, 0, false
}
return findISPE(metaData[4:])
}
offset += boxSize
}
return 0, 0, false
}
// findISPE recursively searches for the ispe box within container boxes.
// Path: meta -> iprp -> ipco -> ispe
func findISPE(data []byte) (int, int, bool) {
offset := 0
size := len(data)
for offset+8 <= size {
boxSize := int(binary.BigEndian.Uint32(data[offset : offset+4]))
boxType := string(data[offset+4 : offset+8])
if boxSize < 8 || offset+boxSize > size {
break
}
content := data[offset+8 : offset+boxSize]
switch boxType {
case "iprp", "ipco":
if w, h, ok := findISPE(content); ok {
return w, h, true
}
case "ispe":
// ispe is a full box: 4 bytes version/flags, then 4 bytes width, 4 bytes height
if len(content) >= 12 {
w := int(binary.BigEndian.Uint32(content[4:8]))
h := int(binary.BigEndian.Uint32(content[8:12]))
if w > 0 && h > 0 {
return w, h, true
}
}
}
offset += boxSize
}
return 0, 0, false
}
// guessMimeTypeFromURL 从 URL 猜测 MIME 类型
func guessMimeTypeFromURL(url string) string {
cleanedURL := url

View File

@ -159,20 +159,36 @@ func DecodeUrlImageData(imageUrl string) (image.Config, string, error) {
}
func getImageConfig(reader io.Reader) (image.Config, string, error) {
// Read all data so we can retry with different decoders
data, readErr := io.ReadAll(reader)
if readErr != nil {
return image.Config{}, "", fmt.Errorf("failed to read image data: %w", readErr)
}
// 读取图片的头部信息来获取图片尺寸
config, format, err := image.DecodeConfig(reader)
if err != nil {
err = errors.New(fmt.Sprintf("fail to decode image config(gif, jpg, png): %s", err.Error()))
common.SysLog(err.Error())
config, err = webp.DecodeConfig(reader)
if err != nil {
err = errors.New(fmt.Sprintf("fail to decode image config(webp): %s", err.Error()))
common.SysLog(err.Error())
config, format, err := image.DecodeConfig(bytes.NewReader(data))
if err == nil {
return config, format, nil
}
common.SysLog(fmt.Sprintf("fail to decode image config(gif, jpg, png): %s", err.Error()))
config, err = webp.DecodeConfig(bytes.NewReader(data))
if err == nil {
return config, "webp", nil
}
common.SysLog(fmt.Sprintf("fail to decode image config(webp): %s", err.Error()))
// Try HEIF/HEIC: parse ISOBMFF ispe box for dimensions
if heifMime := detectHEIF(data); heifMime != "" {
formatName := "heif"
if heifMime == "image/heic" {
formatName = "heic"
}
format = "webp"
if w, h, ok := parseHEIFDimensions(data); ok {
return image.Config{Width: w, Height: h}, formatName, nil
}
return image.Config{}, "", fmt.Errorf("failed to decode HEIF/HEIC image dimensions")
}
if err != nil {
return image.Config{}, "", err
}
return config, format, nil
return image.Config{}, "", err
}