news 2026/10/5 18:09:23

MuPDF 数据结构详解:Fitz 哈希表与自平衡二叉树的实现与应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MuPDF 数据结构详解:Fitz 哈希表与自平衡二叉树的实现与应用
  • 图形学
  • 图像处理

【免费下载链接】mupdf

mupdf mirror

项目地址:https://gitcode.com/gh_mirrors/mu/mupdf
点击查看免费下载

导读

本文聚焦 MuPDF 核心图形库 Fitz(即fz前缀来源)内置的两套通用数据结构:固定长度键哈希表(fz_hash_table)与字符串到值映射的自平衡二叉树(fz_tree,AA-tree)。这两套结构是 MuPDF 内部文档缓存(store)、PDF 资源索引、归档文件(archive)与 HTML 图片去重等高频场景的底层支撑。读完本文,你将掌握两套结构的全部公开 API、引用计数语义、扩容/删除等底层行为,并能直接在基于 MuPDF 的 C 程序中正确使用它们。

概述:MuPDF 中的通用数据结构

MuPDF 的文档渲染管线中充斥着大量"键值查找"需求:按对象句柄查找缓存条目、按资源编号查找字体/颜色空间/图像、按文件名查找归档内条目、按图片 id 去重等。与其为每种场景各写一套,Fitz 内核提供了两个通用容器:

  • fz_hash_table:固定长度键(fixed-length keys)的通用哈希表,见头文件 include/mupdf/fitz/hash.h;
  • fz_tree:将文本字符串映射到任意值的自平衡二叉树(AA-tree 实现),见头文件 include/mupdf/fitz/tree.h。

两者均定义于 Fitz 核心层,通过 include/mupdf/fitz.h 随核心库一起暴露给上层模块使用。

哈希表fz_hash_table

设计要点

fz_hash_table是一个固定长度键的哈希表,即所有键必须具有相同的字节长度(创建时通过keylen参数指定)。键与值均不参与引用计数(not reference counted),由调用方负责在插入/移除时手动维护引用计数。

底层实现为开放寻址 + 线性探测(open addressing with linear probing)。与教科书实现不同,该实现支持正确删除条目而不会引发后续查找行为退化——源码注释明确强调这一点(见 source/fitz/hash.c)。删除时通过do_removal将后续冲突条目向前回填(backward shift),从而保持探测链连续。

哈希函数使用 CRC32C 校验和:hash()内部直接调用fz_crc32c(0, s, len)(见 source/fitz/hash.c),CRC32C 的具体查表实现位于 source/fitz/crc32.c。

创建与销毁

fz_hash_table *fz_new_hash_table(fz_context *ctx, int initial_size, int key_length, int lock, void (*drop_value)(fz_context *ctx, void *value)); void fz_drop_hash_table(fz_context *ctx, fz_hash_table *table);

参数说明:

  • initial_size:初始桶数量。表在"约 80% 满"时会自动扩容为原来的两倍(load > size * 8 / 10触发fz_resize_hash,见 source/fitz/hash.c)。因此初始值不需要精确预估,只需合理即可。
  • key_length:每个键的字节长度。键过长时构造函数会抛出FZ_ERROR_ARGUMENT("hash table key length too large");单键上限为宏FZ_HASH_TABLE_KEY_LENGTH,值为 48(见 include/mupdf/fitz/hash.h)。
  • lock:当前应传0(-1 亦可,语义见下文)。文档明确警告:传其他任何值都会导致不可预测的行为。从源码看(见 source/fitz/hash.c),该字段为-1时表示不加锁,非负时表示持有某个FZ_LOCK,且各操作会调用fz_assert_lock_held校验锁状态——普通用户直接传-1(无锁)即可。注意:fz_new_hash_table的lock参数是锁的索引值,0与-1的语义取决于 MuPDF 版本中的FZ_LOCK枚举,文档以"传零"为安全约定。
  • drop_value:仅用于销毁整张表时释放每个值;插入/移除单个条目时不会调用它。可为NULL。

fz_drop_hash_table释放表空间,并对表中每个值调用drop_value(若非 NULL)后再释放条目数组与表结构本身(见 source/fitz/hash.c)。

查找与插入

void *fz_hash_find(fz_context *ctx, fz_hash_table *table, const void *key); void *fz_hash_insert(fz_context *ctx, fz_hash_table *table, const void *key, void *value);
  • fz_hash_find:返回键关联的值;未找到返回NULL。查找同样使用 CRC32C 定位并线性探测(见 source/fitz/hash.c)。
  • fz_hash_insert:插入键值对。重复键不覆盖旧值——若键已存在,表保持不变并返回旧值指针;若首次插入,键被复制进表内、值所有权移交,返回NULL。不进行引用计数,调用方需自行fz_keep_*值。扩容检查(80% 负载阈值)也在此函数入口执行。

