MuPDF C Cookbook 实战:解析 SumatraPDF 内置 MuPDF 的渲染、多线程与 Story API 示例
2026/9/21 2:05:06 网站建设 项目流程

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.rst(Render Images:渲染图片)
    • ext/mupdf/docs/cookbook/c/multi-threaded.rst(Multi-threaded:多线程渲染)
    • ext/mupdf/docs/cookbook/c/storytest.rst(Stories:Story 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多线程并发渲染所有页面为 PNG
Storiesext/mupdf/docs/examples/storytest.c用 HTML 模板经 Story API 生成 PDF

这三个示例正好覆盖了 MuPDF C API 的三条主线:单页渲染的完整生命周期多线程渲染的正确并发模型面向文档生成的 Story 排版引擎。下面逐例展开。

二、示例一:Render Images —— 单页渲染为 PPM

该示例的目标是把一个文档(PDF、XPS、CBZ 或 EPUB)的指定页渲染成一幅 PPM(P3 文本格式)图片并输出到标准输出。它是一份"最小完整 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 的基准分辨率100
rotate顺时针旋转角度(度)0

2.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 与执行流拆解

(1)fz_context:MuPDF 一切的起点。代码注释明确指出,context 用于"持有异常栈和各类缓存"。fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED)的前两个参数分别是分配器(allocator)与多线程锁结构,单线程程序中传NULL即可;第三个参数是存储缓存上限,FZ_STORE_UNLIMITED表示不限制。在第二个多线程示例中,正是这个参数位置传入锁结构,我们稍后会看到。

(2)fz_try/fz_catch:异常模型与资源安全。MuPDF 不使用errno或返回码报告大多数错误,而是依赖 context 内的异常栈。每个可能抛异常的操作都要包在fz_try(ctx) { ... } fz_catch(ctx) { ... }中,fz_catch块内先fz_report_error(ctx)打印错误,再按需释放已持有的资源。本示例在每一步失败路径上都精确地依次fz_drop_contextfz_drop_document,避免泄漏。这一范式在所有 MuPDF 程序中贯穿始终,也是理解第三个 Story 示例中fz_always块作用的基础。

(3)文档打开与页数查询。fz_register_document_handlers(ctx)注册 MuPDF 内置的所有文档格式处理器,之后fz_open_document(ctx, input)会根据文件内容自动识别格式(因此同一个调用即可打开 PDF、XPS、CBZ、EPUB);fz_count_pages(ctx, doc)返回页数,程序据此校验用户给出的页码是否越界。

(4)变换矩阵:缩放与旋转的组合。渲染前构造一个fz_matrix

ctm = fz_scale(zoom / 100, zoom / 100); ctm = fz_pre_rotate(ctm, rotate);

由于 MuPDF 的基准分辨率是 72 dpi,fz_scale的参数即相对 1:1 的倍率,故缩放百分比要除以 100;fz_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_bbox+fz_new_draw_device+fz_run_display_list的完整管线。

(6)PPM 输出与清理。程序按 PPM P3(纯文本)格式输出:第一行P3,第二行宽高,第三行最大颜色值255,随后按行输出每个像素的 R、G、B。pixmap 内部按stride(行跨度)与n(每像素通道数,RGB 为 3)组织samples缓冲区。最后按"后创建先释放"的顺序fz_drop_pixmapfz_drop_documentfz_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.pngout0001.png…… 等文件。原文档附有两条重要提醒:

  • 所有页面同时渲染,请选用页数较少的文档,以免对机器造成过大压力;
  • 每个页面一个线程,线程数量可能受到运行环境的限制

3.2 为什么需要锁:MuPDF 的多线程模型

MuPDF 的核心库并非为每个调用内部加锁的"线程安全库"。官方给出的并发模型是:一个 context 同时只能被一个线程使用;多线程时,每个工作线程通过fz_clone_context从主 context 克隆出自己的私有 context,同时为库内共享的全局状态(字体缓存、颜色管理等)提供用户自定义的锁。

因此程序首先初始化FZ_LOCK_MAX个非递归互斥量,并把加锁/解锁函数打包进fz_locks_context

void 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_datapthread_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 文本构建 story:user_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 = "<html><head><title>Why do we have a title? Why not?</title></head>" "<body><h1 style=\"text-align:center\">Hook Norton Film Festival</h1>" "<ol>" "<li id=\"filmtemplate\">" "<b id=\"filmtitle\"></b>" "<dl>" "<dt>Director<dd id=\"director\">" "<dt>Release Year<dd id=\"filmyear\">" "<dt>Cast<dd 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)决定每个"稳定页"的可排区域与媒体框;num==0与后续页可返回不同矩形
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_positions):position: static / relative / fixed / absolute四种定位模式,分别配合top/left/bottom/right偏移,以及带固定宽高的变体,输出pos_static.pdfpos_relative.pdfpos_fixed.pdfpos_absolute.pdfpos_fixed_sizes.pdfpos_absolute_sizes.pdf
  • 表格测试test_tables/test_tablespans):colgroupcol spanrowspan/colspanvalign/align、行高、单元格宽度等,输出tables.pdftablespan.pdf
  • 边框测试test_tableborders/test_tableborderwidths):outset/inset/ridge/groove/double/solid/dashed/dotted等全部边框样式及 1px~8px 宽度梯度,输出tableborders.pdftableborderwidths.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,即可得到examplemulti-threadedstorytest三个可执行程序;或用文档给出的gcc命令行链接libmupdf.alibmupdfthird.a手工编译,便于隔离测试 MuPDF 渲染行为。

六、小结

这份 C Cookbook 用三个层层递进的示例覆盖了 MuPDF C API 的核心面:

  1. example.c:最小的完整渲染程序,奠定fz_contextfz_try/fz_catch、矩阵变换与 pixmap 输出等基础范式;
  2. multi-threaded.c:给出官方认可的多线程模型——主线程加载页面与录制显示列表、工作线程fz_clone_context后渲染,并示范fz_locks_context锁接口的正确接入方式;
  3. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询