Files
2026-09-30 10:20:08 +08:00

361 lines
9.6 KiB
Go

package app
import (
"encoding/binary"
"errors"
)
const maxCoverBytes = 20 << 20
type heifBox struct {
kind string
data []byte
}
type heifReader struct {
data []byte
err error
}
func (reader *heifReader) take(size int) []byte {
if reader.err != nil || size < 0 || size > len(reader.data) {
reader.err = errors.New("HEIC 数据不完整")
return nil
}
value := reader.data[:size]
reader.data = reader.data[size:]
return value
}
func (reader *heifReader) uint(size int) uint64 {
if size < 0 || size > 8 {
reader.err = errors.New("HEIC 字段长度无效")
return 0
}
var value uint64
for _, part := range reader.take(size) {
value = value<<8 | uint64(part)
}
return value
}
func heifBoxes(data []byte) ([]heifBox, error) {
var boxes []heifBox
for len(data) > 0 {
if len(data) < 8 || len(boxes) >= 4096 {
return nil, errors.New("HEIC 容器结构无效")
}
size, header := uint64(binary.BigEndian.Uint32(data)), 8
if size == 1 {
if len(data) < 16 {
return nil, errors.New("HEIC 容器头不完整")
}
size, header = binary.BigEndian.Uint64(data[8:]), 16
} else if size == 0 {
size = uint64(len(data))
}
if size < uint64(header) || size > uint64(len(data)) {
return nil, errors.New("HEIC 容器长度无效")
}
boxes = append(boxes, heifBox{kind: string(data[4:8]), data: data[header:int(size)]})
data = data[int(size):]
}
return boxes, nil
}
func heifBoxData(boxes []heifBox, kind string) []byte {
for _, box := range boxes {
if box.kind == kind {
return box.data
}
}
return nil
}
func isHEICImage(data []byte) bool {
if len(data) < 16 || string(data[4:8]) != "ftyp" {
return false
}
size := uint64(binary.BigEndian.Uint32(data))
if size < 16 || size > uint64(len(data)) || size > 256 {
return false
}
for offset := 8; offset+4 <= int(size); offset += 4 {
if offset == 12 {
continue
}
switch string(data[offset : offset+4]) {
case "heic", "heix", "hevc", "hevx":
return true
}
}
return false
}
type heifImage struct {
data []byte
filters []string
}
func extractHEICImage(data []byte) (heifImage, error) {
var image heifImage
if len(data) > maxCoverBytes || !isHEICImage(data) {
return image, errors.New("不是支持的 HEIC 封面")
}
boxes, err := heifBoxes(data)
if err != nil {
return image, err
}
meta := heifBoxData(boxes, "meta")
if len(meta) < 4 || meta[0] != 0 {
return image, errors.New("HEIC 缺少图片元数据")
}
children, err := heifBoxes(meta[4:])
if err != nil {
return image, err
}
primary := &heifReader{data: heifBoxData(children, "pitm")}
version := primary.uint(1)
primary.take(3)
idSize := 2
if version == 1 {
idSize = 4
}
itemID := primary.uint(idSize)
if primary.err != nil || version > 1 || itemID == 0 {
return image, errors.New("HEIC 主图片标识无效")
}
if err := validateHEICItem(heifBoxData(children, "iinf"), itemID); err != nil {
return image, err
}
config, filters, err := heifItemProperties(heifBoxData(children, "iprp"), itemID)
if err != nil {
return image, err
}
item, err := heifItemData(heifBoxData(children, "iloc"), itemID, data, heifBoxData(children, "idat"))
if err != nil {
return image, err
}
image.data, err = heifAnnexB(config, item)
image.filters = filters
return image, err
}
func validateHEICItem(data []byte, itemID uint64) error {
reader := &heifReader{data: data}
version := reader.uint(1)
reader.take(3)
countSize := 2
if version == 1 {
countSize = 4
}
count := reader.uint(countSize)
if reader.err != nil || version > 1 {
return errors.New("HEIC 图片信息无效")
}
boxes, err := heifBoxes(reader.data)
if err != nil || count != uint64(len(boxes)) {
return errors.New("HEIC 图片信息不完整")
}
for _, box := range boxes {
if box.kind != "infe" {
continue
}
item := &heifReader{data: box.data}
version := item.uint(1)
item.take(3)
idSize := 2
if version == 3 {
idSize = 4
}
id := item.uint(idSize)
protected := item.uint(2)
kind := string(item.take(4))
if id == itemID {
if item.err != nil || (version != 2 && version != 3) || protected != 0 || kind != "hvc1" {
return errors.New("HEIC 封面不是独立的 HEVC 图片")
}
return nil
}
}
return errors.New("HEIC 主图片不存在")
}
func heifItemProperties(data []byte, itemID uint64) ([]byte, []string, error) {
boxes, err := heifBoxes(data)
if err != nil {
return nil, nil, err
}
properties, err := heifBoxes(heifBoxData(boxes, "ipco"))
if err != nil {
return nil, nil, err
}
reader := &heifReader{data: heifBoxData(boxes, "ipma")}
version, flags := reader.uint(1), reader.uint(3)
count := reader.uint(4)
if version > 1 || count > 4096 || reader.err != nil {
return nil, nil, errors.New("HEIC 图片属性无效")
}
var config []byte
var filters []string
var width, height uint64
for entry := uint64(0); entry < count && reader.err == nil; entry++ {
idSize := 2
if version == 1 {
idSize = 4
}
id := reader.uint(idSize)
associations := reader.uint(1)
for association := uint64(0); association < associations && reader.err == nil; association++ {
index := reader.uint(1) & 127
if flags&1 != 0 {
index = index<<8 | reader.uint(1)
}
if id != itemID || index == 0 {
continue
}
if index > uint64(len(properties)) {
return nil, nil, errors.New("HEIC 属性索引越界")
}
property := properties[index-1]
switch property.kind {
case "hvcC":
config = property.data
case "ispe":
if len(property.data) != 12 {
return nil, nil, errors.New("HEIC 图片尺寸无效")
}
width = uint64(binary.BigEndian.Uint32(property.data[4:]))
height = uint64(binary.BigEndian.Uint32(property.data[8:]))
case "irot", "imir":
if len(property.data) != 1 {
return nil, nil, errors.New("HEIC 图片方向无效")
}
if property.kind == "imir" {
if property.data[0]&1 == 0 {
filters = append(filters, "hflip")
} else {
filters = append(filters, "vflip")
}
} else {
switch property.data[0] & 3 {
case 1:
filters = append(filters, "transpose=cclock")
case 2:
filters = append(filters, "hflip", "vflip")
case 3:
filters = append(filters, "transpose=clock")
}
}
}
}
}
if reader.err != nil || len(config) < 23 || width == 0 || height == 0 || width > 4096 || height > 4096 {
return nil, nil, errors.New("HEIC 编码信息或图片尺寸无效")
}
return config, filters, nil
}
func heifItemData(data []byte, itemID uint64, file, inline []byte) ([]byte, error) {
reader := &heifReader{data: data}
version := reader.uint(1)
reader.take(3)
lengths, offsets := reader.uint(1), reader.uint(1)
offsetSize, lengthSize := int(lengths>>4), int(lengths&15)
baseSize, indexSize := int(offsets>>4), 0
if version > 0 {
indexSize = int(offsets & 15)
}
countSize := 2
if version == 2 {
countSize = 4
}
count := reader.uint(countSize)
if reader.err != nil || version > 2 || count > 4096 || offsetSize > 8 || lengthSize > 8 || baseSize > 8 || indexSize > 8 {
return nil, errors.New("HEIC 图片位置无效")
}
var result []byte
for entry := uint64(0); entry < count && reader.err == nil; entry++ {
id := reader.uint(countSize)
method := uint64(0)
if version > 0 {
method = reader.uint(2) & 15
}
reference, base := reader.uint(2), reader.uint(baseSize)
extents := reader.uint(2)
if extents > 4096 || (id == itemID && (reference != 0 || method > 1)) {
return nil, errors.New("HEIC 不支持外部图片引用")
}
for extent := uint64(0); extent < extents && reader.err == nil; extent++ {
reader.uint(indexSize)
offset, length := reader.uint(offsetSize), reader.uint(lengthSize)
if id != itemID || reader.err != nil {
continue
}
source := file
if method == 1 {
source = inline
}
if base > uint64(len(source)) || offset > uint64(len(source))-base {
return nil, errors.New("HEIC 图片偏移越界")
}
start := base + offset
if length == 0 {
length = uint64(len(source)) - start
}
if length > uint64(len(source))-start || length > uint64(maxCoverBytes-len(result)) {
return nil, errors.New("HEIC 图片长度越界")
}
result = append(result, source[start:start+length]...)
}
}
if reader.err != nil || len(result) == 0 {
return nil, errors.New("HEIC 图片数据缺失")
}
return result, nil
}
func heifAnnexB(config, item []byte) ([]byte, error) {
if len(config) < 23 || config[0] != 1 {
return nil, errors.New("HEIC 解码配置无效")
}
var result []byte
appendNAL := func(data []byte) error {
if len(data) < 2 || len(data) > maxCoverBytes-4-len(result) {
return errors.New("HEIC 编码数据长度无效")
}
result = append(result, 0, 0, 0, 1)
result = append(result, data...)
return nil
}
reader := &heifReader{data: config[23:]}
for array := 0; array < int(config[22]) && reader.err == nil; array++ {
reader.uint(1)
count := reader.uint(2)
if count > 4096 {
return nil, errors.New("HEIC 解码配置过大")
}
for index := uint64(0); index < count && reader.err == nil; index++ {
size := reader.uint(2)
if err := appendNAL(reader.take(int(size))); err != nil {
return nil, err
}
}
}
if reader.err != nil || len(reader.data) != 0 || len(result) == 0 {
return nil, errors.New("HEIC 解码配置不完整")
}
reader = &heifReader{data: item}
lengthSize := int(config[21]&3) + 1
for count := 0; len(reader.data) > 0; count++ {
size := reader.uint(lengthSize)
if reader.err != nil || size > uint64(len(reader.data)) || count >= 4096 {
return nil, errors.New("HEIC 图片编码不完整")
}
if err := appendNAL(reader.take(int(size))); err != nil {
return nil, err
}
}
return result, nil
}