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:
commit
9816ad87e3
@ -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,
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user