first release

This commit is contained in:
tamakyi
2026-09-18 13:05:49 +08:00
commit b327bc1230
123 changed files with 30003 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
// 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
}