资讯动态

HEIF/HEIC深度解析:从Box容器到派生图像,一次读懂图片压缩新标准

发布时间:2026/10/16 14:43:31 来源:尧图企业网站定制
简介ISO/IEC 23008-12:2017是图像文件格式领域的核心国际标准面向多媒体开发者、编解码器实现者及移动互联网从业者用于解决HEIF/HEIC高效图像存储与异构环境下的媒体交付问题。这份PDF为第一版原版标准包内仅1个文件、整体大小906KB内容涵盖HEVC编码背景、HEIF/HEIC文件结构、图像序列与元数据规则、兼容与互操作要求并涉及加密与数字版权管理等安全机制适合作为权威依据直接查阅规则原文。已有259人学习下载可用于协议解析、编解码器实现、格式转换工具开发或测试用例设计。阅读后可快速定位Scope、Terms、Overview等基础章节并对照正文理解HEIF如何组织单图、序列及元数据避免因二手资料造成的理解偏差为后续工程落地或合规评估提供清晰的原始文本支撑。1. HEIF 与 HEIC为什么一张照片能省下一半体积同样一颗 1200 万像素的传感器输出 JPEG 可能要 5~6MB转存为 HEIC 后只剩 2~3MB放大到屏幕上观感几乎看不出差别。很多人把这个账全记在 HEVC 编码头上但 HEIC 真正的秘密不止是压缩算法更是它背后的容器设计——ISO/IEC 23008-12 定义的 HEIF 图像文件格式。HEIF 把一张图片从「一串像素字节流」改写成「一组对象、操作符与属性的结构化数据库」旋转图片不用重编码裁剪图片不损失画质连拍照片、深度图、HDR 图层可以共存在同一个文件里。搞懂这份标准才算真正理解 HEIC 为什么能 work——同时也是后续所有兼容性「血泪经验」的地基。2. 与 MP4 同源的 box 容器HEIF 文件究竟是怎么组织的2.1 从 JPEG 的扫描流到 HEIF 的 box 树JPEG 的底层形态是「段 扫描数据」的字节流水线文件以 0xFFD8 开头、0xFFD9 结尾EXIF、缩略图、量化表全部内嵌在这条线性流中。解码器想知道一张图的宽高必须先从头扫到 SOF 段想渲染就必须按顺序吞完一整条数据流。这是为「一次性播放」设计的格式所有信息都摊在同一条时间轴上。HEIF 走的是另一条路。它脱胎于 ISO 基础媒体文件格式ISO BMFF——也就是 MP4 所依赖的容器语法。文件不再是线性扫描流而是一棵由 box 组成的树每个 box 用「4 字节长度 4 字节类型」作为头部内部可以继续嵌套子 box。读取文件不再依赖顺序而是按类型做随机访问。23008-12 的第 6.2 节明确写着HEIF 是对 ISO BMFF 的「派生」而非另起炉灶这也是为什么一个 .heic 后缀的文件既是一张静态图又能被当作迷你视频容器来存放图像序列。2.2 最小 HEIF 骨架ftyp、meta、mdat 三个 box 拆解一个最简的单张 HEIF 文件顶层通常只有三个平级 boxftyp、meta、mdat。ftyp 声明文件品牌brand与兼容品牌meta 是「索引区」用 item 描述文件里有哪些图像、属性、派生关系mdat 是「数据区」真正存放 HEVC 编码后的码流。JPEG 里「数据与描述搅在一起」HEIF 里「索引与数据彻底分离」——这就是为什么 HEIF 允许只改 meta 不动 mdat就能实现旋转、裁剪、镜像这类以往必须重新解码的操作。写一个最朴素的顶层 box 遍历能直观看到这个骨架import struct def parse_top_boxes(path): boxes [] with open(path, rb) as f: while True: header f.read(8) if len(header) 8: break size, box_type struct.unpack(I4s, header) if size 1: # 64-bit extended size (size,) struct.unpack(Q, f.read(8)) boxes.append((box_type.decode(latin1), size)) f.seek(size - 8, 1) # 跳过整个 box 内容 return boxes for box_type, size in parse_top_boxes(demo.heic): print(f{box_type}: {size} bytes)这段逻辑的核心在于 box header 的 size 字段普通 box 的 size 是 32 位无符号整数当 size 等于 1 时真正的长度延伸到紧随其后的 8 字节 64 位字段——这是 ISO BMFF 为超大文件预留的扩展位。type 是 4 字符码HEIF 里最常见的 brand 落在 heic、heix、mif1 这几个值上分别对应「HEVC 主配置静态图」「HEVC 主静态配置」「通用图像文件」。这里有一个易错点mdat 里的实际负载并不一定是 HEVC 码流。标准第 5.3 节的「多用途文件」允许同一个文件里存在多个数据区间甚至把缩略图、音频轨和图像项都塞进同一条 mdat。解析器如果假设「见 mdat 就当图像数据读」碰到复合文件就会直接翻车。提示某些 box 的开头还有 version/flags 字节。例如 meta box 的前 4 字节是「1 字节版本号 3 字节标志位」解析嵌套结构前必须先跳过这 4 字节否则所有子 box 的偏移都会错位。理解 box 树之后才能进入 HEIF 真正的核心创新item 与派生。3. 图像项与派生图像无重编码就能裁剪旋转的底层机制3.1 图像项是「文件里的另一个文件」infe、iloc、iinf 三者关系HEIF 对 JPEG 最关键的本体论颠覆在于一张图不是「文件的全部」而是「索引区里登记的一个项item」。标准第 6.1 节把文件分成单图像与图像集合集合里可以包含多个 item每个 item 有自己的 ID、类型、名称和数据位置。索引工作靠 meta box 里的 iinf、infe、iloc 三个子 box 协同完成。infe 登记「是什么」item_ID、item_type、item_name、content_type。iloc 登记「在哪」用 extent_offset 与 extent_length 描述数据块在文件中的字节偏移与长度。iinf 是 item 条目的汇总表。举例来说一个 HEIC 文件里有主图、缩略图、深度图三个 item它们在 mdat 中各自占据不连续的区域iloc 能精确指向每一段解析器无需按顺序读文件直接 seek 就能取回任意 item 的码流。用代码解析 iloc 的核心逻辑长这样def parse_iloc(payload): version payload[0] # 第4字节高4位offset_size低4位length_size第5字节高4位base_offset_size offset_size (payload[4] 4) 0xF length_size payload[4] 0xF base_offset_size (payload[5] 4) 0xF item_count int.from_bytes(payload[6:8], big) p 8 entries [] for _ in range(item_count): if version 2: item_id int.from_bytes(payload[p:p2], big) p 2 else: item_id int.from_bytes(payload[p:p4], big) p 4 p 2 # data_reference_index通常为0 base_offset int.from_bytes(payload[p:pbase_offset_size], big) p base_offset_size extent_count int.from_bytes(payload[p:p2], big) p 2 extents [] for __ in range(extent_count): off int.from_bytes(payload[p:poffset_size], big) p offset_size ln int.from_bytes(payload[p:plength_size], big) p length_size extents.append((base_offset off, ln)) entries.append((item_id, extents)) return entries这段代码把「item ID」与「文件偏移区间」的映射还原了出来。注意 extent 的 base_offset 与 extent_offset 是相对文件起点的绝对偏移length 才是真正的保险丝——解析器必须用 length 约束读取范围绝不能用相邻 extent 的间距去反推数据长度因为 mdat 里可能塞着 padding 或其他轨道数据。很多自研解析器第一次崩溃都发生在没读 iloc 就去 mdat 里「猜」边界。3.2 派生操作符grid、iovl、iden 怎么做到无损编辑HEIF 在 6.6 节定义了一组特殊的 item 类型叫派生图像项derived image item。它不直接持有编码码流而是持有「输入 item ID 操作符 参数」解码器按需从输入项中「算出」输出图。三种核心操作符item_type操作符含义主要参数典型场景iden恒等复制把输入项直接作为输出无文件内部重引用避免重复存储grid把输入项按二维格子拼接行数、列数、输出宽高全景图、分块解码、超大分辨率iovl把输入项按坐标叠加画布宽高、各输入项的偏移坐标贴图合成、带透明通道的 UI 素材grid 的语义值得细拆。解码器拿到一个 grid 项时先读它的尺寸配置——行数、列数、输出宽高然后按序把每个输入 item 解码成子图再根据「第 i 行第 j 列」的数学规则摆到对应位置。整个过程不产生新的编码数据输出像素全部来自既有码流的解码结果。这意味着把一张全景图切成的四块重拼起来或者把九宫格截图合并都可以只新建一个 grid 派生项彻底绕开「重新编码一整套 JPEG」的老流程。标准 6.6 节对 grid 的边界一致性有严格约束输出图像的宽高由 grid 配置决定输入子图按位置排布后必须恰好铺满整个输出画布允许重叠或间隙都被视为非法文件。这个设计让解码器不需要「猜」某个区域是否透明、是否空着派生逻辑变成一个确定性的数学映射。3.3 预派生编码图像与多层图像在编码层已经为容器铺路除了容器层的派生操作符标准 6.4.7 节还定义了预派生编码图像pre-derived coded image。这类 item 的码流本身就不是「一张完整图」而是 HEVC 里具备可拼接属性的编码单元典型例子是 tile。HEVC 的 tile 边界天然支持独立解码HEIF 便允许把这些 tile 单独登记为 item或让一个 item 引用另一 item 内的某个 tile 区域。多层图像multi-layer image也是同一思路HEVC 的可扩展编码允许把基础层与增强层放进同一个文件的不同 item播放时按需选择层的组合。这个机制面向 HDR 和 360° 内容——基础层兼容普通屏幕增强层叠加后输出高动态范围画面。标准没有为「多层」发明复杂语法而是复用 6.5.11 节的 layer selection 属性在容器层描述「选第几层参与渲染」。读到这里应该能体会HEIF 的想象力是把「图像的语义」从编码器里剥离出来交给容器层表达。旋转、裁剪、拼接、选层全部变成「对项做操作」视频与图像的边界在这里开始模糊。4. 属性系统色彩、旋转、裁切为何不写在像素里4.1 属性与项目分开存储ipco ipma 的映射机制如果说 item 定义了「文件里有什么」属性property则定义了「这些内容该怎么被理解和显示」。HEIF 的第 9.3 节定义了两个配套 boxipcoItem Property Container存放属性定义ipmaItem Property Association建立 item 与属性之间的映射。属性可以被多个 item 复用——多个 item 共享同一份旋转属性逻辑上像「对象与类的关联」而不是 JPEG 里每张图各自内嵌一份 EXIF。ipma 的 entry 由 item_ID 和 property index 数组组成property index 指向 ipco 里的第几个属性。这里有个几乎必踩的坑property index 是 1-based0 表示「无属性」。解析器如果直接用数组下标做 0-based 访问会在第一个带属性的 item 上就发生错位。标准使用这种编号设计本质上是为「无属性」保留一个显式的空值锚点。4.2 解码配置与像素级细节从 HEVC 参数集到 clap、paspHEIF 的每个编码图像 item 都必须关联一个解码器配置属性decoder configuration其中封装 HEVC 的 VPS、SPS、PPS 参数集。这个设计解决了一个被 JPEG 时代忽略的痛点JPEG 的量化表和解码参数深埋在扫描流里想「只取一张图的某一块」也得先解析完整头部HEIF 把配置项抽到属性层解析器可以先读配置再按需跳读 mdat 里的目标样本。标准 6.5.2 节把它命名为 decoder configuration and initialization并强调「初始化必须在解码任何样本之前完成」。同样活跃在属性层的还有一组显示相关属性clapclean aperture干净孔径即无损裁剪、pasp像素宽高比、rloc相对位置、irot旋转 90/180/270、imir镜像、colr颜色信息。这些属性的共同特征是「不动 mdat 的一个字节」。修改 irot 只是改 1 字节图像显示方向立刻变化背后的 HEVC 码流完全不变。这就是为什么 HEIF 的旋转、翻转操作能在毫秒级完成也是 JPEG 无法企及的。解析旋转与裁剪属性逻辑非常轻def parse_irot(payload): # 只有1字节低2位表示顺时针旋转90°的次数 value payload[0] 0x3 return {0: 0°, 1: 90°, 2: 180°, 3: 270°}[value] def parse_clap(payload): # 4组「分子分母」32位整数宽度、高度、水平偏移、垂直偏移 nums [] for i in range(0, 16, 4): n int.from_bytes(payload[i:i4], big) d int.from_bytes(payload[i4:i8], big) nums.append(n / d if d else 0) return {width: nums[0], height: nums[1], h_offset: nums[2], v_offset: nums[3]}注意 irot 的语义是「顺时针旋转 N 次」。多个属性同时存在时标准对应用顺序有隐性约束先应用 clap再应用 irot/imir否则旋转之后裁剪框的位置会与原意不符。这个顺序规范在 6.5.9 与 6.5.10 之间形成了绑定关系——不遵守它渲染出来的画面区域会差出几个像素到整个视野。4.3 属性的 essential 与 non-essential容错设计的智慧属性还有一个容易被忽略的分级essential关键与 non-essential非关键。关键属性在 ipma 里带 essential 标志解码器不支持就必须拒绝渲染防止产出错误图像非关键属性则允许跳过。这个分级是媒体容器设计里少见的「优雅降级」——比如一个「深度图」属性看不懂的旧版解码器可以忽略它依然正常显示主图而颜色信息这种关键属性看不懂就必须报错不能乱渲。这个设计的底层动机是生态兼容。文件格式的演进不可能一夜之间让所有终端跟上标准委员会选择用「明确声明关键性」来代替「隐式要求所有实现支持全部特性」。实现者面对一个不认识的 non-essential 属性最安全的操作是「假装没看见」面对 essential 属性则必须给出明确失败而不是自作主张地猜一个默认值。5. 常见问题排查解析 HEIF 最容易翻车的五个细节5.1 品牌与扩展名不一致heic、heix、mif1 到底谁说了算现象一个后缀为 .heic 的文件自研解析器读出来 major_brand 却是 mif1按 HEVC 配置去初始化解码器直接失败。原因扩展名是给操作系统和用户看的brand 才是给解析器看的。mif1 只承诺「符合通用 HEIF 规范」并不承诺「里面一定用 HEVC」。同一容器里可能装着采用 JPEG 编码的 item也可能装着其他编码的轨道。仅凭 major_brand 一个值就断定编码格式等于把定位逻辑建立在文件名上。解决解析器必须先读 ftyp 的 compatible_brands 列表再逐 item 查看它的 decoder configuration确认到底是哪种编码格式。正确流程是读 ftyp → 读 meta/iinf → 对每个 item 查属性 → 拿到解码器配置 → 再决定调用哪个解码库。顺序错了后面全是黑匣子式报错。5.2 派生项依赖缺失引用链断裂时的幽灵报错现象打开一个 grid 派生项解码器提示「输入项不存在」或直接崩溃单独解码每个子图却都正常。原因HEIF 的派生项允许引用任意 item包括另一个派生项。文件里可能存在「派生项 → 派生项 → 底层编码项」的多层引用链。部分实现只做了单层递归遇到深层引用时找不到叶子节点的数据抛出的错误信息却指向顶层的 mdat 偏移让人误以为是文件损坏。解决递归解析 item 引用关系先构建一张 item_ID → item 类型的依赖图再检查每个派生项的输入是否都能回溯到真实的编码 item。建议对引用深度设上限比如 8 层超限直接判定文件非法防止恶意构造的无限递归耗尽内存。这种绕圈引用在规范里被禁止但实际文件中偶尔能碰到防御性检查不能省。5.3 属性顺序问题irot 与 clap 先后不同结果完全不同现象同一张带旋转和裁剪属性的 HEIF 图在两个不同渲染库里显示出的画面区域不一致一个显示的是旋转前的左上角另一个显示的是旋转后的左上角。原因标准对显示属性的应用顺序有明确约定先 clap后 irot/imir。某个库实现时把顺序写反裁剪逻辑发生在旋转之后裁出来的内容自然对不上。这个顺序并不显眼写在两个相邻条款里容易被当成「建议」而非「必须」。解决所有客户端解析器统一按「clap → irot → imir」的顺序做合成投影。更稳妥的做法是把属性顺序写成独立的内部表示层让上层渲染逻辑永远面对统一坐标而不是各自维护一套属性应用流水线。协作开发时这条顺序应该写进接口注释和回归测试用例用测试锁住行为。5.4 图像序列轨的样本依赖随机访问不是真的随机现象从 HEIF 的图像序列轨里随机抽一个中间的样本单独解码出来是花屏但按顺序从头解码就正常。原因7.4.1 节的 direct reference samples list 允许样本直接引用相邻样本当前帧可以引用前一帧做预测但容器并未保证「第 N 帧的数据能独立解码」。图像序列轨本质上是视频轨道HEVC 的帧间预测让孤立解码成为奢望。解析器没有先构建参考依赖链就撞上了花屏。解决随机访问前先解析 sample group找到最近的关键帧从那里开始解码再丢弃不需要的帧。不要试图「精确解出第 N 帧」要接受「从上一个关键帧一路解过来」的代价。这个约束是帧间编码的物理事实不是容器的缺陷。5.5 完整性校验与篡改检测哈希校验的是结构而不是语义现象一个 HEIF 文件被第三方工具编辑过拍摄时间元数据变了标准自带的完整性校验却依然通过。原因标准 8.4 节的完整性校验integrity checks对「文件结构」做哈希保障的是「文件未被结构性篡改」管不到 EXIF 等语义字段。把「完整性校验通过」理解成「元数据未被改动」会在取证或版权场景里得出错误结论。解决需要防篡改时要额外对关键 item 的负载数据做哈希并把哈希值放到带数字签名的元数据项中而不是依赖标准自带的 integrity check。理解 8.4 的边界才能知道它在安全方案里该站哪班岗。6. 从 HEIF 看容器设计当「图像文件」变成「媒体数据库」6.1 容器优先标准把可扩展性设计成了一等公民HEIF 的底层哲学可以浓缩成一句话把图像文件从「一次性编码产物」改写成「可索引、可组合、可派生的对象系统」。JPEG 的每个像素都是为「最终显示」服务的HEIF 的每个 item 和属性都是在为「将来的用途」预留空间。一个全景图JPEG 存成一张大图解码时整幅进内存HEIF 存成多个 item 加一个 grid想看局部就只解码对应 item内存占用可以差一个数量级。这个取舍的代价是格式复杂度。JPEG 解码器几百行 C 代码就能写完HEIF 光解析 meta 就需要处理十余种 box 的版本分支。标准为此保留了不少在一个简单解码器看来「冗余」的设计比如 64 位扩展 size、多版本 iloc、essential 标志。以后续实现者的视角回看这种代码量换来的灵活度是否划算取决于你的产品是否需要连拍序列、深度图、屏幕内容分割、HDR 多层的组合——而这些恰恰是现代手机相册每天都在产生的内容。6.2 从 HEVC 到 AVIF容器的生命力比编码更长久对 HEIF 最常见的批评是「生态支持慢、授权风险高」。HEVC 本身的专利池让一部分平台长期不愿意接 HEIC导致 HEIF 一度被误解为某个特定编码的私有外壳。后来 AVIF使用 AV1 编码以几乎同样的 HEIF 容器语法进入市场大家才意识到平台拒绝的是 HEVC 的授权成本而不是 HEIF 的容器思想。AVIF 与 HEIF 同根同源等于对容器层设计做了一次独立验证——同样的 box 结构、同样的 item 派生机制、同样的属性系统换上更开放的编码就重新活了一遍。我在这类跨格式兼容性问题上吃过的教训是评估一个新图像格式不要把「编码算法」和「容器结构」绑死在一起投票。同一份 23008-12 的容器语法今天可以承载 HEVC明天可以承载 AV1、JPEG它是「图片版 MP4」。把视角切换到容器层之后所有看似反直觉的行为——旋转不重编码、能塞视频轨、改元数据不改像素——就都顺理成章了。从那以后我每次拿到陌生格式的样本文件都会强制走一遍「先看 ftyp 品牌再画 item 依赖图最后确认属性顺序」的流程再没被黑匣子式报错坑过。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