- 图形学
- 图像处理
【免费下载链接】mupdf
mupdf mirror
在 MuPDF 的 Story(HTML 排版布局)体系中,DOM类型是 JavaScript 绑定(mutool runshell)下访问和操作 Story 内部 HTML/DOM 文档树的唯一入口:它不提供构造函数,而是由Story.prototype.document()返回,随后即可通过 20 余个实例方法完成查找节点、克隆模板、增删子节点、读写属性等操作,从而在不修改原始 HTML 字符串的前提下动态生成最终排版内容。读完后,你将掌握:DOM的获取方式与生命周期约束、全部实例方法的参数语义与底层fz_dom_*C API 实现细节,以及如何用"模板 +clone()+remove()"的经典套路编写可运行的mutool run排版脚本(参考示例 docs/examples/storytest.js)。
1. DOM 在 Story 工作流中的位置
官方文档 docs/reference/javascript/types/DOM.rst 对DOM的定义是:
This represents an HTML or an DOM node. It is a helper class intended to access the DOM (Document Object Model) content of a
Storyobject.
三个要点:
无构造函数。文档中
.. class:: DOM后标注了|no_new|,即不能new DOM();实例只能通过 Story.prototype.document() 获得。|only_mutool|标记。整个DOM类只标注了|only_mutool|(类头部及getText()、getTag()上重复标注),意味着该类型只在mutool run脚本环境中可用。运行方式即:mutool run script.js [ arguments ... ]详见 docs/tools/mutool-run.md。
底层即
fz_xml树。从源码结构看,JS 层每个DOM实例对应 C 层的fz_xml *节点。在 source/tools/murun.c 中,DOM.*方法被注册到名为fz_xml的 Userdata 类型下;每个ffi_DOM_*绑定函数(source/tools/murun.c)都只是取出fz_xml *后转调 source/fitz/xml-dom.c 中声明于 include/mupdf/fitz/xml.h 的fz_dom_*函数,并用fz_try/fz_catch捕获 C 层异常后以rethrow(J)转成 JS 异常。因此 C API 的注释可以直接作为 JS 方法语义的权威说明。
同一套 DOM 接口在其他语言绑定中也有对应物,例如 Java 平台类 platform/java/src/com/artifex/mupdf/fitz/DOM.java 及其 JNI 实现 platform/java/jni/dom.c,方法名与 JS 版基本一一对应,可用于交叉参考。
关键生命周期约束:DOM 只在首次排版前有效
这是使用DOM前必须理解的底层约束。C 头文件 include/mupdf/fitz/story.h 对fz_story_document的注释写明:
fz_story_document返回的是borrowed reference(借用引用),由 Story 持有并负责销毁,"Do not destroy this reference";- "This only makes sensebefore the first placementof the story or retrieval of the warnings. Once either of those things happen the DOM representation is destroyed."
也就是说:调用story.place(rect)或获取排版警告之后,DOM 树即被销毁,此前从story.document()取到的任何DOM引用都不再有效。所以正确的脚本结构是"先完成全部 DOM 操作,再进入place()/draw()循环",docs/examples/storytest.js 的示例正是按这个顺序编写的。
2. 实例方法完整参考
以下覆盖 DOM.rst 中列出的全部 22 个方法。除特别说明外,方法中的this就是被操作的节点。
2.1 获取文档骨架
| 方法 | 签名 | 返回值 | 说明 |
|---|---|---|---|
body() | dom.body() | DOM | 返回body元素的 DOM |
documentElement() | dom.documentElement() | DOM | 返回顶层(最外层)元素 |
createElement(tag) | dom.createElement("div") | DOM | 创建元素,尚未挂入 DOM |
createTextNode(text) | dom.createTextNode("text") | DOM | 创建文本节点,尚未挂入 DOM |
body()在 C 层的实现非常直接,就是对整棵树做一次深度优先查找(source/fitz/xml-dom.c):
fz_xml *fz_dom_body(fz_context *ctx, fz_xml *dom) { if (dom == NULL) return NULL; return fz_xml_find_dfs(dom, "body", NULL, NULL); }documentElement()则是沿up指针走到文档根,再返回其首个子节点(source/fitz/xml-dom.c):
while (dom->up) dom = dom->up; return dom->down;两个工厂方法createElement/createTextNode分别转调fz_new_dom_node与fz_new_dom_text_node(source/fitz/xml-dom.c):节点内存从文档自己的内存池(dom->u.doc.pool)分配,down/next/prev均为空——即"游离节点"状态。注意两点:
- 元素节点会自动跳过命名空间前缀:
fz_new_dom_node中若 tag 含:,只取冒号之后的部分作为标签名; - 文本节点的内容被逐字节拷入节点自身的池内存(
u.node.u.text),down被置为MAGIC_TEXT标记,表示这是叶子文本节点。
由于这些节点尚未链接,创建后必须用第 2.3 节的插入方法挂到树上,否则不会出现在排版结果中。
2.2 查找与遍历
| 方法 | 签名 | 返回值 | 说明 |
|---|---|---|---|
find(tag, attribute, value) | dom.find("tag", "att", "val") | DOM|null | 从当前节点起深度优先查找第一个匹配元素 |
findNext(tag, attribute, value) | dom.findNext(...) | DOM|null | 查找"下一个"匹配元素 |
firstChild() | dom.firstChild() | DOM|null | 第一个子节点,无则null |
parent() | dom.parent() | DOM|null | 父节点,无则null |
next() | dom.next() | DOM|null | 下一个兄弟元素 |
previous() | dom.previous() | DOM|null | 上一个兄弟元素 |
find/findNext的三个参数均可传null表示"任意",但语义并非简单的"通配"。C 层声明 include/mupdf/fitz/xml.h 给出精确规则:
tag == NULL:任意标签名都匹配;att == NULL:不检查属性,所有标签都匹配;att != NULL且match == NULL:只有带有该属性的节点才匹配(不比较值);att != NULL且match != NULL:节点必须拥有该属性且值等于match。
实现上find转调fz_xml_find_dfs、findNext转调fz_xml_find_next_dfs(source/fitz/xml-dom.c),即深度优先遍历:先深入子树,再回溯找后续兄弟。这也解释了 storytest.js 中按id查找模板占位符的写法——id值唯一,深度优先找到的第一个命中就是目标:
var templat = body.find(null, "id", "filmtemplate"); ... var tmp = film.find(null, "id", "filmtitle");firstChild有一个从源码可确认的细节:文本节点没有子节点,fz_dom_first_child遇到文本节点直接返回NULL(source/fitz/xml-dom.c);parent对文档根的首个元素(up指向 document 指针而非普通节点)同样返回null。
2.3 插入、移除与克隆
| 方法 | 签名(this= 父节点/参照节点) | 说明 |
|---|---|---|
appendChild(childDom) | parent.appendChild(child) | 把child作为最后一个子节点挂到this下 |
insertBefore(elementDom) | ref.insertBefore(elt) | 在参照节点this之前插入elt |
insertAfter(elementDom) | ref.insertAfter(elt) | 在参照节点this之后插入elt |
remove() | elt.remove() | 把节点从 DOM 上摘下,不释放,可再挂到别处 |
clone() | elt.clone() | 深克隆节点(含全部子孙与属性),克隆体未挂入 DOM |
这三个插入方法的行为规则在 include/mupdf/fitz/xml.h 注释中明确:"unlinking the child from its current position if required",即插入即移动:如果节点已经在树中,会先从原位置摘出再插入新位置,无需手动先remove()。
以fz_dom_append_child(source/fitz/xml-dom.c)为例,实现分三步:
- 同文档校验:
check_same_doc确认父子节点属于同一棵文档树,否则抛出FZ_ERROR_ARGUMENT:"Parent and child must be from the same document"; - 环检测:沿
parent向上回溯,若遇到child本身则抛出 "Can't add a parent to its child."——不允许把父节点挂到自己的后代下;insertBefore/insertAfter也有对应检查("Can't add a node before/after its child.",source/fitz/xml-dom.c); - 先摘再插:先把
child的双向兄弟链(prev/next)断开,再找到parent->down链的末端把child接上,最后child->up = parent。
remove()只摘链不释放(source/fitz/xml-dom.c):断开prev/next后把up指回文档根指针,节点继续由文档内存池持有,之后可以随时appendChild到别处——这正是官方文档所说的 "The element can be added back elsewhere if required"。
clone()调用深拷贝函数clone_xml(source/fitz/xml-dom.c):文本节点直接复制内容;元素节点则逐条复制属性名/属性值串,再递归复制整条子节点链。克隆体与原文档共用同一个内存池,但完全未链接,需要显式插入。
2.4 属性与内容读取
| 方法 | 签名 | 返回值 | 说明 |
|---|---|---|---|
addAttribute(attribute, value) | elt.addAttribute("id", "x") | DOM | 添加/设置属性,返回更新后的元素 |
removeAttribute(attribute) | elt.removeAttribute("id") | DOM | 删除属性,返回更新后的元素 |
getAttribute(attribute) | 运行时绑定名attribute | string|null | 读单个属性值,无则null |
getAttributes() | elt.getAttributes() | Object | 返回{属性名: 属性值}字典对象 |
getText() | elt.getText() | string|null | 节点文本内容(|only_mutool|) |
getTag() | elt.getTag() | string|null | 节点标签名(|only_mutool|) |
一个值得注意的命名细节:文档按getAttribute命名,但 source/tools/murun.c 的注册名是"DOM.attribute",文档自身示例也是xml.attribute("attribute")——在mutool run中实际调用名是attribute。
getAttributes()的实现体现了 C 层"按序号枚举属性"的接口形态(include/mupdf/fitz/xml.h):fz_dom_get_attribute(ctx, elt, i, &att)以i = 0, 1, 2, ...逐个取,取完返回NULL。JS 绑定侧就是一个 while 循环,把每次取到的att作为属性名、返回值作为属性值塞进新建的 JS 对象(source/tools/murun.c)。
3. 实战:三种内容构建方式(源自官方示例)
docs/examples/storytest.js 展示了用DOM构建 Story 内容的三种模式,均以"页面循环放置 + 绘制"收尾。先给出共用的排版循环骨架(第 2、3 种模式共用,docs/examples/storytest.js):
var mediabox = [0, 0, 512, 640]; var margin = 10; var placed; do { var where = [ mediabox[0] + margin, mediabox[1] + margin, mediabox[2] - margin, mediabox[3] - margin ]; var dev = writer.beginPage(mediabox); placed = story.place(where); story.draw(dev, Matrix.identity); writer.endPage(); } while (placed.more); writer.close();3.1 纯编程式构建:document()+body()+ 插入
不写任何 HTML 字符串,直接拿 Story 的内部空文档手工搭树(docs/examples/storytest.js):
var writer = new DocumentWriter("out2.pdf", "PDF", ""); var story = new Story(null, "", 11); // 注意:buffer 传 null,由 DOM 构建内容 var dom = story.document(); // 取得 DOM(无构造函数) var body = dom.body(); body.appendChild(dom.createTextNode("This is some text.")); var tmp = dom.createElement("b"); // 创建元素(尚未挂入) body.appendChild(tmp); // 挂入 body tmp.appendChild(dom.createTextNode("This is some bold text.")); body.appendChild(dom.createTextNode("This is some normal text."));要点:createElement与appendChild配合,"创建 → 挂接"是固定两步;所有 DOM 操作必须在第一个story.place(...)之前完成(见第 1 节生命周期约束)。
3.2 模板 + 克隆:find/clone/remove组合套路
这是DOMAPI 最有实战价值的用法:HTML 模板中放一个带id的占位节点,运行时find出来、循环clone填充数据、最后remove模板本体(docs/examples/storytest.js)。
模板(含占位li#filmtemplate,其内有#filmtitle、#director、#filmyear、#cast四个空占位符):
var 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>";填充与清理(数据为films数组,字段title/director/year/cast):
var dom = story.document(); var body = dom.body(); var templat = body.find(null, "id", "filmtemplate"); // 按 id 定位模板 for (var f = 0; f < films.length; f++) { var film = templat.clone(); // 深克隆,未挂入 DOM var tmp = film.find(null, "id", "filmtitle"); // 在克隆体内部再查找 tmp.appendChild(dom.createTextNode(films[f].title)); tmp = film.find(null, "id", "director"); tmp.appendChild(dom.createTextNode(films[f].director)); tmp = film.find(null, "id", "filmyear"); tmp.appendChild(dom.createTextNode(films[f].year)); tmp = film.find(null, "id", "cast"); for (var j = 0; j < films[f].cast.length; j++) { tmp.appendChild(dom.createTextNode(films[f].cast[j])); tmp.appendChild(dom.createElement("br")); } templat.parent().appendChild(film); // 挂到模板同级 } templat.remove(); // 移除模板本体这个套路里有几个值得点名的行为,全部可由源码印证:
- 在克隆体上
find是安全的:clone()返回的树与原树结构相同,find的深度优先搜索在克隆子树内独立进行; templat.parent().appendChild(film)利用了"插入即移动/追加到末尾"的语义,克隆体每次追加为最后一个兄弟;remove()不产生悬空风险:被摘下的模板节点仍由文档池持有,只是不再参与排版;- 若想在插入时精确定位而非追加到末尾,可改用
insertBefore/insertAfter配合兄弟查找。
3.3 只读访问场景
getAttribute/getAttributes/getText/getTag/ 遍历方法适合在生成前检查或改写已解析 HTML。例如枚举某节点全部属性:
var p = body.find(null, "id", "a"); var dict = p.getAttributes(); // => { id: "a" } console.log(p.getTag(), p.getAttribute("id"));对应的 C 侧函数是fz_dom_attribute(单属性,include/mupdf/fitz/xml.h,注释:返回借用指针或 NULL)与fz_dom_get_attribute(序号枚举)。
4. 底层实现速览(源码级补充)
把 JS 方法映射到源码,整体调用链为:
JS: dom.find(...) -> ffi_DOM_find (source/tools/murun.c) -> fz_dom_find (source/fitz/xml-dom.c) -> fz_xml_find_dfs (source/fitz/xml.c 的 XML 通用遍历)从源码结构看,几个对使用者重要的设计事实:
- 内存全部归文档池。
fz_new_dom_node/fz_new_dom_text_node(source/fitz/xml-dom.c)都在dom->u.doc.pool上分配,JS 层无需(也无法)单独释放节点;这也是remove()只做解链的原因——头文件注释明确 "No reference counting changes for the element"(include/mupdf/fitz/xml.h)。 - 节点即双向链表。每个节点持有
down(首子)、up(父)、u.node.next/prev(兄弟双向链);insert*/remove本质都是对这三组指针的摘挂操作。 - 错误以异常形式上抛。同文档校验失败、成环操作等直接
fz_throw(ctx, FZ_ERROR_ARGUMENT, ...),经ffi_*的fz_catch/rethrow(J)变成 JS 运行时异常,可在脚本中try/catch。 - document 指针是特殊根。
fz_new_dom创建的 document item 不直接返回,而是返回其down(source/fitz/xml-dom.c);各 DOM 操作内部用skip_doc_pointer/doc_pointer处理这一层间接,用户代码无需关心。
C 语言侧可直接调用同一批fz_dom_*函数,C 版完整示例(fz_story_document+ DOM 操作,位置见 docs/examples/storytest.c 与 docs/examples/storytest.c)可与 JS 示例对照阅读。
5. 使用限制与常见陷阱
- 时机:
DOM引用仅在首次story.place(...)之前有效(include/mupdf/fitz/story.h)。先排版后操作 DOM 属于未定义行为(底层树已销毁)。 - 环境:
DOM是mutool run专属(|only_mutool|),不要在其他 JS 宿主中假定其存在;getText()、getTag()同样标注了该限制。 - 游离节点:
createElement/createTextNode/clone的返回体都不在树中,忘记appendChild是"内容凭空消失"的最常见原因。 - 单属性读取的运行时名是
attribute而非getAttribute(绑定注册见 source/tools/murun.c)。 - 跨文档移动会抛错:把 A 文档的节点插到 B 文档的树上会触发 "Parent and child must be from the same document";把节点插到自己的祖先上会触发成环错误。
- 文档原稿中的小笔误:
DOM.rst中createTextNode的示例误写为xml.createElement("Hello world!"),实际应调用createTextNode,本文已按正确用法给出。
6. 参考文件清单
| 内容 | 路径 |
|---|---|
| DOM 类型官方参考(本文主体) | docs/reference/javascript/types/DOM.rst |
Story 类型参考(document()出处) | docs/reference/javascript/types/Story.rst |
mutool run使用说明 | docs/tools/mutool-run.md |
| JS 实战示例(本文示例来源) | docs/examples/storytest.js |
| C 语言对照示例 | docs/examples/storytest.c |
fz_dom_*C API 声明 | include/mupdf/fitz/xml.h |
| DOM 操作实现 | source/fitz/xml-dom.c |
fz_story_document与 DOM 生命周期注释 | include/mupdf/fitz/story.h |
| JS 绑定函数与注册 | source/tools/murun.c、source/tools/murun.c |
| Java 平台同构绑定 | platform/java/src/com/artifex/mupdf/fitz/DOM.java |
- 图形学
- 图像处理
【免费下载链接】mupdf
mupdf mirror
相关推荐
MuPDF 引擎中的 Story DOM 编程接口:基于 mutool run 的 HTML DOM 操作与程序化排版实战
MuPDF 引擎中的 Story DOM 编程接口:基于 mutool run 的 HTML DOM 操作与程序化排版实战 导读 本文聚焦 MuPDF 引擎为
桌面应用文档MuPDF JavaScript 编程指南:从 mutool run 到 mupdf.js 的完整实战
MuPDF JavaScript 编程指南:从 mutool run 到 mupdf.js 的完整实战 MuPDF 为 JavaScript 开发者提供了两条几
图形学图像处理MuPDF 在 JavaScript 中的使用指南:MuPDF.js(WebAssembly)与 mutool run 详解
MuPDF 在 JavaScript 中的使用指南:MuPDF.js(WebAssembly)与 mutool run 详解 MuPDF 是一套轻量级、高性能的
图形学图像处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考