news 2026/9/21 2:39:48

深入解析 MuPDF C API 指南:从核心模块到 PDF 对象层的架构与实践(SumatraPDF 内嵌版)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析 MuPDF C API 指南:从核心模块到 PDF 对象层的架构与实践(SumatraPDF 内嵌版)
  • 桌面应用
  • 文档

【免费下载链接】sumatrapdf

SumatraPDF reader

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

本指南基于 MuPDF 官方 C API 文档(ext/mupdf/docs/reference/c/introduction.md),系统梳理 MuPDF 对外提供的稳定公共接口。读者将掌握:MuPDF 六大模块(Core、I/O、Graphics、Device、Document、PDF)的职责划分与协作关系、fz_context上下文与异常处理机制、多线程使用规则,以及如何基于这些 API 写出可编译、可运行的渲染与文档处理代码。全文以本仓库内嵌的 MuPDF 源码与 SumatraPDF 集成代码(src/EngineMupdf.cpp)为佐证,确保每个结论都有据可查。

文档定位:面向开发者的稳定 API 指南

MuPDF 的 C API 指南(introduction.md)开宗明义:这是一份面向开发者的库使用指南,覆盖 MuPDF 库公开且稳定的部分。它有两个明确的受众目标:

  • 新开发者:想要使用 MuPDF 底层特性的入门向导;
  • 有经验的开发者:需要查阅公共接口的参考手册。

文档范围特意限定在"单线程应用"的起步场景。MuPDF 内部还有更多函数与数据结构,也支持多线程使用,但这些超出本文档范围。文档强调:其中记录的函数与结构是稳定 API,很少变更;若发生变更,会在 "api-changes" 文档中记录迁移说明。

注意:本仓库(SumatraPDF)将 MuPDF 以子模块方式内嵌在 ext/mupdf 目录下,完整的 C API 头文件位于 ext/mupdf/include/mupdf,其中fitz子目录对应文档中的核心图形库。SumatraPDF 自身的 PDF 引擎(src/EngineMupdf.cpp)正是这套 API 的典型真实消费者。

MuPDF 六大核心模块总览

文档将公共 API 划分为六个功能模块,从底层到顶层依次为:

模块职责对应头文件目录(从源码结构看)
Core运行时上下文、异常处理、字符串操作、数学、哈希表、二叉树等基础工具ext/mupdf/include/mupdf/fitz 下context.hgeometry.hhash.htree.h
I/O数据缓冲区、流读写、压缩、加密buffer.hstream.hcompress.hcrypt.h
Graphics颜色、字体、渐变着色(shading)、图像等图形资源对象color.hfont.hshade.himage.hpixmap.hpath.h
Device文档内容的访问接口:回调结构,页面上的每段文本、线条、图像都会回调到设备device.hdisplay-list.hstructured-text.h
Document多格式文档读写,串联上述模块提供渲染、格式转换、搜索document.hlink.houtline.h
PDFPDF 底层结构访问:查询、修改、创建 PDF 对象与流,可新建/修改文档、提取数据ext/mupdf/include/mupdf/pdf 下object.hdocument.hxref.h

Device:理解 MuPDF 渲染与文本提取的关键抽象

Device 接口是理解 MuPDF 架构的钥匙。文档明确指出:

"A device is a callback structure, that gets called for each piece of text, line art, and image on a page."

即设备是一个回调结构,页面上的每个文本片段、每条矢量线条、每幅图像都会触发设备回调。MuPDF 有多个设备实现:

  • 渲染设备:把页面内容绘制到光栅图像(pixmap)上——这是最常见的使用方式;
  • 文本收集设备:把所有文本汇集为结构化结构,供选择、复制、搜索页面文本使用。

这种"解释器 + 设备"的设计让同一份页面内容可以"绘制"到不同目标上:渲染成位图、提取文本、生成 SVG、写入显示列表等,而不需要为每种输出重写文档解析逻辑。

核心:fz_context 上下文与资源管理

文档指出 Core 模块包含"运行时上下文(runtime context)、异常处理以及各类实用函数"。要理解 MuPDF,首先要理解贯穿几乎所有函数的第一个参数——fz_context

fz_context 是什么

