ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MuPDF C API 编程实战:fz_context、异常处理与多线程渲染完全指南(基于 SumatraPDF 仓库源码)

MuPDF C API 编程实战:fz_context、异常处理与多线程渲染完全指南(基于 SumatraPDF 仓库源码) MuPDF C API 编程实战fz_context、异常处理与多线程渲染完全指南基于 SumatraPDF 仓库源码【免费下载链接】sumatrapdfSumatraPDF reader项目地址: https://gitcode.com/gh_mirrors/su/sumatrapdf导读本文以 SumatraPDF 仓库中内置的 MuPDF 源码位于 ext/mupdf为核心系统讲解 MuPDF C 接口的三大基石fz_context 上下文模型、基于宏的 fz_try/fz_always/fz_catch 异常处理以及多线程渲染。读者将掌握 context 的创建、克隆与释放理解 MuPDF 不使用 C 却实现 try/catch 语义的底层原理并学会在多线程应用中正确配置 fz_locks_context、利用 display list 实现跨线程渲染。文中所有关键结论均可在仓库源码与示例程序example.c、multi-threaded.c中找到依据并辅以 SumatraPDF 自身在 src/EngineMupdf.cpp 中的真实集成方式作为印证。一、最小可用示例从打开文档到输出像素MuPDF 官方在ext/mupdf/docs/examples/目录下提供了完整可编译的示例。其中 example.c 演示了最基础的用法——打开一个 PDF/XPS/CBZ/EPUB 文档渲染某一页并以 ASCII PPM 格式输出到标准输出。#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; }该示例的运行方式源码注释中有完整说明# 在源码树内构建并渲染第一页100% 缩放、0 度旋转 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几个关键参数需要留意页码从 1 开始代码中atoi(argv[2]) - 1转为内部从 0 计数缩放比例是百分比100% 对应 72 dpi 基准分辨率旋转单位为顺时针角度通过fz_scale与fz_pre_rotate组合出变换矩阵ctm渲染目标是 RGB 颜色空间fz_device_rgb(ctx)输出 pixmap 的samples、stride、w、h、n字段直接可读。示例特意指出这段代码没有任何错误处理目的是降低入门复杂度任何严肃的程序都必须使用下文描述的 fz_try/fz_catch 异常处理策略——示例中每个fz_try/fz_catch块实际上就是这一策略的最小形态。二、公共函数参数fz_context 到底是什么2.1 为什么几乎所有 MuPDF 函数都带一个 ctxMuPDF 接口中绝大多数函数都接受一个fz_context *ctx参数。这个 context 承载了 MuPDF 在解析、渲染文档时的全部全局状态官方文档明确列举了它包含的内容异常栈exception stack支撑下文 fz_try/fz_catch 宏机制的运行环境内存分配器memory allocator允许调用方注入自定义 malloc/realloc/free资源存储resource store用于缓存图片、字体等渲染资源一组锁及加锁/解锁函数locks用于多线程安全。如果没有提供锁及配套函数那么该 context及其克隆只能在单线程应用中使用。2.2 源码级验证fz_context 的内部结构在 ext/mupdf/include/mupdf/fitz/context.h 中可以查到struct fz_context的完整定义。从源码结构看它内部明确分为allocfz_alloc_context、locksfz_locks_context用户在创建时注入的分配器与锁errorfz_error_context持有异常栈槽位stack[256]、当前错误码、errno 与错误消息缓冲最大 256 字节消息这是每个 context独立拥有的部分warnfz_warn_context警告消息缓冲与去重计数aafz_aa_context抗锯齿位数0~8、最小线宽等渲染参数也是每个 context 独立的共享部分font字体上下文、store资源存储、glyph_cache字形缓存、colorspace颜色空间等这些在克隆 context 之间共享。正是独立异常栈 共享缓存这一划分构成了多线程中每个线程克隆一个 context方案的基础。2.3 创建与释放 context创建与释放的入口context.hfz_new_context(alloc, locks, max_store)三个参数分别为自定义分配器、锁集合、资源存储上限字节数。alloc 与 locks 均可传NULL分别表示使用标准库分配器、单线程模式max_store可传FZ_STORE_UNLIMITED0不设上限或FZ_STORE_DEFAULT256 20约 256 MiB 的合理上限。创建失败时返回NULL。fz_clone_context(ctx)克隆一个 context供多线程使用详见第五节。fz_drop_context(ctx)释放 context 及其全局状态会顺带 flush 缓冲的警告传NULL则什么都不做。注意不能在一个正处于活动状态的 fz_try/fz_always/fz_catch 块内释放该块所用的 context。三、错误处理不靠 C 的 try/catch3.1 核心机制MuPDF 使用一组异常处理宏来简化错误返回与资源清理。从概念上讲它们与 C 的 try/catch 非常相似但不需要任何特殊编译器支持——其底层是 C 的setjmp/longjmp。基本形式如下fz_try(ctx) { // Try to perform a task. Never return, goto or // longjmp out of here. break may be used to // safely exit (just) the try block scope. } fz_always(ctx) { // Any code here is always executed, regardless of // whether an exception was thrown within the try or // not. Never return, goto or longjmp out from // here. break may be used to safely exit (just) the // always block scope. } fz_catch(ctx) { // This code is called (after any always block) only // if something within the fz_try block (including any // functions it called) threw an exception. The code // here is expected to handle the exception (maybe // record/report the error, cleanup any stray state // etc) and can then either exit the block, or pass on // the exception to a higher level (enclosing) fz_try // block (using fz_throw, or fz_rethrow). }其中fz_always 块是可选的可以安全省略。宏的真实定义可以在 context.h 中看到它们只是一段黑盒式的宏展开#define fz_var(var) fz_var_imp((void *)(var)) #define fz_try(ctx) if (!fz_setjmp(*fz_push_try(ctx))) if (fz_do_try(ctx)) do #define fz_always(ctx) while (0); if (fz_do_always(ctx)) do #define fz_catch(ctx) while (0); if (fz_do_catch(ctx))配套的抛错与查询 API均声明于 context.hfz_throw(ctx, errcode, fmt, ...)/fz_vthrow抛出异常必须处于某个外层 fz_try 块内fz_rethrow(ctx)在 fz_catch 内把当前异常原样抛给上层前提是期间没有介入其他 fz_try/fz_catchfz_morph_error(ctx, fromcode, tocode)在 catch 内修改异常类型常用于降级异常严重程度fz_caught(ctx)取得当前异常的错误码fz_caught_message(ctx)取得格式化后的消息字符串fz_caught_errno(ctx)对 SYSTEM 类错误取回 errnofz_rethrow_if/fz_rethrow_unless按错误码条件重抛fz_report_error(ctx)把异常上报到注册的错误回调example.c 中每个 catch 块都在用它fz_ignore_error(ctx)彻底吞掉一个已处理的异常。MuPDF 定义的错误类型枚举context.h包括FZ_ERROR_NONE、FZ_ERROR_GENERIC、FZ_ERROR_SYSTEM致命的内存不足或系统调用错误、FZ_ERROR_LIBRARY、FZ_ERROR_ARGUMENT参数非法/越界、FZ_ERROR_LIMIT资源或硬性限制、FZ_ERROR_UNSUPPORTED、FZ_ERROR_FORMAT不可恢复的语法/格式错误、FZ_ERROR_SYNTAX应被诊断并忽略的语法错误以及仅供内部使用的FZ_ERROR_TRYLATER、FZ_ERROR_ABORT、FZ_ERROR_REPAIRED。3.2 宏方案的三大限制基于宏的实现有 3 个主要限制官方文档逐条强调绝不要从 try 块内return也不能goto或longjmp跳出去。这会破坏宏的内部簿记在之后引发问题代码虽然能检测到这类行为但此时已来不及给出原始违规位置的可用错误报告。try/always/catch 不是一条原子 C 语句。例如下面的写法不会得到预期结果if (condition) fz_try(ctx) { ... } fz_catch(ctx) { ... }必须改写为if (condition) { fz_try(ctx) { ... } fz_catch(ctx) { ... } }宏基于 setjmp/longjmp因此 C 标准对这两个函数的一切限制同样适用于 fz_try/fz_catch。特别是任何在 fz_try 开始之后、抛异常之前被赋值的真正局部变量其值在抛异常过程中可能变成未定义。3.3 fz_var防止局部变量在长跳转中丢失为了缓解限制 3MuPDF 提供了fz_var()宏它告诉编译器确保该变量不会因抛异常而被重置。其展开为fz_var_imp((void *)(var))一个对变量的地址求值的调用从而让编译器在 longjmp 跨越栈帧时保留其值。官方文档给出的模范代码是一个盖房子的比喻完整展示了 fz_try/fz_always/fz_catch fz_var 的组合用法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) { // No bricks available, make do with straw? m make_straw(); } w make_walls(m, p); r make_roof(m, t); // Note, NOT: 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; }这段代码值得逐条解读make_tiles()在 fz_try 之前调用若它抛异常会直接由更外层的异常处理器接管若成功t在 fz_try 开始前就已赋值因此无需对 t 调用 fz_var。先尝试用砖块make_bricks作为建材失败则回退到稻草make_straw若再失败会落入 fz_catch整个流程干净地失败。假设combine对传入的 walls 和 roof 各取新引用因此无论成败w和r都必须清理——这正是 fz_always 块的职责。遵循标准 C 惯例销毁 NULL 是安全的fz_drop_* 系列均允许传 NULL。此外官方文档强调fz_always 块内绝不能调用可能抛异常的函数而 fz_catch 内若想重抛使用 fz_rethrow。SumatraPDF 的多线程渲染循环multi-threaded.c也严格遵循了这一模式fz_always里fz_drop_device与fz_drop_pagefz_catch里fz_rethrow。3.4 真实世界的错误回调除了异常宏context 还支持注册错误/警告回调fz_set_error_callback/fz_set_warning_callbackcontext.h回调会在异常处理过程中被调用但回调本身绝不能抛异常。SumatraPDF 正是这样做的——在 src/EngineMupdf.cpp 中InstallFitzErrorCallbacks将fz_print_cb同时注册为 warning 与 error 回调把 MuPDF 的警告/错误消息统一转发进 SumatraPDF 的日志系统并对找不到系统字体未知 epub 版本等可忽略信息做了过滤src/EngineMupdf.cpp。四、多线程规则、锁与两种架构选择4.1 先想清楚你真的需要多线程吗官方文档首先给出了一个务实的提醒MuPDF 可以在完全不感知线程的前提下被构建进多线程应用。如果应用在一个线程里打开文档并充当服务器为其他线程按需提供页面并渲染那么 MuPDF 始终只被这一个线程调用——对其他线程而言没有任何线程安全问题也就不需要任何锁。本节讨论的是更复杂的情形在同一个应用里从多个线程并发调用 MuPDF。4.2 五条铁律官方文档给出了确保多线程顺畅运行的 5 条规则不同线程不允许同时对同一 context 发起 MuPDF 调用。大多数时候最简单的方式就是每个线程各用一个 context——在线程创建的同时创建新 context细节见克隆 context一节。不同线程不允许同时使用同一个文档。同一时刻只能有一个线程访问文档但一旦从文档生成了 display list多个线程就可以同时操作这些 display list。文档也可以被多个线程使用前提是有防护措施保证使用不是同时的。不同线程不允许同时调用同一个 device。多线程同时调用一个 device 会使其状态错乱甚至崩溃多个线程轮流调用同一 device 是完全可以的只要有防护避免同时调用。除非 MuPDF 纯粹单线程使用否则必须在创建 context 时就提供 fz_locks_context。MuPDF 需要用用户提供的锁函数来保护对某些结构/资源/库的不安全并发访问——即使使用完全独立的 MuPDF 实例也是如此。所有在用 context 必须共享同一个 fz_locks_context或其底层锁。官方强烈建议fz_new_context只调用一次之后用fz_clone_context派生新 context这样自动保证所有实例使用同一锁机制。当前虽然仍支持多次调用fz_new_context创建完全独立的 context但这些 context必须共享同一个 fz_locks_context或依赖同一组底层锁创建不同独立 context 的能力将来可能被移除。4.3 fz_locks_context 与 FZ_LOCK_MAX调用方需要提供FZ_LOCK_MAX个互斥锁。MuPDF 调用锁结构里的 lock/unlock 函数指针时传入的是该结构里的 user 指针与锁编号i0 i FZ_LOCK_MAX。这些互斥锁既可以是递归的也可以是非递归的因为 MuPDF 只会以非递归风格调用。锁结构的定义context.htypedef struct { void *user; void (*lock)(void *user, int lock); void (*unlock)(void *user, int lock); } fz_locks_context; enum { FZ_LOCK_ALLOC 0, FZ_LOCK_FREETYPE, FZ_LOCK_GLYPHCACHE, FZ_LOCK_MAX };可以看到当前有 3 把内部锁分配锁FZ_LOCK_ALLOC、FreeType 字体引擎锁FZ_LOCK_FREETYPE、字形缓存锁FZ_LOCK_GLYPHCACHE。MuPDF 内部为避免死锁有一条简单规则绝不在已经持有锁 i0 i n时再去拿锁 n为验证这一规则还提供了调试代码可通过定义FITZ_DEBUG_LOCKING启用在 MEMENTO 或非 NDEBUG 构建下自动开启。context.h 中的fz_lock/fz_unlock内联函数展示了 MuPDF 内部加锁的统一入口先做锁调试断言再调用用户提供的ctx-locks.lock(ctx-locks.user, lock)。fz_keep_imp/fz_drop_imp等引用计数辅助函数也都在FZ_LOCK_ALLOC上做加解锁确保引用计数增减在多线程下是原子的context.h。4.4 SumatraPDF 的锁实现真实世界的样本SumatraPDF 在 src/EngineMupdf.cpp 中实现了这套回调——用EngineMupdf对象自身的fz_locks[lock]互斥锁数组来充当FZ_LOCK_MAX把锁static void fz_lock_context_cs(void* user, int lock) { EngineMupdf* e (EngineMupdf*)user; e-fz_locks[lock].Lock(); } static void fz_unlock_context_cs(void* user, int lock) { EngineMupdf* e (EngineMupdf*)user; e-fz_locks[lock].Unlock(); }并在构造函数里一次性组装src/EngineMupdf.cppfz_locks_ctx.user this; fz_locks_ctx.lock fz_lock_context_cs; fz_locks_ctx.unlock fz_unlock_context_cs; _ctx fz_new_context(nullptr, fz_locks_ctx, FZ_STORE_DEFAULT); ... fz_register_document_handlers(_ctx);这里user指针被用来携带EngineMupdf对象从而找到锁数组正是官方文档推荐用 user 指针传递锁数组、避免全局变量的实践。注册完文档处理器后SumatraPDF 还会注入 Windows 系统字体加载与内嵌字体加载器参见 src/mupdf/README.md 对mupdf_load_system_font.c、noto_sumatra.c的说明。4.5 多线程示例主线程读页、每页一个渲染线程multi-threaded.c 演示了官方推荐的第一种架构一个主线程负责从文档读取页面并生成 display list每页一个渲染线程负责把 display list 画成 pixmap。它的构建与运行方式# 源码树内构建将每页渲染为独立 PNG make examples ./build/debug/multi-threaded document.pdf # 基于已安装库编译 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示例的头部注释特别提醒所有页面会同时渲染请选页数少的文件以免过度压榨机器同时不同环境的线程数量限制也可能成为瓶颈。该示例的骨架值得拆解1初始化锁与主 contextmulti-threaded.cpthread_mutex_t mutex[FZ_LOCK_MAX]; fz_locks_context locks; // 初始化 FZ_LOCK_MAX 个非递归互斥锁 for (i 0; i FZ_LOCK_MAX; i) { if (pthread_mutex_init(mutex[i], NULL) ! 0) fail(pthread_mutex_init()); } // user 指针指向锁数组lock/unlock 函数据此定位具体锁 locks.user mutex; locks.lock lock_mutex; locks.unlock unlock_mutex; ctx fz_new_context(NULL, locks, FZ_STORE_UNLIMITED);配套的lock_mutex/unlock_mutexmulti-threaded.c就是user转回pthread_mutex_t*数组后按下标加解锁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()); }2主线程读页 生成 display listmulti-threaded.c主线程逐个fz_load_page→fz_bound_page→fz_new_display_list→fz_new_list_device→fz_run_page→fz_close_device把页面的全部绘制命令固化进 display list随后通过struct thread_data把ctx供渲染线程克隆、display list、包围盒 bbox 等传给渲染线程。代码注释明确强调页面的加载不能放在工作线程里做因为同一时刻只允许一个线程访问文档。主线程在 fz_always 里丢弃 device 与 page在 fz_catch 里 fz_rethrow。3渲染线程克隆 context 渲染 display listmulti-threaded.cvoid * renderer(void *data_) { struct thread_data *data (struct thread_data *)data_; int pagenumber contenteditable="false">【免费下载链接】sumatrapdfSumatraPDF reader项目地址: https://gitcode.com/gh_mirrors/su/sumatrapdf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表