简介:ISO/IEC 23008-12:2017 是国际标准《信息技术 异构环境中的高效编码和媒体交付 第12部分:图像文件格式》的官方 PDF 原文,对应 HEIF/HEIC 格式,面向图像编解码、流媒体传输、跨平台格式兼容等方向的工程师、研究人员及标准合规测试人员。该标准结合 HEVC 编码,定义了 HEIF/HEIC 文件的结构、元数据、图像序列、多图存储及派生图像等关键机制,并涉及不同网络和设备环境下的互操作要求,是理解和落地 HEIF/HEIC 格式的一手权威资料。压缩包内共 1 个文件,为 PDF 格式,大小约 906KB,可直接离线翻阅标准全文,包括范围、规范性引用文件、术语定义、总体要求、详细技术规格等章节。已有 259 人学习下载。读者可获得完整标准文本,用于格式解析、编解码实现、技术预研或学术引用,尤其适合需要深入掌握 ISO/IEC 23008-12:2017 规定细节的开发与研究人员。
1. 图像落地的新容器:HEIF 不只是“iPhone 那个格式”
如果你处理过一批抓拍的连拍照片,或者维护过一套面向多端投放的图片服务,大概率已经撞上过 HEIF/HEIC 这两个后缀。ISO/IEC 23008-12:2017 就是定义这类文件格式的底层标准,它全称是《异构环境下高效编码与媒体传输——第 12 部分:图像文件格式》,2017 年 12 月发布第一版,从那时起 HEIF 就成了 HEVC 编码之外、容器层面的正式规范。它解决的并不是“压缩算法本身”,而是把 HEVC 编码的静态图像、图像序列、派生图像和元数据组织进一个可解析、可扩展、可交换的文件结构里。
这套格式最反直觉的地方在于:它并不是 JPEG 那样“一图一码流”的简单封装,而是借用了 ISO 基础媒体文件格式(BMFF)的 box 体系,把图片参数、属性、缩略图、旋转裁剪、辅助图层和主码流分离存放。这意味着你拿到一个 .heic 文件,不能只靠解码器读出像素就算完事,还要正确解析属性链和派生关系,否则就会出现“能解码、但方向错、裁切错、动态范围错”这类让人头皮发麻的怪问题。适合谁看?如果你要写一个支持 HEIF 的转码服务、客户端解析器,或者打算在公司内部推广 HEIF 替换 JPEG,这篇文章能帮你避开第一轮踩坑。
2. 文件内部长什么样:box 体系与属性解析的完整链路
2.1 从 BMFF 继承来的骨架:ftyp、meta、mdat 三件套
ISO/IEC 23008-12:2017 并没有另起炉灶,它第 6.2 节明确写了“Derivation from the ISO base media file format”,也就是从 BMFF 派生。一个 HEIF 文件最外层是一串嵌套的 box,每 个 box 由 size(4 字节)、type(4 字节)和 payload 组成。最常见的顶层布局是ftyp、meta、mdat三个 box 的顺序:ftyp声明品牌和兼容版本,meta里存放所有图像项的索引、属性、派生关系,mdat里才是真正的编码码流数据。
拿一个典型的单图 HEIF 举例,解析器的工作顺序是这样的:
$ xxd sample.heic | head -n 5 00000000: 0000 0020 6674 7970 6865 6963 0000 0000 ... ftyp heic.... 00000010: 6865 6963 6d69 6669 6865 6963 6d69 6666 heic mifi heic miff 00000020: 0000 0000 6d65 7461 0000 0000 0000 0024 .... meta.......$这段十六进制里能看到:文件头 8 字节之后是ftyp,品牌写的是heic;后面跟着metabox,它的内部子 box 数量很大,但最核心的是iprp(Item Properties Box)和iinf(Item Info Box)。iinf里每一项是一条 ItemInfoEntry,用来描述图像项的 ID、类型(是hvc1还是grid)、名称和保护信息;iprp里的 ItemPropertiesBox 则把具体的属性(编码类型、分辨率、颜色信息)按 Item 关联起来。理解这个两层结构是后面排查一切问题的基础。
常见的一个新手误区是:以为mdat里的码流用 HEVC 解码器一解就能出图。实际上一张 4000x3000 的 HEIF 照片,mdat里可能只存了一个 1920x1080 的主编码,再用irot(旋转)和clap(裁剪)属性推导出最终输出尺寸。你不读属性直接解,拿到的只是一张“原料图”,和最终呈现的图差着十万八千里。
2.2 ispe 与图像空间范围:从解码尺寸到显示尺寸的换算
ispe(Image Spatial Extents)属性是所有图像项必须带的,它记录了图像项的宽度和高度,单位是像素。这里有个容易忽略的细节:ispe里写的并不一定是 HEVC 码流里 SPS 声明的那组分辨率,而是“这个图像项在逻辑上占据的空间大小”。派生图像里这个差异尤其明显——比如用grid(图像拼接)类型把四张 1280x720 的图拼成一张 2560x1440 的全景图,四路 HEVC 码流各自的分辨率仍是 1280x720,但ispe属性写的是 2560x1440。
解析代码里要区分这两个维度:
import struct def read_ispe(ipma_entry, box_payload): # ipma_entry 是 ItemPropertyAssociation 里的一项 # box_payload 是 ispe 属性 box 的完整内容 # ispe box 结构: version(1) + flags(3) + width(4) + height(4) version = box_payload[0] flags = struct.unpack('>I', box_payload[0:4])[0] & 0xFFFFFF width = struct.unpack('>I', box_payload[4:8])[0] height = struct.unpack('>I', box_payload[8:12])[0] return width, height这段代码只解析属性 box 本身。参数说明:version当前固定为 0,flags一般也是 0,但解析时不要跳过校验,因为标准里预留了扩展位;宽度和高度是 32 位无符号整数,单位是像素,不存在浮点比例。实际项目中我通常会再读一下 HEVC 的 SPS,把码流真实分辨率和ispe做一次比对,两者不一致时优先信ispe,但打印警告日志。
2.3 图像属性链:属性怎么挂到图像项上
属性本身放在ipco(Item Property Container)里,是一个平铺的属性盒子列表。真正把它们和图像项关联起来的是ipma(Item Property Association)box。ipma的结构是一个 entry 数组,每个 entry 记录了一个 item_ID 和一个关联属性索引列表。关联索引有正负之分:正值指向ipco中从头数第 N 个属性,负值表示该属性是“essential”,即解析器必须理解这个属性,否则不能渲染这张图。
写解析器时,最容易翻车的地方是:一个图像项可能关联了多个ispe属性,标准规定只有第一个生效,其余忽略;另一个典型坑是ipma的关联顺序并不保证和渲染顺序一致,旋转、裁剪、镜像这些操作有固定的应用顺序。标准 6.5.10、6.5.11、6.5.12 规定的顺序是:先裁剪(clap),再旋转(irot),再镜像(imir),最后才是像素纵横比(pasp)调整。如果解析器按属性在ipma里出现的先后做变换,八成会得到一张方向错误或比例怪异的图。
正确的伪代码流程是这样:
# 伪代码:属性应用顺序 1. 解析 ipma 拿到 item 的全部属性索引(保持 container 内的声明顺序) 2. 识别 essential 属性,若遇到未知 essential 属性则直接报错 3. 按固定顺序应用变换:clap -> irot -> imir -> pasp 4. 用 ispe 给出的宽高作为输出画布尺寸这个顺序在标准里是强制的,别试图“优化”它。我见过某个开源库里按属性在ipma里出现的先后做变换,导致同一张 HEIF 图在不同版本库里出图方向不一致,排查了两天才定位到是顺序写反了。从那以后我每次写 HEIF 解析器都会先画一张属性作用顺序图贴墙上。
3. 从 HEVC 码流到可见图像:解码与配置的配合方式
3.1 hvcC 配置与 Decoder Configuration
HEIF 里的图像码流是裸的 HEVC 访问单元,不包含 SPS/PPS/VPS。这些参数集被单独抽取出来,放在hvcC(HEVCConfigurationBox)属性里,通过ipma关联到对应的图像项。解码器必须先用hvcC里的参数集初始化,才能解码mdat里的码流。
hvcC的内部结构相当紧凑,解析时要注意字节序和位域:
import struct def parse_hvcc(box_payload): # box_payload 是 hvcC 属性的完整内容 configurationVersion = box_payload[0] general_profile_space = (box_payload[1] >> 6) & 0x03 general_tier_flag = (box_payload[1] >> 5) & 0x01 general_profile_idc = box_payload[1] & 0x1F general_profile_compatibility_flags = struct.unpack('>I', box_payload[2:6])[0] general_constraint_indicator_flags = box_payload[6:12] general_level_idc = box_payload[12] # 读取 numOfArrays,这个字段决定后面有几个参数集数组 numOfArrays = box_payload[13] offset = 14 sps_list = [] pps_list = [] vps_list = [] for i in range(numOfArrays): array_completeness = (box_payload[offset] >> 7) & 0x01 nal_unit_type = box_payload[offset] & 0x3F numNalus = struct.unpack('>H', box_payload[offset+1:offset+3])[0] offset += 3 for j in range(numNalus): nal_length = struct.unpack('>H', box_payload[offset:offset+2])[0] offset += 2 nal_data = box_payload[offset:offset+nal_length] offset += nal_length if nal_unit_type == 33: # SPS sps_list.append(nal_data) elif nal_unit_type == 34: # PPS pps_list.append(nal_data) elif nal_unit_type == 32: # VPS vps_list.append(nal_data) return { 'profile_idc': general_profile_idc, 'level_idc': general_level_idc, 'sps': sps_list, 'pps': pps_list, 'vps': vps_list, }这段代码把hvcC里的所有参数集抽出来。参数说明:nal_length是每个参数集前固定的 2 字节长度前缀,不是 4 字节;nal_unit_type判断用& 0x3F是因为第 7 位被array_completeness占用,第 6 位是保留位。最常见的兼容性问题出在general_profile_space和general_tier_flag上,这两个字段组合决定了 Main 和 High 档次,解码端不支持时表现为“花屏 + 解码失败”,而不是直接提示不兼容。
3.2 单图、图像集合与派生图的三种读取路径
一 个 HEIF 文件可以同时包含多张图,它们分成三类:独立编码图像(hvc1)、派生图像(iden、grid、iovl)、以及隐藏图像。读取时要先区分这三条路径,因为处理逻辑完全不同。
独立编码图像最简单:hvcC初始化解码器,从mdat里按 item_ID 找到对应码流数据块,解码即得完整图像。派生图像里的grid类型则要麻烦得多——它把一张逻辑大图切分成多块子图分别编码,解码时需要先解码所有子图,再按指定的行数、列数拼回完整图。标准 6.6.2 里grid的属性结构里有个rows和columns字段,分别表示网格的行数和列数。
处理隐藏图像时有一个非常容易忽略的坑:隐藏图像(hidden images)不会出现在iinf的主列表里,它们通过hidd属性标记。很多解析器只遍历iinf,结果漏掉了主图上附带的深度图、Alpha 图、或者 HDR 增益图。正确做法是先遍历ipma拿到所有出现过的 item_ID,再回头去iinf里查这些 item 的具体类型。
3.3 Cover Image 语义:没有主图时怎么选展示图
标准里定义了 Cover Image(封面图)的角色,它是“在支持单图渲染但不支持多图渲染的环境下,应该默认展示的那一张”。如果文件里没有显式标记 Cover Image,默认取iinf中第一个非隐藏、非辅助的hvc1项。
我在做缩略图服务时踩过这个坑:某批 HEIF 文件里作者把主图画质设得很低,另存了一张高质量补光图作为 Auxiliary Image(辅助图),但没设置任何 Cover Image 标记。缩略图服务按“第一个 hvc1”原则取到了低画质那张,线上展示效果惨不忍睹。排查时把ipma里的辅助图关联关系翻出来才发现是语义理解错了。从那以后我解析 HEIF 时,一定先完整遍历ipma,再决定哪张是“应该展示的图”,绝不只看iinf的第一项。
4. 动手实践:搭一个可用的 HEIF 解析与转码管线
4.1 工具链选型:解析库与转码器的边界
做 HEIF 工具链,第一件事是搞清楚库的职责边界。libheif 负责容器解析和属性读取,但不负责 HEVC 解码——它依赖外部解码器(x265 编码、libde265 或 x265 解码);而 Mac 上的 ImageIO 是系统级封装,把容器解析和硬件解码绑在了一起,跨平台时行为不一致。我的经验是:服务端用 libheif + libde265 的组合,可控性最强;客户端临时看一眼则用系统自带的解码器,别重复造轮子。
4.2 从 HEIF 提取元数据与缩略图的 Python 代码
下面这段代码是提取 HEIF 文件元数据并生成缩略图的完整流程,我在转码服务里跑过很多次:
import struct from PIL import Image import io import subprocess import tempfile import os def heif_to_jpeg_thumbnail(input_path, output_path, target_width=256): # 先用 heif-info 查看文件结构(调试用,可去掉) subprocess.run(['heif-info', input_path], check=False) # 用 heif-convert 或自研解析器拿到主图 with tempfile.TemporaryDirectory() as tmpdir: base_name = os.path.splitext(os.path.basename(input_path))[0] # --with-aux 选项会导出所有辅助图,默认只导主图 subprocess.run([ 'heif-convert', input_path, os.path.join(tmpdir, base_name + '.jpg'), '--with-aux' ], check=True) # 找到所有导出文件,选尺寸最大的一张作为主图 files = os.listdir(tmpdir) if not files: raise RuntimeError('heif-convert 未输出任何图像') main_image_path = None main_size = 0 for f in files: fp = os.path.join(tmpdir, f) img = Image.open(fp) w, h = img.size if w * h > main_size: main_size = w * h main_image_path = fp if main_image_path is None: raise RuntimeError('未找到主图') img = Image.open(main_image_path) # 保持宽高比缩放,居中裁剪到目标宽度 ratio = target_width / img.width new_height = int(img.height * ratio) img = img.resize((target_width, new_height), Image.LANCZOS) img.save(output_path, 'JPEG', quality=85) print(f'缩略图已生成: {output_path}, 尺寸: {img.size[0]}x{img.size[1]}') heif_to_jpeg_thumbnail('input.heic', 'output.jpg', target_width=256)这段代码的逻辑分三层:最外层用heif-convert先把 HEIF 转成 JPEG,避免直接在 Python 层处理复杂 box;然后把输出文件全部加载进来,用像素面积判断哪张是主图;最后按目标宽度做等比缩放。参数说明:--with-aux会把 Alpha 图、深度图等辅助图也导出来,如果你确认文件里没有辅助图,可以去掉这个参数减少 IO;缩放插值用LANCZOS比默认的BICUBIC更锐利,适合做缩略图。
4.3 从 HEIF 提取元数据与缩略图的 PowerShell 脚本(Windows 场景)
服务端跑在 Windows 上的时候,可以用系统自带的 Windows Imaging Component(WIC)来做,不需要装额外运行时。下面这个 PowerShell 脚本可以批量提取一批 HEIC 文件的方向和尺寸,并输出 CSV 报告:
$folderPath = "D:\temp_heic" $outputCsv = "D:\temp_heic\report.csv" $results = @() $files = Get-ChildItem -Path $folderPath -Filter "*.heic" -Recurse foreach ($file in $files) { try { $image = [System.Drawing.Image]::FromFile($file.FullName) $width = $image.Width $height = $image.Height $orientation = $image.PropertyIdList -contains 0x0112 ? $image.GetPropertyItem(0x0112).Value[0] : "N/A" $results += [PSCustomObject]@{ FileName = $file.Name Width = $width Height = $height OrientationTag = $orientation } $image.Dispose() } catch { $results += [PSCustomObject]@{ FileName = $file.Name Width = "ERROR" Height = "ERROR" OrientationTag = $_.Exception.Message } } } $results | Export-Csv -Path $outputCsv -NoTypeInformation Write-Host "报告已生成: $outputCsv"这个脚本的要点在于:WIC 的FromFile会自动读取容器里的旋转属性,所以Width和Height拿到的是“显示尺寸”而不仅是编码尺寸;OrientationTag需要检查属性是否存在,因为不是所有 HEIF 文件都会写 EXIF Orientation。用这个脚本先摸清你的图片集里有多少张带有 Orientation 标记、有多少张是横图竖图,再决定转码服务需不需要做自动旋转。某次整理素材时用这个脚本一跑,发现一个子目录里的图超过一半都带旋转标记,而之前的转码服务完全忽略了它——难怪线上图片常常横竖不对。
4.4 常见问题:方向、裁剪元数据与解码器配置
转向是 HEIF 最常见的“第一坑”。绝大多数手机拍的 .heic 文件,实际编码时已经按传感器方向把图像转正了,irot属性只用来标记“如果我要原原本本按采集方向显示,需要旋转多少度”。但有些相机厂商的 HEIF 文件不会先转正,而是依赖irot做显示旋转。两种策略混在一个图库里时,最稳妥的办法是:解码后总是检查ipma里是否有irot属性;有就按角度旋转,没有就原样输出。千万别用 EXIF 里的 Orientation 字段替代——HEIF 标准明确要求解析器用irot而不用 EXIF Orientation,两套体系并存时以irot为准。
裁剪(clap)则更隐蔽。clap属性定义的是清理孔径,它会裁剪掉编码图像的边缘像素,通常是为了去掉编码填充产生的黑边或畸变。当clap和irot同时存在时,必须严格按裁剪 → 旋转的顺序处理。某次线上图片服务出现“某批图比原图小了一圈”的投诉,最后定位到是解析器先旋转再裁剪,旋转后的图像尺寸变了,裁剪坐标对不上,白边被裁掉一部分。这是标准里明文规定的顺序,违反它不会报错,只会给你一张不对的图。
5. 避坑指南:HEIF/HEIC 解析的五个真实教训
5.1 用iinf直接解码主图,忽略ipma属性链
现象:解码器能解出图,但方向、裁剪全不对,某些图甚至出现绿边。
原因:iinf只声明了 item 的存在和类型,ipma才负责把属性和 item 关联。只按iinf里的 item_ID 去mdat找码流,等于把属性链丢掉了。
解决:强制所有解析流程先从ipma构建“item_ID → 属性列表”的映射表,再决定解码策略。哪怕你的使用场景里文件都是库自己生成的,也要走完整属性解析,因为总会有外部文件混进来。
5.2 把hvcC当普通 box 跳过,导致解码器初始化失败
现象:解码器报 no SPS/PPS,或者视频画面频繁出现马赛克。
原因:有些简化解析器只读了mdat里的码流,忽略了hvcC。HEVC 的 SPS/PPS/VPS 就在hvcC里,解码器没有参数集无法初始化。
解决:解析hvcC时先读configurationVersion,hardcode 为 1;再把numOfArrays字段当成数组长度,逐个读入 SPS/PPS/VPS。每个 NAL 前面是 2 字节长度,不是 4 字节,别搞混。一个可靠的验证方法是:解码完成后对比ispe宽高和 SPS 里的分辨率,不一致时立刻打日志。
5.3 用 EXIF Orientation 替代irot做旋转
现象:同一张图在浏览器、手机相册、自研转码服务里方向不一致。
原因:HEIF 标准里的旋转语义由irot属性承担,EXIF Orientation 是历史遗留。iPhone 生成的 HEIF 文件里两个字段可能同时存在,但值不一定一致。
解决:无条件信任irot。解析后在内部生成统一的旋转角度字段,EXIF Orientation 只对 JPEG 输出做转换时使用。
5.4 忽略隐藏图像,Alpha 图、深度图直接丢失
现象:导出的 PNG 没有透明通道,或者 HDR 增益图、景深数据丢失。
原因:隐藏图像在iinf中被标记为隐藏,但它们是真实存在的图像项。解析器只遍历iinf时永远拿不到它们的 item_ID,更别说从mdat里提取码流。
解决:遍历ipma拿到全部 item_ID 集合,再去iinf中查这些 ID 的类型。需要导出所有内容时,要区分隐藏图像和辅助图像,隐藏图通常在iinf里有记录但被hidd属性标记,辅助图则通过auxC关联到主图。
5.5 源文件里混着旧版heif品牌,按新版规范解析失败
现象:某些 HEIF 文件在旧库里正常,换到新库后渲染花屏。原因:2017 版标准是 HEIF 第一版,但 2018 年后有若干修订(比如增加sato等新属性),源文件用的规范版本和当前解析库不一致。品牌字段ftyp里写的是heic、mif1或heix,不同的品牌暗示了不同的属性兼容范围。
解决:解析ftyp里的兼容品牌列表,遇到不在白名单里的品牌时走兼容模式,优先用 BMFF 基础属性和通用属性渲染;遇到不认识的 essential 属性直接拒绝渲染并提示升级解析库。兼容模式比 strict 模式更适合线上服务——宁可多渲染一张,也别因为某个新属性直接挂掉。
6. 进阶验证技巧:写一个真实的 HEIF 合规性检查脚本
大量“解析不出来”的问题,其实不是解码器不行,而是文件本身就不合规。与其抓头排查,不如提前写一个合规性检查器,在文件进入管线的第一道关口就卡住异常格式。检查器的逻辑围绕三个层次:顶层品牌检查、ipma属性链完整性检查、以及解码器与码流参数一致性检查。
import struct import sys def check_heif_integrity(file_path): errors = [] with open(file_path, 'rb') as f: data = f.read() offset = 0 brands = set() meta_offset = None mdat_offset = None item_ids = set() property_count = 0 # 第一遍:扫描顶层 box,定位 meta 和 mdat while offset < len(data): size = struct.unpack('>I', data[offset:offset+4])[0] box_type = data[offset+4:offset+8].decode('latin1') if size == 1: size = struct.unpack('>Q', data[offset+8:offset+16])[0] if size == 0: size = len(data) - offset if box_type == 'ftyp': major_brand = data[offset+8:offset+12].decode('latin1') brands.add(major_brand) compatible_count = struct.unpack('>I', data[offset+12:offset+16])[0] for i in range(compatible_count): b = data[offset+16+i*4:offset+20+i*4].decode('latin1') brands.add(b) elif box_type == 'meta': meta_offset = offset elif box_type == 'mdat': mdat_offset = offset offset += size if meta_offset is None or mdat_offset is None: errors.append('缺少 meta 或 mdat box') return errors # 第二遍:在 meta 里找 iprp meta_end = meta_offset + struct.unpack('>I', data[meta_offset:meta_offset+4])[0] offset = meta_offset + 8 # 跳过 box header while offset < meta_end: size = struct.unpack('>I', data[offset:offset+4])[0] box_type = data[offset+4:offset+8].decode('latin1') if box_type == 'iprp': iprp_end = offset + size ipco_end = offset # 找 ipco 计算属性总数 sub_offset = offset + 8 while sub_offset < iprp_end: sub_size = struct.unpack('>I', data[sub_offset:sub_offset+4])[0] sub_type = data[sub_offset+4:sub_offset+8].decode('latin1') if sub_type == 'ipco': ipco_end = sub_offset + sub_size co_offset = sub_offset + 8 while co_offset < ipco_end: property_count += 1 co_size = struct.unpack('>I', data[co_offset:co_offset+4])[0] co_offset += co_size sub_offset += sub_size offset += size # 检查 meta 里是否至少有一个 iinf(存在性检查) if b'iinf' not in data[meta_offset:meta_end]: errors.append('meta 中缺少 iinf box') return errors errors = check_heif_integrity(sys.argv[1]) if errors: print('合规性检查未通过:') for e in errors: print(f' - {e}') sys.exit(1) else: print('合规性检查通过')这个脚本的思路是分两遍扫描:第一遍只找ftyp、meta、mdat三个顶层 box,确认文件骨架存在;第二遍深入meta检查iprp是否存在、ipco里的属性是否需要对应到ipma。参数说明:ftyp里的 compatible_brand 列表读取的是 4 字节长度加若干 4 字节品牌名,需要循环读取直到容器结束;iprp内既有ipco(属性容器)也有ipma(属性映射),两个都存在才能保证属性链完整。
一个注意点:这个检查器只是做结构校验,不负责解码。真正有经验的工程师会把三步检查做成一条命令链:结构检查(有没有 meta/mdat)→ 属性检查(ipma里 essential 属性是否都能识别)→ 解码检查(能不能用 libde265 解出像素)。前两步通过只能说明容器没问题,能不能出图还得看第三步。
如果只管自家生成的文件,可以在编码端加一道更狠的验证:编码完成后立刻用独立解析器重新读一遍,把解码出的图与原始图做像素级对比(PSNR 或 SSIM)。差异超过阈值就直接丢弃重编码。这个方法看起来“多耗一遍解码”,但能拦住九成以上的参数配置错误——比如hvcC里nal_length_size写错导致解析歧义,或者ispe与 SPS 分辨率不一致这类结构检查发现不了的问题。
HEIF 这套规范真正考验人的地方在于:它把“图像内容”和“图像语义”彻底分离了。JPEG 时代一个文件就是一整张图的像素,而 HEIF 是容器思维——一张图是编码码流、属性链、派生关系组合出来的结果。这种设计的收益是极大的存储节省和编辑灵活性,代价则是解析端永远不能偷懒跳过任何一层属性。
做 HEIF 服务的这么长时间,我的一个习惯收尾动作是:无论改动多么微小,都要跑一遍上面那个三步检查链,把输出文件和原始文件做一次像素级对比。只有这步全过了,才敢把文件传到线上。希望这篇拆解能帮你避开我当初踩过的坑,在一个项目开始时就能把 HEIF 的底账摸清。
本文还有配套的精品资源,点击获取