361 lines
9.6 KiB
Go
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
|
|
}
|