删除与遍历

void fz_hash_remove(fz_context *ctx, fz_hash_table *table, const void *key); void fz_hash_for_each(fz_context *ctx, fz_hash_table *table, void *state, void (*callback)(fz_context *ctx, void *state, void *key, int key_length, void *value));
  • fz_hash_remove:按键移除条目。不释放值(不引用计数),调用方需自行释放。若键不存在,会打印一条警告"assert: remove non-existent hash entry"(见 source/fitz/hash.c)。
  • fz_hash_for_each:对表中每个键值对调用回调。回调签名含key_length参数,便于在固定长度键下安全读取键内容;state为透传的任意上下文指针。遍历顺序即桶数组顺序,与插入顺序无关。

头文件中还提供了头文件中未在文档中单独列出的fz_hash_filter:遍历并移除所有回调返回真的条目(同样不释放值),见 include/mupdf/fitz/hash.h 与 source/fitz/hash.c。

典型用例:MuPDF 内部缓存与资源索引

从源码调用点可以看出这套哈希表承担的核心职责:

  • 文档存储缓存:fz_store用fz_new_hash_table(ctx, 4096, sizeof(fz_store_hash), FZ_LOCK_ALLOC, NULL)建立缓存索引(source/fitz/store.c),并按需fz_hash_insert/fz_hash_remove维护条目(source/fitz/store.c、source/fitz/store.c)。
  • PDF 资源表:PDF 文档的字体、颜色空间、图像资源分别建表,键为sizeof(*key)的结构体,值为 PDF 对象,并使用pdf_drop_obj_as_void作为销毁回调(source/pdf/pdf-resources.c)。
  • 颜色空间换算缓存:colorspace.c中以n * sizeof(float)的浮点数组为键缓存 ICC 换算结果,并使用fz_free释放值(source/fitz/colorspace.c);颜色转换的 lookup 表也以509为初始桶数建表(source/fitz/colorspace.c)。

自平衡二叉树fz_tree

设计要点

fz_tree是将文本字符串映射到值的自平衡二叉查找树,实现为AA-tree(Arne Andersson 树):每个节点带level字段,通过skew(左倾修正)与split(层级提升)两种旋转操作维持平衡(见 source/fitz/tree.c)。头文件注释将其概括为 "AA-tree to look up things by strings"(见 include/mupdf/fitz/tree.h)。

关键语义:

  • 查找使用strcmp做简单指针等价比较(source/fitz/tree.c);
  • 插入时不复制键内容,也不复制值——节点仅保存key与value的指针(源码中key实际通过fz_strdup复制,见 source/fitz/tree.c,因此调用方无需为键的生命周期负责;值则仅存指针,由调用方保证存活)。文档层面以"键和值仅作为指针被保存"表述。

无构造函数:根节点即树

fz_tree没有构造函数——不存在"包含根"的容器结构。树的根节点就是fz_tree*本身,初始为空树时直接使用NULL,插入函数返回新的根节点,因此调用方必须用返回值不断更新根指针。

fz_tree *tree = NULL; tree = fz_tree_insert(ctx, tree, "A", my_a_obj); tree = fz_tree_insert(ctx, tree, "B", my_b_obj); tree = fz_tree_insert(ctx, tree, "C", my_c_obj); assert(fz_tree_lookup(ctx, tree, "B") == my_b_obj);

注意:插入后必须将返回值赋回根变量,否则后续操作可能基于过期的根节点。同时不要插入重复键(重复插入同一键时strcmp == 0的节点会走右分支继续插入,可能产生语义不明的重复节点,文档明确要求避免)。

查找与销毁

void *fz_tree_lookup(fz_context *ctx, fz_tree *node, const char *key); void fz_drop_tree(fz_context *ctx, fz_tree *node, void (*dropfunc)(fz_context *ctx, void *value));
  • fz_tree_lookup:按字符串查找,命中返回对应值,否则返回NULL。查找过程沿左右子树下降(见 source/fitz/tree.c)。
  • fz_drop_tree:递归释放整棵树。先释放左右子树,再释放节点键副本,并对每个值调用dropfunc(可传NULL跳过值的释放),最后释放节点本身(见 source/fitz/tree.c)。

典型用例:内存归档与 HTML 资源去重

  • 内存归档(tree archive):fz_new_tree_archive直接以fz_tree为底层存储,将文件名映射到fz_buffer;has_entry/read_entry/open_entry均通过fz_tree_lookup实现,添加条目时用fz_tree_insert更新根节点,销毁时用fz_drop_tree(ctx, tree, drop_tree_archive_entry)释放每个 buffer(source/fitz/archive.c)。
  • EPUB 元数据查重:epub 文档用fz_tree_lookup判断条目是否已存在、以fz_tree_insert累积信息,并以NULL作为 dropfunc 销毁(source/html/epub-doc.c)。
  • HTML 图片去重:HTML 解析器以图片 id 为键、fz_image*为值建树,销毁时以fz_drop_image作为 dropfunc 统一释放(source/html/html-parse.c、source/html/html-parse.c)。