overview.md 详细说明了上下文的内容:绝大多数 MuPDF 接口函数都接收一个 context 参数,它保存了 MuPDF 在解析和渲染页面时使用的全局状态,例如:

  • 异常栈(exception stack):配合下文fz_try/fz_catch异常机制使用;
  • 内存分配器(memory allocator):允许自定义分配器;
  • 资源存储(resource store):缓存图像、字体等;
  • 一组锁及加锁/解锁函数:用于多线程场景。

文档特别强调:若未提供锁与配套函数,context 及其代理只能用于单线程应用——这一限制是多线程部分的核心前提。

创建与销毁 context

context.md 给出了最小可运行的创建示例:

#include <mupdf/fitz.h> #include <stdio.h> #include <stdlib.h> main() { fz_context *ctx = fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED); if (!ctx) { fprintf(stderr, "Failed to create a new Fitz context!\n"); return EXIT_FAILURE; } ... do stuff ... fz_drop_context(ctx); return EXIT_SUCCESS; }

创建函数原型为:

fz_context *fz_new_context(const fz_alloc_context *alloc, const fz_locks_context *locks, size_t max_store);

三个参数的含义:

参数说明取值建议
alloc自定义内存分配器NULL使用系统默认分配器
locks线程安全所需的锁回调单线程传NULL;多线程必须提供(详见下文)
max_store资源存储(缓存)允许增长的最大字节数,软限制FZ_STORE_DEFAULTFZ_STORE_UNLIMITED

文档特别说明第三个参数是软限制:缓存可能临时超过它,但 MuPDF 会开始清理陈旧数据尽量压回限制以下;内存紧张时设置较小值可防止缓存失控增长。

销毁使用fz_drop_context(ctx),对应的引用计数递增函数是fz_clone_context(ctx)(用于多线程克隆)。

异常处理:fz_try / fz_always / fz_catch 体系

MuPDF 采用基于setjmp/longjmp的异常处理体系,由三个宏封装:fz_tryfz_alwaysfz_catch。概念上类似 C++ 的 try/catch,但不需要任何特殊编译器支持(overview.md、error.md)。

基本结构

