MuPDF C Cookbook 实战:解析 SumatraPDF 内置 MuPDF 的渲染、多线程与 Story API 示例

发布时间:2026/9/21 2:05:15
MuPDF C Cookbook 实战:解析 SumatraPDF 内置 MuPDF 的渲染、多线程与 Story API 示例 MuPDF C Cookbook 实战解析 SumatraPDF 内置 MuPDF 的渲染、多线程与 Story API 示例【免费下载链接】sumatrapdfSumatraPDF reader项目地址: https://gitcode.com/gh_mirrors/su/sumatrapdf本指南以当前仓库ext/mupdf内置的 MuPDF 官方 C 语言 Cookbook 为核心完整解析其三个实战示例将 PDF 渲染为 PPM 图片的单页渲染、基于 pthread 的多线程 PNG 渲染以及使用 Story API 由 HTML 模板生成 PDF 的文档编排。读完本文你将掌握 MuPDF 的fz_context异常模型、fz_try/fz_catch资源管理范式、显示列表display list与设备device渲染管线、多线程上下文克隆与锁机制以及 Story API 的分页排版流程并能在本仓库内直接构建运行这些示例。一、这份 Cookbook 在哪里仓库位置与阅读方式当前仓库gh_mirrors/su/sumatrapdf是 SumatraPDF 阅读器的完整源码树其中ext/mupdf目录内嵌了 SumatraPDF 所依赖的 MuPDF 渲染引擎及其官方文档。本文所讨论的 C 语言 Cookbook 位于目录索引ext/mupdf/docs/cookbook/c/index.rst三个章节ext/mupdf/docs/cookbook/c/example.rstRender Images渲染图片ext/mupdf/docs/cookbook/c/multi-threaded.rstMulti-threaded多线程渲染ext/mupdf/docs/cookbook/c/storytest.rstStoriesStory API 示例index.rst本身只是一份toctree导航真正的技术内容通过 reStructuredText 的literalinclude指令将ext/mupdf/docs/examples/目录下的三个 C 源文件原样嵌入到文档中。也就是说食谱recipe即代码阅读文档就等于阅读这些可编译、可运行的完整示例文档章节嵌入的源文件核心主题Render Imagesext/mupdf/docs/examples/example.c将文档指定页渲染为 ASCII PPM 图片Multi-threadedext/mupdf/docs/examples/multi-threaded.c多线程并发渲染所有页面为 PNGStoriesext/mupdf/docs/examples/storytest.c用 HTML 模板经 Story API 生成 PDF这三个示例正好覆盖了 MuPDF C API 的三条主线单页渲染的完整生命周期、多线程渲染的正确并发模型、面向文档生成的 Story 排版引擎。下面逐例展开。二、示例一Render Images —— 单页渲染为 PPM该示例的目标是把一个文档PDF、XPS、CBZ 或 EPUB的指定页渲染成一幅 PPMP3 文本格式图片并输出到标准输出。它是一份最小完整 MuPDF 程序演示了使用 MuPDF 时几乎必然遇到的每一个 API 环节。2.1 命令行用法与参数语义源码开头的注释即原文档直接展示的内容给出了两种构建与运行方式# 方式一在 MuPDF 源码树内构建并运行 make examples ./build/debug/example document.pdf 1 100 0 page1.ppm# 方式二使用已安装的库手工编译 gcc -I/usr/local/include -o example \ /usr/local/share/doc/mupdf/examples/example.c \ /usr/local/lib/libmupdf.a \ /usr/local/lib/libmupdfthird.a \ -lm ./example document.pdf 1 100 0 page1.ppm程序运行时参数语义如下程序自身在usage输出中亦有说明参数含义默认值input-file要打开的文档路径支持 PDF、XPS、CBZ、EPUB 等 MuPDF 支持的格式必填page-number页码从 1 开始内部会转换为从 0 开始的索引必填zoom缩放百分比100% 对应 72 dpi 的基准分辨率100rotate顺时针旋转角度度02.2 完整源码/* How to use MuPDF to render a single page and print the result as a PPM to stdout. */ #include mupdf/fitz.h #include stdio.h #include stdlib.h int main(int argc, char **argv) { char *input; float zoom, rotate; int page_number, page_count; fz_context *ctx; fz_document *doc; fz_pixmap *pix; fz_matrix ctm; int x, y; if (argc 3) { fprintf(stderr, usage: example input-file page-number [ zoom [ rotate ] ]\n); fprintf(stderr, \tinput-file: path of PDF, XPS, CBZ or EPUB document to open\n); fprintf(stderr, \tPage numbering starts from one.\n); fprintf(stderr, \tZoom level is in percent (100 percent is 72 dpi).\n); fprintf(stderr, \tRotation is in degrees clockwise.\n); return EXIT_FAILURE; } input argv[1]; page_number atoi(argv[2]) - 1; zoom argc 3 ? atof(argv[3]) : 100; rotate argc 4 ? atof(argv[4]) : 0; /* Create a context to hold the exception stack and various caches. */ ctx fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED); if (!ctx) { fprintf(stderr, cannot create mupdf context\n); return EXIT_FAILURE; } /* Register the default file types to handle. */ fz_try(ctx) fz_register_document_handlers(ctx); fz_catch(ctx) { fz_report_error(ctx); fprintf(stderr, cannot register document handlers\n); fz_drop_context(ctx); return EXIT_FAILURE; } /* Open the document. */ fz_try(ctx) doc fz_open_document(ctx, input); fz_catch(ctx) { fz_report_error(ctx); fprintf(stderr, cannot open document\n); fz_drop_context(ctx); return EXIT_FAILURE; } /* Count the number of pages. */ fz_try(ctx) page_count fz_count_pages(ctx, doc); fz_catch(ctx) { fz_report_error(ctx); fprintf(stderr, cannot count number of pages\n); fz_drop_document(ctx, doc); fz_drop_context(ctx); return EXIT_FAILURE; } if (page_number 0 || page_number page_count) { fprintf(stderr, page number out of range: %d (page count %d)\n, page_number 1, page_count); fz_drop_document(ctx, doc); fz_drop_context(ctx); return EXIT_FAILURE; } /* Compute a transformation matrix for the zoom and rotation desired. */ /* The default resolution without scaling is 72 dpi. */ ctm fz_scale(zoom / 100, zoom / 100); ctm fz_pre_rotate(ctm, rotate); /* Render page to an RGB pixmap. */ fz_try(ctx) pix fz_new_pixmap_from_page_number(ctx, doc, page_number, ctm, fz_device_rgb(ctx), 0); fz_catch(ctx) { fz_report_error(ctx); fprintf(stderr, cannot render page\n); fz_drop_document(ctx, doc); fz_drop_context(ctx); return EXIT_FAILURE; } /* Print image data in ascii PPM format. */ printf(P3\n); printf(%d %d\n, pix-w, pix-h); printf(255\n); for (y 0; y pix-h; y) { unsigned char *p pix-samples[y * pix-stride]; for (x 0; x pix-w; x) { if (x 0) printf( ); printf(%3d %3d %3d, p[0], p[1], p[2]); p pix-n; } printf(\n); } /* Clean up. */ fz_drop_pixmap(ctx, pix); fz_drop_document(ctx, doc); fz_drop_context(ctx); return EXIT_SUCCESS; }2.3 关键 API 与执行流拆解1fz_contextMuPDF 一切的起点。代码注释明确指出context 用于持有异常栈和各类缓存。fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED)的前两个参数分别是分配器allocator与多线程锁结构单线程程序中传NULL即可第三个参数是存储缓存上限FZ_STORE_UNLIMITED表示不限制。在第二个多线程示例中正是这个参数位置传入锁结构我们稍后会看到。2fz_try/fz_catch异常模型与资源安全。MuPDF 不使用errno或返回码报告大多数错误而是依赖 context 内的异常栈。每个可能抛异常的操作都要包在fz_try(ctx) { ... } fz_catch(ctx) { ... }中fz_catch块内先fz_report_error(ctx)打印错误再按需释放已持有的资源。本示例在每一步失败路径上都精确地依次fz_drop_context或fz_drop_document避免泄漏。这一范式在所有 MuPDF 程序中贯穿始终也是理解第三个 Story 示例中fz_always块作用的基础。3文档打开与页数查询。fz_register_document_handlers(ctx)注册 MuPDF 内置的所有文档格式处理器之后fz_open_document(ctx, input)会根据文件内容自动识别格式因此同一个调用即可打开 PDF、XPS、CBZ、EPUBfz_count_pages(ctx, doc)返回页数程序据此校验用户给出的页码是否越界。4变换矩阵缩放与旋转的组合。渲染前构造一个fz_matrixctm fz_scale(zoom / 100, zoom / 100); ctm fz_pre_rotate(ctm, rotate);由于 MuPDF 的基准分辨率是 72 dpifz_scale的参数即相对 1:1 的倍率故缩放百分比要除以 100fz_pre_rotate在缩放基础上叠加顺时针旋转。5一行完成渲染。fz_new_pixmap_from_page_number(ctx, doc, page_number, ctm, fz_device_rgb(ctx), 0)把第page_number页从 0 起按ctm变换渲染到 RGB 色域的 pixmap最后一个参数0表示不渲染透明背景。这是最高层的便捷函数想理解底层机制需要看第二个示例——它手动拆解了fz_new_pixmap_with_bboxfz_new_draw_devicefz_run_display_list的完整管线。6PPM 输出与清理。程序按 PPM P3纯文本格式输出第一行P3第二行宽高第三行最大颜色值255随后按行输出每个像素的 R、G、B。pixmap 内部按stride行跨度与n每像素通道数RGB 为 3组织samples缓冲区。最后按后创建先释放的顺序fz_drop_pixmap→fz_drop_document→fz_drop_context。在 SumatraPDF 项目中这一打开文档 → 取页 → 构造矩阵 → 渲染 pixmap的流程正是上层 src/EngineMupdf.cpp 渲染 PDF 页面时的底层骨架。阅读本示例有助于理解阅读器逐页渲染时每一步在引擎内部发生了什么。三、示例二Multi-threaded —— 多线程并发渲染 PNG第二个示例演示了 MuPDF 官方推荐的多线程渲染模式主线程负责解析文档、构造显示列表每个页面一个工作线程负责渲染主线程随后逐个 join 并写出 PNG。原文档明确要求先读懂example.c并阅读 MuPDF 参考手册中多线程一节再回到本示例因为它建立在示例一的全部概念之上。3.1 构建与运行# 在 MuPDF 源码树内 make examples ./build/debug/multi-threaded document.pdf# 使用已安装的库手工编译注意多了 -lpthread gcc -I/usr/local/include -o multi-threaded \ /usr/local/share/doc/mupdf/examples/multi-threaded.c \ /usr/local/lib/libmupdf.a \ /usr/local/lib/libmupdfthird.a \ -lpthread -lm ./multi-threaded document.pdf运行后按页码顺序生成out0000.png、out0001.png…… 等文件。原文档附有两条重要提醒所有页面同时渲染请选用页数较少的文档以免对机器造成过大压力每个页面一个线程线程数量可能受到运行环境的限制。3.2 为什么需要锁MuPDF 的多线程模型MuPDF 的核心库并非为每个调用内部加锁的线程安全库。官方给出的并发模型是一个 context 同时只能被一个线程使用多线程时每个工作线程通过fz_clone_context从主 context 克隆出自己的私有 context同时为库内共享的全局状态字体缓存、颜色管理等提供用户自定义的锁。因此程序首先初始化FZ_LOCK_MAX个非递归互斥量并把加锁/解锁函数打包进fz_locks_contextvoid lock_mutex(void *user, int lock) { pthread_mutex_t *mutex (pthread_mutex_t *) user; if (pthread_mutex_lock(mutex[lock]) ! 0) fail(pthread_mutex_lock()); } void unlock_mutex(void *user, int lock) { pthread_mutex_t *mutex (pthread_mutex_t *) user; if (pthread_mutex_unlock(mutex[lock]) ! 0) fail(pthread_mutex_unlock()); }locks.user mutex; /* 指向互斥量数组避免全局变量 */ locks.lock lock_mutex; locks.unlock unlock_mutex; ctx fz_new_context(NULL, locks, FZ_STORE_UNLIMITED);lock参数是 MuPDF 内部定义的锁编号范围 0..FZ_LOCK_MAX-1这里直接把锁编号当作互斥量数组下标使用。只有创建主 context 时传入锁结构克隆出的 context 才会继承同一套锁。3.3 主线程与渲染线程之间传递的数据结构struct thread_data { fz_context *ctx; /* 主线程 context用于在工作线程内克隆 */ int pagenumber; /* 页码从 1 起用于打印 */ fz_display_list *list; /* 页面绘制命令显示列表 */ fz_rect bbox; /* 要渲染的页面区域 */ fz_pixmap *pix; /* 渲染结果 pixmap由渲染线程填充 */ int failed; /* 渲染线程是否失败 */ };这里的关键设计是显示列表主线程把每页的绘制命令记录成fz_display_list显示列表与任何线程无关可以被工作线程安全消费这是 MuPDF 多线程渲染得以成立的基石。3.4 渲染线程克隆上下文 → 建 pixmap → 跑显示列表每个工作线程执行renderer函数。它的第一步就是克隆私有 context主线程传入的ctx指针不能直接跨线程使用ctx fz_clone_context(ctx);随后在fz_try内完成渲染data-pix fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), fz_round_rect(bbox), NULL, 0); fz_clear_pixmap_with_value(ctx,>fz_var(dev); fz_try(ctx) { /* ... 渲染 ... */ } fz_always(ctx) fz_drop_device(ctx, dev); fz_catch(ctx) >page fz_load_page(ctx, doc, i); /* 加载页面 */ bbox fz_bound_page(ctx, page); /* 计算页面边界框 */ list fz_new_display_list(ctx, bbox); /* 为页面创建显示列表 */ dev fz_new_list_device(ctx, list); /* 列表设备把命令写入显示列表 */ fz_run_page(ctx, page, dev, fz_identity, NULL); fz_close_device(ctx, dev); /* fz_always: 丢弃 device 与 page绘制命令已全部落入 list */原文档特别强调页面的加载fz_load_page只能在主线程做因为同一时刻只能有一个线程访问 document页面一旦被录制进显示列表fz_drop_page后列表仍可安全地交给任意工作线程。之后程序填充thread_data、pthread_create创建线程。渲染完成后主线程依次pthread_join检查data-failed成功则用fz_save_pixmap_as_png(ctx,>/* Multi-threaded rendering of all pages in a document to PNG images. */ #include mupdf/fitz.h #include stdio.h #include stdlib.h #include pthread.h void fail(const char *msg) { fprintf(stderr, %s\n, msg); abort(); } struct thread_data { fz_context *ctx; int pagenumber; fz_display_list *list; fz_rect bbox; fz_pixmap *pix; int failed; }; void *renderer(void *data_) { struct thread_data *data (struct thread_data *)data_; int pagenumber >static void test_story(fz_context *ctx, const char *filename, const char *options, const char *storytext) { fz_document_writer *writer NULL; fz_story *story NULL; fz_buffer *buf NULL; fz_device *dev NULL; fz_archive *archive NULL; fz_rect mediabox { 0, 0, 512, 640 }; /* 页面媒体框尺寸 */ float margin 10; int more; fz_var(writer); fz_var(story); fz_var(buf); fz_var(dev); fz_var(archive); fz_try(ctx) { writer fz_new_pdf_writer(ctx, filename, options); /* 输出 PDF */ buf fz_new_buffer_from_copied_data(ctx, (unsigned char *)storytext, strlen(storytext)1); archive fz_open_directory(ctx, .); /* 解析相对资源图片等 */ story fz_new_story(ctx, buf, /* user_css */, 11 /* em 字号 */, archive); do { fz_rect where, filled; /* 每页的可排版区域媒体框向内缩 margin */ where.x0 mediabox.x0 margin; where.y0 mediabox.y0 margin; where.x1 mediabox.x1 - margin; where.y1 mediabox.y1 - margin; dev fz_begin_page(ctx, writer, mediabox); /* 尝试把剩余内容排进 where返回是否还有剩余需要新页 */ more fz_place_story(ctx, story, where, filled); /* 把已排版的内容绘制到当前页 */ fz_draw_story(ctx, story, dev, fz_identity); fz_end_page(ctx, writer); } while (more); /* 有剩余内容就开新页继续 */ fz_close_document_writer(ctx, writer); } fz_always(ctx) { fz_drop_story(ctx, story); fz_drop_buffer(ctx, buf); fz_drop_document_writer(ctx, writer); fz_drop_archive(ctx, archive); } fz_catch(ctx) { fz_report_error(ctx); } }这段代码把 Story 的工作方式讲得很透彻fz_new_story(ctx, buf, user_css, em, archive)用 HTML 文本构建 storyuser_css可传入附加 CSS此处为空字符串em是基准字号archive用于解析 HTML 中引用的相对资源如img src...示例用fz_open_directory(ctx, .)打开当前目录作为资源容器排版与绘制分离fz_place_story负责把内容放入给定矩形where并返回more剩余内容是否需新页fz_draw_story再把已排版部分绘制到页面设备两者之间是fz_begin_page/fz_end_page整个do { ... } while (more)循环即一页页吞掉故事内容的自动分页流程媒体框被固定为 512×640页边距 10。4.2 用 HTML 模板填充数据id 与元素定位storytest.c还演示了如何在 HTML 模板中通过id标记待填充的占位元素。它内置了一份电影节模板const char *festival_template htmlheadtitleWhy do we have a title? Why not?/title/head bodyh1 style\text-align:center\Hook Norton Film Festival/h1 ol li id\filmtemplate\ b id\filmtitle\/b dl dtDirectordd id\director\ dtRelease Yeardd id\filmyear\ dtCastdd id\cast\ /dl /li ul /body/html;配合程序内置的电影数据film_t结构体数组含《Pulp Fiction》《The Usual Suspects》《Fight Club》三部影片的片名、导演、年份与演员表Story 引擎即可按模板重复排版生成目录式页面。这套HTML 模板 数据填充的思路是 MuPDF 面向动态文档生成的推荐路径。4.3 稳定化输出fz_write_stabilized_story与三个回调test_write_stabilized_story()展示了更高层的fz_write_stabilized_story接口——一次调用完成HTML 内容 → 分页 → 附加页眉/目录的稳定化排版通过三个回调实现回调作用contentfn(ctx, ref, positions, buffer)根据fz_write_story_positions各元素的页码、深度、heading、矩形、文本等向输出 buffer 追加 HTML 内容例如生成目录rectfn(ctx, ref, num, filled, rect, ctm, mediabox)决定每个稳定页的可排区域与媒体框num0与后续页可返回不同矩形pagefn(ctx, ref, page_num, mediabox, dev, after)在每页绘制前后挂钩用fz_fill_path等绘制装饰图形示例在每页角落画两个不同颜色的三角形static void test_write_stabilized_story(fz_context *ctx) { fz_document_writer *writer fz_new_pdf_writer(ctx, out_toc.pdf, ); fz_try(ctx) { fz_write_stabilized_story(ctx, writer, /*user_css*/, 11 /*em*/, toc_contentfn, NULL /*contentfn_ref*/, toc_rectfn, NULL /*rectfn_ref*/, toc_pagefn, NULL /*pagefn_ref*/, NULL /* archive */); fz_close_document_writer(ctx, writer); } fz_always(ctx) fz_drop_document_writer(ctx, writer); fz_catch(ctx) fz_rethrow(ctx); }contentfn中遍历positions打印每个元素的页码、深度、是否为标题、id、矩形与文本并据此动态构造带链接的目录 HTML——这是实现自动生成目录页的直接范例。4.4 排版特性的测试矩阵storytest.c后半部分是一组针对排版细节的回归测试每项测试输出一个独立 PDF可一一验证渲染结果定位测试test_positionsposition: static / relative / fixed / absolute四种定位模式分别配合top/left/bottom/right偏移以及带固定宽高的变体输出pos_static.pdf、pos_relative.pdf、pos_fixed.pdf、pos_absolute.pdf、pos_fixed_sizes.pdf、pos_absolute_sizes.pdf表格测试test_tables/test_tablespanscolgroup、col span、rowspan/colspan、valign/align、行高、单元格宽度等输出tables.pdf、tablespan.pdf边框测试test_tableborders/test_tableborderwidthsoutset/inset/ridge/groove/double/solid/dashed/dotted等全部边框样式及 1px~8px 宽度梯度输出tableborders.pdf、tableborderwidths.pdf。示例开头的 HTML 常量snark还演示了-mupdf-leading自定义 CSS 属性如-mupdf-leading:7pt;——MuPDF 对 HTML/CSS 子集做了专属扩展用于精确控制行距。五、这三个示例在 SumatraPDF 中的位置需要说明的是这份 Cookbook 属于ext/mupdf内置的 MuPDF 上游文档但它对 SumatraPDF 的读者有直接的工程价值渲染管线同源SumatraPDF 对 PDF 的实际渲染由 src/EngineMupdf.cpp 驱动 MuPDF 完成示例一、二所演示的context → document → page → matrix → pixmap → draw device正是该引擎内部的调用骨架多线程思想一致SumatraPDF 的后台页面渲染服务 src/PageRenderService.cpp 同样遵循解析与渲染分离、显示列表跨线程复用的思路示例二可作为理解其并发模型的最小原型可直接构建验证在仓库的ext/mupdf子树内按 MuPDF 源码树方式执行make examples即可得到example、multi-threaded、storytest三个可执行程序或用文档给出的gcc命令行链接libmupdf.a与libmupdfthird.a手工编译便于隔离测试 MuPDF 渲染行为。六、小结这份 C Cookbook 用三个层层递进的示例覆盖了 MuPDF C API 的核心面example.c最小的完整渲染程序奠定fz_context、fz_try/fz_catch、矩阵变换与 pixmap 输出等基础范式multi-threaded.c给出官方认可的多线程模型——主线程加载页面与录制显示列表、工作线程fz_clone_context后渲染并示范fz_locks_context锁接口的正确接入方式storytest.c展示 Story API 的place/draw 分页循环、HTML 模板数据填充、fz_write_stabilized_story三回调机制以及覆盖定位、表格、边框的排版测试矩阵。对希望深入 SumatraPDF 渲染机制、或准备基于 MuPDF 做二次开发的读者而言这三个源文件example.c、multi-threaded.c、storytest.c是最值得精读的起点——它们既是文档、又是可运行的程序也是理解阅读器底层引擎行为的最短路径。【免费下载链接】sumatrapdfSumatraPDF reader项目地址: https://gitcode.com/gh_mirrors/su/sumatrapdf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考