哈希表 vs 二叉树:如何选择

维度fz_hash_tablefz_tree
键类型任意固定长度字节(keylen指定)C 字符串(strcmp比较)
结构开放寻址线性探测哈希表自平衡 AA-tree
查找复杂度平均 O(1),冲突时线性探测O(log n)
重复键插入返回旧值、不覆盖禁止插入重复键
引用计数均不计数,调用方自理均不计数,调用方自理
销毁回调构造函数传入drop_value销毁时传入dropfunc
扩容80% 负载自动翻倍无容量概念,动态插入
典型用途文档缓存 store、PDF 资源表、颜色换算缓存内存归档、HTML/EPUB 资源去重

两条选择建议:

  1. 键是定长二进制结构(如对象句柄、struct或固定长度数组)时优先fz_hash_table;
  2. 键是文本名称/路径(如文件名、图片 id、URI)且需要按字典序遍历能力时优先fz_tree。

两者均要求调用方管理值的引用计数:插入前fz_keep_*、移除或销毁时fz_drop_*(或通过 drop 回调),这是使用 MuPDF 容器最需要注意的约定。

总结

fz_hash_table与fz_tree是 MuPDF Fitz 内核提供的两个轻量通用容器:前者以固定长度键 + 线性探测提供平均 O(1) 查找,并支持安全的删除与 80% 负载自动扩容;后者以 AA-tree 提供字符串键的 O(log n) 查找,且无需独立根结构。二者的头文件声明见 include/mupdf/fitz/hash.h 与 include/mupdf/fitz/tree.h,实现分别位于 source/fitz/hash.c 与 source/fitz/tree.c。在 MuPDF 的文档缓存、PDF 资源索引、内存归档与 HTML/EPUB 处理中,它们承担着高频键值查找的职责,理解其语义(尤其是不引用计数与重复键行为)是编写正确 MuPDF 插件代码的前提。

  • 图形学
  • 图像处理

【免费下载链接】mupdf

mupdf mirror

项目地址:https://gitcode.com/gh_mirrors/mu/mupdf
点击查看免费下载

相关推荐

上一篇:Effect HttpApi 类型化响应头实战指南:WithHeaders、encodeToWithHeaders 与响应头覆盖语义
下一篇:Open-Meteo 免费天气 API 实战:Docker 一行命令跑通气象数据服务

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 18:01:25

PX4开发环境搭建:Ubuntu 18.04下QGC与Qt Creator完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 17:41:55

Go电商系统实战:Gin+MongoDB+Redis高并发架构解析

简介:本资源是一套基于Go语言的B2C电商系统实战源码,面向具备Go基础的中高级开发者,聚焦Web后端开发、高并发架构与微服务实践,助力快速掌握电商核心模块(如用户中心、商品管理、订单服务)的工程化落地。压…

作者头像 李华
网站建设 2026/10/5 17:38:12

X3850 X6 6241 IMM2固件升级实战指南

1. X3850x6服务器固件升级的底层逻辑:为什么6241型号必须用IMM2专用Fireware?IBM System x3850 X6是2013年前后发布的高端四路Xeon E7服务器平台,其核心价值在于支持高达4TB内存、16路PCIe扩展和双IMM(Integrated Management Modu…

作者头像 李华
网站建设 2026/10/5 17:35:02

链路状态路由算法C++实现:邻接矩阵与Dijkstra最短路径详解

简介:这份文档面向计算机网络课程学习者与路由算法入门者,系统讲解链路状态路由算法的原理与实现,帮助读者理解自治系统内部路由选择的核心机制。内容围绕发现邻接点、测量链路开销、构造并传播链路状态分组、更新拓扑视图、计算最短路径五个…

作者头像 李华
网站建设 2026/10/5 17:31:04

DeepSeek Harness桌面端深度解析:从API Key配置到内网离线部署

1. 桌面端来了,为什么这件事比想象中重要DeepSeek Harness 出官方桌面端这件事,我第一反应不是"终于等到了",而是"早该如此"。过去大半年,身边用 DSH 的人基本分成两派:一派死磕命令行&#xff0c…

作者头像 李华
网站建设 2026/10/5 17:25:48

ANSYS Workbench谐响应分析全流程详解与常见坑位排查

搜“workbench”,跳出来的工具五花八门:MySQL Workbench、Motor Control Workbench……但如果你是在结构仿真这个圈子里混的,说一句“workbench谐响应”,大家心知肚明,说的是ANSYS Workbench里的Harmonic Response分析…

作者头像 李华