fz_try(ctx) { // 尝试执行任务。绝不能从这里 'return'、'goto' 或 'longjmp' 出去。 // 'break' 可用于安全退出(仅)try 块作用域。 } fz_always(ctx) { // 无论 try 块内是否抛出异常,这里的代码都会执行。 // 同样绝不能 'return'、'goto' 或 longjmp 出去。 } fz_catch(ctx) { // 仅当 try 块(包括其调用的任何函数)抛出异常时, // 在 always 块之后执行。这里应处理异常(记录/报告错误、 // 清理遗留状态等),随后可退出该块, // 或用 fz_throw / fz_rethrow 将异常传给外层 fz_try 块。 }
  • fz_always可选,可安全省略;
  • 所有可能出错的函数调用都应放在fz_try块内,否则出错时程序会直接调用exit()(error.md 明确警告 "You don't want that");
  • fz_always的典型用途是无条件释放资源——无论 try 块成功还是出错。

三条必须牢记的限制

基于宏与setjmp的实现带来三个主要限制:

  1. 禁止从 try 块内 return/goto/longjmp:这会破坏宏的内部簿记(housekeeping),后续会引发问题;代码虽能检测到这类违规,但为时已晚,无法给出有用的错误定位。
  2. try/always/catch 不是一个原子的 C 语句。下面的写法是错的:
if (condition) fz_try(ctx) { ... } fz_catch(ctx) { ... } // 错误!

必须写成:

if (condition) { fz_try(ctx) { ... } fz_catch(ctx) { ... } }
  1. 宏基于 setjmp/longjmp 实现,因此 C 标准对这两个函数的所有限制都适用于fz_try/fz_catch。特别是:在 fz_try 开始之后、到抛出异常之间被赋值的"真正局部"变量,在异常抛出过程中可能变成未定义值

fz_var:对抗变量丢失

为缓解第三点,MuPDF 提供fz_var()宏,它告诉编译器确保该变量不会因抛出异常而被撤销。典型示例(来自 error.md):

char *buf = NULL; fz_var(buf); fz_try(ctx) { buf = fz_malloc(ctx, 100); // Do stuff with buf that may throw an exception. } fz_always(ctx) { fz_free(ctx, buf); } fz_catch(ctx) { fz_rethrow(ctx); }

若不用fz_var(buf)保护,出错时局部变量buf可能被重置为进入 try 前的值(NULL),导致内存泄漏。

抛出与重抛异常

void fz_throw(fz_context *ctx, int error_code, const char *fmt, ...); void fz_rethrow(fz_context *ctx);

fz_throw使用 printf 风格格式化字符串;fz_rethrow用于在fz_catch块中完成清理后把异常继续上抛。错误码枚举(来自 error.md):

enum { FZ_ERROR_SYSTEM, // 致命:内存耗尽或系统调用错误 FZ_ERROR_LIBRARY, // 第三方库的未分类错误 FZ_ERROR_ARGUMENT, // 传给函数的参数无效或越界 FZ_ERROR_LIMIT, // 资源或其他硬限制导致的失败 FZ_ERROR_UNSUPPORTED, // 尝试使用不支持的特性 FZ_ERROR_FORMAT, // 不可恢复的语法或格式错误 FZ_ERROR_SYNTAX, // 应诊断并忽略的语法错误 };

此外还有fz_warn(ctx, "warning: %s", msg)用于非致命告警,以及fz_caught_message(ctx)用于在 catch 块中取得错误信息。

完整范例:build_house

overview.md 提供了一个极具教学意义的完整模型代码,展示了嵌套 try、备选方案回退、统一清理与重新抛出的全套模式:

house build_house(plans *p) { material m = NULL; walls w = NULL; roof r = NULL; house h = NULL; tiles t = make_tiles(); fz_var(w); fz_var(r); fz_var(h); fz_try(ctx) { fz_try(ctx) { m = make_bricks(); } fz_catch(ctx) { // 没有砖可用,用稻草凑合? m = make_straw(); } w = make_walls(m, p); r = make_roof(m, t); // 注意:绝不能写 return combine(w,r); h = combine(w, r); } fz_always(ctx) { drop_walls(w); drop_roof(r); drop_material(m); drop_tiles(t); } fz_catch(ctx) { fz_throw(ctx, "build_house failed"); } return h; }

要点:

  1. make_tiles()若抛异常会直接被更外层处理器接管;若成功,tfz_try开始前已赋值,因此无需fz_var(t)
  2. 先尝试做砖,失败则回退到稻草,再失败则落入fz_catch,整个流程干净失败;
  3. 假设combine对传入的 walls 和 roof 各取一份新引用,因此wr在所有情况下都要清理;
  4. 遵循标准 C 约定:销毁NULL是安全的。

内存管理:分配器、池与引用计数

memory.md 说明:Fitz 中所有内存都通过分配器分配,可按需替换为自定义分配器。

分配与释放

void *fz_malloc(fz_context *ctx, size_t size); void *fz_realloc(fz_context *ctx, void *old, size_t size); void *fz_calloc(fz_context *ctx, size_t count, size_t size); void fz_free(fz_context *ctx, void *ptr);

与标准 C 函数的差别:它们不会返回 NULL——要么成功,要么抛异常(如FZ_ERROR_MEMORY)。另有带类型转换的宏:

T *fz_malloc_struct(fz_context *ctx, T); // 分配并清零 T *fz_malloc_array(fz_context *ctx, size_t count, T); // 分配,不初始化! T *fz_realloc_array(fz_context *ctx, T *old, size_t count, T);

极少数需要失败返回NULL的场景,可用fz_malloc_no_throw等变体。

池分配器

用于批量分配"同生共死"的小对象,池释放时其上的所有对象一并释放:

fz_pool *fz_new_pool(fz_context *ctx); void *fz_pool_alloc(fz_context *ctx, fz_pool *pool, size_t size); char *fz_pool_strdup(fz_context *ctx, fz_pool *pool, const char *s); void fz_drop_pool(fz_context *ctx, fz_pool *pool);

引用计数:keep / drop 约定

MuPDF 中大多数对象用引用计数管理生命周期,动词约定为keep(递增)与drop(递减);为统一接口,非引用计数对象也使用 drop 命名——这样将来给对象加上引用计数时,调用方代码无需改动。例如 pixmap 的 api-overview.md 所示:

fz_pixmap *fz_keep_pixmap(fz_context *ctx, fz_pixmap *pix); // 递增引用计数 void fz_drop_pixmap(fz_context *ctx, fz_pixmap *pix); // 减到 0 时释放

I/O 模块:缓冲区、流、过滤器与归档

io.md 系统介绍了 I/O 模块,包括数据缓冲区、输入/输出流、可链式组合的解码过滤器,以及文件归档(archive)抽象。

缓冲区 fz_buffer

fz_buffer表示通用的数据块,公开字段为:

typedef struct { unsigned char *data; size_t len; // 当前长度 size_t cap; // 总容量 ... 保留内部字段 ... } fz_buffer;

创建方式多种多样:

fz_buffer *fz_new_buffer(fz_context *ctx, size_t capacity); // 空缓冲区,给定初始容量 fz_buffer *fz_new_buffer_from_shared_data(fz_context *ctx, const unsigned char *data, size_t size); // 只引用,不持有;data 在缓冲区存活期间不得变动/消失 fz_buffer *fz_new_buffer_from_copied_data(fz_context *ctx, const unsigned char *data, size_t size); // 拷贝数据 fz_buffer *fz_new_buffer_from_base64(fz_context *ctx, const char *data, size_t size); // 解码 BASE64

动态追加数据(自动扩容):

void fz_append_data(fz_context *ctx, fz_buffer *buf, const void *data, size_t len); void fz_append_string(fz_context *ctx, fz_buffer *buf, const char *string); void fz_append_byte(fz_context *ctx, fz_buffer *buf, int byte); void fz_append_rune(fz_context *ctx, fz_buffer *buf, int rune); void fz_append_int16_be/le(fz_context *ctx, fz_buffer *buf, int x); // 大小端整数 void fz_append_int32_be/le(fz_context *ctx, fz_buffer *buf, int x); void fz_append_printf(fz_context *ctx, fz_buffer *buffer, const char *fmt, ...);

还可写入位流:fz_append_bits(写入 value 的低 count 位)、fz_append_bits_pad(补零到字节对齐;缓冲区长度始终覆盖所有位,最后一字节未用位恒为 0)。fz_string_from_buffer可取得以零结尾的 C 字符串指针(借用指针,仅在缓冲区再次改动前短暂使用)。文件操作:fz_read_file读文件入缓冲区、fz_save_buffer存缓冲区到文件。

输入流 fz_stream

流是数据的读取源,部分流类型可解压/解密,且可以链式组合成管道

fz_stream *fz_open_file(fz_context *ctx, const char *filename); // 读文件 fz_stream *fz_open_memory(fz_context *ctx, const unsigned char *data, size_t len); // 读内存 fz_stream *fz_open_buffer(fz_context *ctx, fz_buffer *buf); // 读缓冲区

基础操作:

int64_t fz_tell(fz_context *ctx, fz_stream *stm); void fz_seek(fz_context *ctx, fz_stream *stm, int64_t offset, int whence); size_t fz_read(fz_context *ctx, fz_stream *stm, unsigned char *data, size_t len); size_t fz_skip(fz_context *ctx, fz_stream *stm, size_t len); fz_buffer *fz_read_all(fz_context *ctx, fz_stream *stm, size_t initial); char *fz_read_line(fz_context *ctx, fz_stream *stm, char *buf, size_t n); // 类似 fgets() int fz_read_byte / fz_peek_byte / fz_is_eof(...);

定长整数读取(默认大端,另有_le小端变体):

uint16_t fz_read_uint16; uint32_t fz_read_uint24; uint32_t fz_read_uint32; uint64_t fz_read_uint64; int16_t fz_read_int16; int32_t fz_read_int32; int64_t fz_read_int64;

位流读取:fz_read_bitsfz_read_rbitsfz_sync_bitsfz_is_eof_bits

过滤器:解码/解压/解密管道

解码过滤器可链在输入流上,例如:fz_open_flated(Flate/zlib 解压)、fz_open_a85d(ASCII85 解码)、fz_open_ahxd(ASCIIHex 解码)、fz_open_rld(RunLength 解码)、fz_open_dctd(JPEG 解码,可传色彩变换、CMYK 反转、缩放因子与 JPEG 表流)、fz_open_faxd(CCITT Fax 解码,含 k、行长、字节对齐、黑白反转等参数)、fz_open_lzwd(LZW 解码)、fz_open_predict(Predictor 解码器,用于 PDF 中的 PNG/TIFF 预测)、fz_open_arc4/fz_open_aesd(ARCFOUR / AES 解密)、fz_open_null_filter(截断流)。

输出流 fz_output

输出流写入数据到汇点(通常是文件或缓冲区),同样可链式组合以压缩、加密、编码。关键区别:写操作成功后必须先fz_close_outputfz_drop_output——close 负责刷新缓冲并写出结束标记,drop 仅释放内存;写数据出错时可直接 drop(无法正常收尾)。

fz_output *fz_new_output_with_path(fz_context *, const char *filename, int append); fz_output *fz_new_output_with_buffer(fz_context *ctx, fz_buffer *buf); // 自定义汇点:提供 state 指针与 write/close/drop 回调 fz_output *fz_new_output(fz_context *ctx, int buffer_size, void *state, void (*write)(fz_context *ctx, void *state, const void *data, size_t n), void (*close)(fz_context *ctx, void *state), void (*drop)(fz_context *ctx, void *state));

写函数族:fz_write_data/string/byte/runefz_write_int16_be/lefz_write_int32_be/lefz_write_printf/vprintffz_write_base64(可换行)。链式输出过滤器:fz_new_arc4_outputfz_new_ascii85_outputfz_new_asciihex_outputfz_new_deflate_output(带压缩 effort 与头部选项)、fz_new_rle_output。注意这些过滤器不接管被链流的所有权,仅向其写入——可先写头部、创建压缩过滤器、写入数据、关闭过滤器、再继续写原流。

文件归档 fz_archive

归档是只读文件集合抽象,典型是 ZIP 或磁盘目录,也支持其他格式:

fz_archive *fz_open_directory(fz_context *ctx, const char *path); fz_archive *fz_open_archive(fz_context *ctx, const char *filename); // 自动探测 ZIP/TAR fz_archive *fz_open_archive_with_stream(fz_context *ctx, fz_stream *file); int fz_count_archive_entries(fz_context *ctx, fz_archive *arch); const char *fz_list_archive_entry(fz_context *ctx, fz_archive *arch, int idx); int fz_has_archive_entry(fz_context *ctx, fz_archive *arch, const char *name); fz_stream *fz_open_archive_entry(fz_context *ctx, fz_archive *arch, const char *name); fz_buffer *fz_read_archive_entry(fz_context *ctx, fz_archive *arch, const char *name);

反向操作:fz_zip_writer可创建新的 ZIP 归档(fz_new_zip_writerfz_write_zip_entry带压缩开关、fz_close_zip_writerfz_drop_zip_writer)。EPUB(本质是 ZIP)等复合格式文档正是依赖这套归档接口解析的。

Graphics 与渲染资源

Graphics 模块提供图形资源对象:颜色、字体、渐变、图像,以及承载渲染结果的 pixmap。结合 api-overview.md 可得到这些资源的要点:

  • 颜色空间fz_colorspace,常用fz_device_rgb(ctx)等设备颜色空间;fz_convert_pixmap可做颜色空间转换(含打样空间、颜色参数、是否保留 alpha)。
  • pixmap:光栅图像对象,公开访问器有fz_pixmap_width/height/x/y/stride/components/samples/colorspace等;创建方式包括fz_new_pixmap(指定颜色空间、宽高、分色与 alpha)、fz_new_pixmap_with_bboxfz_new_pixmap_with_data(包装既有缓冲区,负责释放)。常用操作:fz_clear_pixmapfz_scale_pixmapfz_invert_pixmapfz_gamma_pixmapfz_clone_pixmap
  • bitmap:1 位/分量半色调位图,fz_new_bitmap_from_pixmap由 pixmap 生成,用于打印输出,样本 MSB 优先、兼容 PBM 格式。
  • path:矢量路径,fz_moveto/lineto/curveto/closepath/rectto构建,fz_bound_path求包围盒;描边参数fz_stroke_state含线帽(FZ_LINECAP_BUTT/ROUND/SQUARE/TRIANGLE)、线连接(FZ_LINEJOIN_MITER/ROUND/BEVEL/MITER_XPS)、线宽、斜接限制与虚线序列。
  • 字体fz_font,配合 glyph 缓存用于文本渲染。

文档模块:打开、鉴权、分页、渲染与搜索

Document 模块是使用频率最高的高层入口。结合 api-overview.md:

打开文档与鉴权

fz_document *fz_open_document(fz_context *ctx, const char *filename); // 自动探测格式 fz_document *fz_open_document_with_stream(fz_context *ctx, const char *magic, fz_stream *stream); fz_document *fz_open_accelerated_document(fz_context *ctx, const char *filename, const char *accel); int fz_needs_password(fz_context *ctx, fz_document *doc); int fz_authenticate_password(fz_context *ctx, fz_document *doc, const char *password);

页数、页面加载与页面边界

int fz_count_pages(fz_context *ctx, fz_document *doc); // 总页数 int fz_count_chapters(fz_context *ctx, fz_document *doc); // 章数(EPUB 等多章格式) fz_page *fz_load_page(fz_context *ctx, fz_document *doc, int number); // 按扁平页号,从 0 开始 fz_page *fz_load_chapter_page(fz_context *ctx, fz_document *doc, int chapter, int page); fz_rect fz_bound_page(fz_context *ctx, fz_page *page); // 页包围盒(pt,y 轴向下)

注意文档明确:API 中页号一律从 0 开始(zero-based)

渲染

void fz_run_page(fz_context *ctx, fz_page *page, fz_device *dev, fz_matrix transform, fz_cookie *cookie);

fz_run_page把页面(内容 + 注释 + 表单控件)渲染到设备;另有fz_run_page_contents(仅内容流)、fz_run_page_annots(仅注释)、fz_run_page_widgets(仅表单控件)。fz_cookie用于渲染过程中的进度/中止控制。高阶一步到位接口在 ext/mupdf/include/mupdf/fitz/util.h:

fz_pixmap *fz_new_pixmap_from_page(fz_context *ctx, fz_page *page, fz_matrix ctm, fz_colorspace *cs, int alpha); fz_pixmap *fz_new_pixmap_from_page_number(fz_context *ctx, fz_document *doc, int number, fz_matrix ctm, fz_colorspace *cs, int alpha);

文本搜索与显示列表

搜索接口返回命中四边形(fz_quad)数组:

int fz_search_page(fz_context *ctx, fz_page *page, const char *needle, int *hit_mark, fz_quad *hit_bbox, int hit_max);

显示列表把页面渲染录制为可复用命令序列(多线程部分的关键角色):

fz_display_list *fz_new_display_list_from_page(fz_context *ctx, fz_page *page); fz_pixmap *fz_new_pixmap_from_display_list(fz_context *ctx, fz_display_list *list, fz_matrix ctm, fz_colorspace *cs, int alpha);

元数据与书签

int fz_lookup_metadata(fz_context *ctx, fz_document *doc, const char *key, char *buf, size_t size);

标准键包括"format""encryption""info:Title""info:Author"等,返回字符串长度,未找到返回 -1。书签用于跨会话恢复阅读位置:

fz_bookmark fz_make_bookmark(fz_context *ctx, fz_document *doc, fz_location loc); fz_location fz_lookup_bookmark(fz_context *ctx, fz_document *doc, fz_bookmark mark);

可重排文档(EPUB、FB2 等)使用fz_layout_document(ctx, doc, w, h, em)设定页面布局,配套预置常量如FZ_LAYOUT_KINDLE_W/H/EMFZ_LAYOUT_A5_W/H/EM

从 Hello World 到可运行程序:快速上手示例

api-overview.md 给出了一个把 PDF 第 0 页以 150 dpi 渲染为 PNG 的完整示例,集成了本文档介绍的 context、异常处理、文档/页面/pixmap 生命周期:

#include "mupdf/fitz.h" int main(void) { fz_context *ctx = fz_new_context(NULL, NULL, FZ_STORE_DEFAULT); fz_register_document_handlers(ctx); fz_document *doc = NULL; fz_page *page = NULL; fz_pixmap *pix = NULL; fz_try(ctx) { doc = fz_open_document(ctx, "input.pdf"); page = fz_load_page(ctx, doc, 0); /* 150 dpi = 150/72 scale */ fz_matrix ctm = fz_scale(150.0f / 72, 150.0f / 72); pix = fz_new_pixmap_from_page(ctx, page, ctm, fz_device_rgb(ctx), 0); fz_save_pixmap_as_png(ctx, pix, "output.png"); } fz_always(ctx) { fz_drop_pixmap(ctx, pix); fz_drop_page(ctx, page); fz_drop_document(ctx, doc); } fz_catch(ctx) { fprintf(stderr, "error: %s\n", fz_caught_message(ctx)); } fz_drop_context(ctx); return 0; }

注意其结构完全遵循前文规则:fz_new_context(NULL, NULL, FZ_STORE_DEFAULT)单线程创建、所有可能抛异常的操作收拢在fz_try内、fz_always中统一 drop(且 drop 顺序与创建相反)、fz_catch打印fz_caught_message。本仓库 ext/mupdf/docs/examples 下还有更完整的example.cmulti-threaded.csearchtest.cstorytest.c等配套示例,可作为进一步研读的起点。

多线程使用:五条铁律与 context 克隆

虽然本指南定位单线程,但 overview.md 对多线程做了完整阐述。首先明确:如果文档在一个线程中打开并充当"服务器"为其他线程提供页面渲染服务,MuPDF 始终只被单线程调用,则完全无需加锁,这是最简单高效的模式。

真正需要并发调用时,须遵守以下五条规则:

  1. 不同线程不得同时调用使用同一 context——最简单做法是每线程一个 context(通过克隆获得);
  2. 不同线程不得同时调用使用同一 document——同一时刻仅一个线程可访问文档;但从该文档创建的显示列表可被多线程同时操作
  3. 不同线程不得同时调用使用同一 device——并发调用 device 会使其状态错乱甚至崩溃;
  4. 除非纯单线程,否则创建 context 时必须提供fz_locks_context——即使使用完全独立的 MuPDF 实例,MuPDF 仍需借用户提供的锁保护跨线程的共享结构/资源/库;
  5. 所有 context 必须共享同一个fz_locks_context(或其底层锁)——强烈建议只调用一次fz_new_context,之后用fz_clone_context派生新 context;虽然目前仍支持多次fz_new_context创建完全独立的 context,但必须共享同一锁集,且该能力未来可能移除。

锁的实现细节:调用方应提供FZ_LOCK_MAX个互斥量,MuPDF 通过回调以"用户指针 + 锁编号 i(0 ≤ i < FZ_LOCK_MAX)"方式加锁/解锁;互斥量递归或非递归均可(MuPDF 只做非递归调用)。

克隆 context 的原理

每个 context 含一个异常栈,嵌套的fz_try/fz_catch会操作它,显然同一异常栈不能被多线程同时使用。但若每线程fz_new_context一个新 context,则会得到互相独立的 store/glyph cache,通常不是我们想要的。fz_clone_context因此而生:新 context 与给定 context 共享除异常栈以外的一切(store、字形缓存等),且每个克隆体仍可用fz_free_context单独释放。

通用方案:程序启动时创建一个"基础" context,随后反复克隆出供各线程使用的 context。多线程并发模式下典型架构二选一:由单一指定线程打开文档并充当"服务器"为其他线程生成显示列表(长期看更高效),或自行加互斥锁保护对文档的所有 MuPDF 调用。可参考 ext/mupdf/docs/examples/multi-threaded.c——它演示了一个主线程加每页一个渲染线程的模式。

PDF 模块:对象与流的底层访问

Document 之上的 PDF 模块提供底层 PDF 结构访问能力(introduction.md)。其核心价值在于:当高层 API 不能满足需求时,可以直接在 PDF 对象(object)与流(stream)层面:

  • 查询:检查文档特性、提取数据;
  • 修改:修改既有文档的对象与流;
  • 创建:创建新的 PDF 对象、流乃至全新文档。

对应头文件集中在 ext/mupdf/include/mupdf/pdf:object.h(PDF 对象模型,间接引用、字典、数组、名称树等)、document.hpdf_documentpdf_page的创建/保存)、xref.h(交叉引用表与流读取)、annot.h(注释)、form.h(表单字段)、clean.h(文档清理)、javascript.hrecolor.hzugferd.h等。SumatraPDF 的引擎封装(src/EngineMupdf.cpp)即同时调用了 fitz 层渲染 API 与 PDF 层对象 API,是理解两层协作的现成实例。

实用 API 速查与约定

包含方式:推荐通过伞形头文件包含整个公共 API(ext/mupdf/include/mupdf/fitz.h):

#include "mupdf/fitz.h"

该头文件按顺序聚合 Core(version.hconfig.hsystem.hcontext.houtput.hlog.h)、工具(crypt.hgeometry.hhash.hxml.hjson.h等)、I/O(buffer.hstream.hfilter.harchive.h)、资源(store.hcolor.hpixmap.himage.hfont.hpath.htext.h等)、渲染(device.hdisplay-list.hstructured-text.hglyph-cache.h)与文档(link.houtline.hdocument.h)各组头文件。

三条全局约定(来自 api-overview.md):

  1. 所有字符串参数默认UTF-8 编码(除非另有说明);
  2. 全程页号从 0 开始
  3. fz_context非线程安全——每线程用fz_clone_context建独立 context,并通过锁共享资源存储;
  4. 内存管理遵循keep/drop模式:fz_keep_*递增引用计数,fz_drop_*递减并在归零时释放。

总结

MuPDF 的 C API 设计层次清晰:Core提供上下文与异常地基,I/O提供缓冲/流/过滤器的数据管道,Graphics提供渲染所需的资源对象,Device以回调抽象解耦"内容解析"与"内容输出",Document在高层串起渲染、转换与搜索,PDF则在底层暴露对象与流级别的完全控制。对开发者而言,抓住三条主线即可快速上手:一切以fz_context为入口、一切可能失败的调用放进fz_try/fz_catch、一切资源遵循 keep/drop 与fz_always统一清理。本仓库中,SumatraPDF 的 src/EngineMupdf.cpp 与 ext/mupdf/docs/examples 下的示例程序,是这套 API 从文档走向生产代码的最佳范本。

  • 桌面应用
  • 文档

【免费下载链接】sumatrapdf

SumatraPDF reader

项目地址:https://gitcode.com/gh_mirrors/su/sumatrapdf
点击查看免费下载
上一篇:CocoaLumberjack性能调优指南:从源码级别优化日志效率
下一篇:如何永久保存微信聊天记录:WeChatMsg完整使用指南

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

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

Grafana图像渲染插件安装与依赖缺失终极指南

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

作者头像 李华
网站建设 2026/9/21 2:37:05

VMware虚拟机光标消失原因与修复指南

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

作者头像 李华
网站建设 2026/9/21 2:37:02

从Fastjson 1.x迁移到Fastjson2:性能、安全与API兼容性实践指南

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

作者头像 李华
网站建设 2026/9/21 2:36:15

基于OpenCV和Python的车牌识别系统实现:从图像处理到模板匹配

简介&#xff1a;一套基于OpenCV与Python的车牌识别毕业设计项目&#xff0c;整合Tkinter图形界面与SVM分类模型&#xff0c;面向计算机视觉、图像处理方向的本科毕设、课程设计及实战学习者。系统实现车牌定位、字符分割、特征提取与自动识别&#xff0c;代码覆盖灰度化、直方…

作者头像 李华
网站建设 2026/9/21 2:35:23

连续小波变换原理详解:从傅里叶死穴到Python时频图实操

站在信号处理这个行当里摸爬滚打这些年&#xff0c;我越来越觉得“连续小波变换&#xff08;CWT&#xff09;”是个被低估的工具。很多人一听“时频局部分析”就觉得高深&#xff0c;其实它解决的是一个特别接地气的问题&#xff1a;傅里叶变换能告诉你信号里有什么频率&#x…

作者头像 李华