Files
rond-with-you/internal/exif/heif.go
T
2026-09-18 13:05:49 +08:00

223 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// HEIF/HEIC 容器里的 EXIF 提取。
//
// HEIC 是 ISO BMFF(和 MP4 同一套盒子结构):文件头是 ftyp,后面跟一个 meta box,
// 里面 iinf 描述每个 item(其中有一个 item_type == "Exif"),iloc 给出它的位置。
// 取出来的 Exif item 是「4 字节 TIFF 偏移 + 标准 TIFF」,后半段与 JPEG 的 APP1 完全一样,
// 所以直接交给 parseTIFF 复用。
//
// 纯 Go 实现,不引 libheif:内网/离线构建环境下 CGO 依赖太麻烦,而我们只要 EXIF 这一小块。
package exif
import (
"encoding/binary"
"errors"
)
var errNotHEIF = errors.New("不是 HEIF/HEIC 容器")
// boxHeader 读一个 box 的头,返回类型、负载起点与下一个 box 的起点。
func boxHeader(b []byte, off int) (typ string, payload, next int, ok bool) {
if off < 0 || off+8 > len(b) {
return "", 0, 0, false
}
size := int(binary.BigEndian.Uint32(b[off : off+4]))
hdr := 8
switch size {
case 1: // largesize
if off+16 > len(b) {
return "", 0, 0, false
}
size = int(binary.BigEndian.Uint64(b[off+8 : off+16]))
hdr = 16
case 0: // 一直到文件末尾
size = len(b) - off
}
if size < hdr || off+size > len(b) {
return "", 0, 0, false
}
return string(b[off+4 : off+8]), off + hdr, off + size, true
}
// parseHEIF 从 HEIC/HEIF 里取 EXIF。
func parseHEIF(b []byte) (Info, error) {
for off := 0; off+8 <= len(b); {
typ, payload, next, ok := boxHeader(b, off)
if !ok {
break
}
if typ == "meta" {
// meta 是 FullBox:负载前面还有 4 字节 version+flags
if payload+4 > next {
return Info{}, errNotHEIF
}
return parseMeta(b, payload+4, next)
}
if next <= off {
break
}
off = next
}
return Info{}, errNoEXIF
}
func parseMeta(b []byte, start, end int) (Info, error) {
iinfStart, iinfEnd := -1, -1
ilocStart, ilocEnd := -1, -1
for off := start; off+8 <= end; {
typ, payload, next, ok := boxHeader(b, off)
if !ok || next > end {
break
}
switch typ {
case "iinf":
iinfStart, iinfEnd = payload, next
case "iloc":
ilocStart, ilocEnd = payload, next
}
off = next
}
if iinfStart < 0 || ilocStart < 0 {
return Info{}, errNoEXIF
}
itemID, ok := findExifItem(b, iinfStart, iinfEnd)
if !ok {
return Info{}, errNoEXIF
}
off, length, ok := findItemExtent(b, ilocStart, ilocEnd, itemID)
if !ok || off < 0 || length <= 0 || off+length > len(b) {
return Info{}, errNoEXIF
}
item := b[off : off+length]
if len(item) < 4 {
return Info{}, errNoEXIF
}
// Exif item 开头 4 字节是 TIFF 头相对本 item 起点的偏移
tiff := int(binary.BigEndian.Uint32(item[:4]))
if tiff < 0 || tiff+8 > len(item) {
tiff = 4 // 少数写入方这里填得不准,退化成「紧跟这 4 字节」
}
return parseTIFF(item[tiff:])
}
// findExifItem 在 iinf 里找 item_type 为 "Exif" 的 item。
func findExifItem(b []byte, start, end int) (uint32, bool) {
if start+4 > end {
return 0, false
}
ver := b[start]
p := start + 4
var count int
if ver == 0 {
if p+2 > end {
return 0, false
}
count = int(binary.BigEndian.Uint16(b[p : p+2]))
p += 2
} else {
if p+4 > end {
return 0, false
}
count = int(binary.BigEndian.Uint32(b[p : p+4]))
p += 4
}
for i := 0; i < count; i++ {
typ, payload, next, ok := boxHeader(b, p)
if !ok || next > end {
break
}
// infe v2 起:version+flags(4) + item_ID(2) + protection(2) + item_type(4)
if typ == "infe" && payload < next && b[payload] >= 2 && payload+12 <= next {
if string(b[payload+8:payload+12]) == "Exif" {
return uint32(binary.BigEndian.Uint16(b[payload+4 : payload+6])), true
}
}
p = next
}
return 0, false
}
// findItemExtent 在 iloc 里找指定 item 的第一段数据(偏移 + 长度)。
func findItemExtent(b []byte, start, end int, wantID uint32) (int, int, bool) {
if start+8 > end {
return 0, 0, false
}
ver := b[start]
p := start + 4
offsetSize := int(b[p] >> 4)
lengthSize := int(b[p] & 0x0F)
p++
baseSize := int(b[p] >> 4)
indexSize := 0
if ver >= 1 {
indexSize = int(b[p] & 0x0F)
}
p++
if offsetSize == 0 || lengthSize == 0 || p+2 > end {
return 0, 0, false
}
count := int(binary.BigEndian.Uint16(b[p : p+2]))
p += 2
// 越过越界就读不动了,返回 false 让上层当作没找到
readN := func(n int) (uint64, bool) {
if n < 0 || p+n > end {
return 0, false
}
var v uint64
for i := 0; i < n; i++ {
v = v<<8 | uint64(b[p+i])
}
p += n
return v, true
}
for i := 0; i < count; i++ {
var id uint32
if ver == 0 {
v, ok := readN(2)
if !ok {
return 0, 0, false
}
id = uint32(v)
} else {
v, ok := readN(4)
if !ok {
return 0, 0, false
}
id = uint32(v)
}
if ver >= 1 {
if _, ok := readN(2); !ok { // construction_method + reserved
return 0, 0, false
}
}
if _, ok := readN(2); !ok { // data_reference_index
return 0, 0, false
}
base, ok := readN(baseSize)
if !ok {
return 0, 0, false
}
extentCountV, ok := readN(2)
if !ok {
return 0, 0, false
}
for e := 0; e < int(extentCountV); e++ {
if ver >= 1 && indexSize > 0 {
if _, ok := readN(indexSize); !ok {
return 0, 0, false
}
}
off, ok1 := readN(offsetSize)
length, ok2 := readN(lengthSize)
if !ok1 || !ok2 {
return 0, 0, false
}
if id == wantID {
return int(base + off), int(length), true
}
}
}
return 0, 0, false
}