Pandoc RTF 读取器图片解析实战:从\shp\pict内嵌图像到 Pandoc Image 节点的完整链路
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
本文以 pandoc 仓库中的命令测试用例 test/command/10145.md 为切入点,完整拆解 Pandoc 的 RTF 读取器(Text.Pandoc.Readers.RTF)如何解析 RTF 文档中通过 Shape 对象(\shp\shpinst)包裹、以\pict\jpegblip内嵌的 JPEG 图片,并输出为带尺寸属性的 PandocImage节点。读完本文,你将理解 RTF 图片的十六进制编码方式、twips 尺寸单位到英寸的换算规则、图片文件的 SHA-1 命名机制,以及 Pandoc 命令测试(Command Test)的编写与运行方法。
测试用例速览:一个内嵌 JPEG 图片的 RTF 文档
test/command/10145.md是 pandoc 的 golden test(金标准测试)之一:它给定一段 RTF 输入,要求pandoc的 RTF 读取器将其转换为 Pandoc 的 native(内部 AST 文本表示)格式,并与预期的 native 输出逐字符比对。
% pandoc -f rtf -t native {\rtf1\ansi\deff0{\fonttbl{\f0 \fswiss Helvetica;}{\f1 \fmodern Courier;}} ... ^D [ Para [ Image ( "" , [] , [ ( "width" , "0.2777777777777778in" ) , ( "height" , "0.3055555555555556in" ) ] ) [ Str "image" ] ( "9ea9761885249909bcd8a610706eef51b54dc351.jpg" , "" ) ] , Para [ Str "moon" ] ]这个用例虽然只有 24 行,却覆盖了 RTF 图片解析的几个关键难点:Shape 包装结构、图片二进制数据的十六进制表示、图片显示尺寸的换算、以及图片资源的持久化命名。下面逐层展开。
RTF 输入解剖:Shape 与 Pict 的嵌套结构
RTF 中内嵌图片有两种常见形式:直接使用{\pict ...}图片对象,或使用文本框 / 形状包装的{\shp ...}结构。本用例属于后者,其核心片段为:
{\pard \ql \f0 \sa0 \li0 \fi0 {\shp{\*\shpinst\shpwr2\shpwrk3\shpbypara\shpbyignore \shptop1320\shpbottom7745\shpbxcolumn\shpbxignore\shpleft0\shpright9638 {\sp{\sn shapeType}{\sv 75}} {\sp{\sn wzDescription}{\sv }} {\sp{\sn wzName}{\sv }} {\sp{\sn pib}{\sv {\pict\jpegblip\picw20\pich22\picwgoal400\pichgoal440 ffd8ffe000104a464946...ffd9}}}}} \par} {\pard \ql \f0 \sa0 \li0 \fi0 moon\par}各要素的作用如下:
| RTF 控制字 / 组 | 含义 | 在本用例中的值 |
|---|---|---|
{\shp ...} | Shape 对象组,包裹形状及其属性 | 外层容器 |
\shpinst | Shape 实例说明(\*表示可选目的地) | 形状实例 |
\shpwr2 | 文字环绕方式(环绕) | 环绕形状 |
\shptop1320/\shpbottom7745 | 形状在页面中的垂直位置(twips) | 1320 / 7745 |
\shpleft0/\shpright9638 | 形状在页面中的水平位置(twips) | 0 / 9638 |
{\sp{\sn pib}{\sv ...}} | 形状属性对:\sn为属性名,\sv为属性值;pib表示“图片二进制数据” | 图片数据载体 |
{\pict\jpegblip ...} | 图片对象;jpegblip声明图片类型为 JPEG | 内嵌 JPEG |
\picw20/\pich22 | 图片原始像素宽高 | 20 × 22 px |
\picwgoal400/\pichgoal440 | 图片显示尺寸(twips,1 英寸 = 1440 twips) | 400 × 440 twips |
ffd8ffe0...ffd9 | JPEG 文件的十六进制字节流(以ffd8起始、ffd9结尾) | 图片数据 |
其中{\sp{\sn pib}{\sv {\pict...}}}是理解整个用例的关键:\sn pib声明该属性是“图片二进制数据”,\sv的值是一个\pict对象。Pandoc 读取器只有在pib属性下才真正提取图片内容。
期望输出解读:Pandoc Image 节点
转换得到的 native AST 是一个包含两个块(Block)的文档:
[ Para [ Image ( "" -- 元素 id 为空 , [] -- 无 class , [ ( "width" , "0.2777777777777778in" ) -- 宽 400/1440 英寸 , ( "height" , "0.3055555555555556in" ) -- 高 440/1440 英寸 ] ) [ Str "image" ] -- 替代文本(alt) ( "9ea9761885249909bcd8a610706eef51b54dc351.jpg" , "" ) -- src 与 title ] , Para [ Str "moon" ] -- 段落中的普通文本 ]值得注意的三个细节:
- 尺寸换算:
picwgoal=400twips 除以 1440(1 英寸的 twips 数)得到0.2777777777777778in,pichgoal=440得到0.3055555555555556in。这个换算关系与源码中fromIntegral w / 1440的计算完全一致。 - alt 文本:图片的替代文本被固定为字符串
image,因此输出为[Str "image"]。 - 资源命名:图片被存储为以 SHA-1 哈希命名、
.jpg扩展名的媒体文件。用例中的文件名9ea9761885249909bcd8a610706eef51b54dc351.jpg正是内嵌 JPEG 字节流的 SHA-1 摘要。
源码实现原理:RTF 读取器的图片处理链路
入口与注册
RTF 读取器在 src/Text/Pandoc/Readers/RTF.hs 中实现,其公开接口为readRTF :: ReaderOptions -> a -> m Pandoc(第 48-57 行)。该读取器在 src/Text/Pandoc/Readers.hs 中注册为("rtf", TextReader readRTF),即通过pandoc -f rtf即可调用。
读取过程分为两步:
- 词法分析(Tokenizer):将 RTF 文本切分为 token 流,token 类型包括控制字(
ControlWord,如\pict)、控制符号(ControlSymbol,如\{)、十六进制字节(HexVals,对应\'xx)、未格式化文本(UnformattedText)和分组(Grouped)。 - 语义处理(processTok):对 token 流逐个处理,维护一个属性栈(
Properties)与文档状态(RTFState),最终产出Blocks。
\shp与\sp组的分流
在processTok中,与形状相关的 token 有专门分支:
{\shp ...}组(第 481-482 行):Grouped (Tok _ (ControlWord "shp" _) : toks)直接进入组内继续处理;- 属性对
{\sp{\sn 名字}{\sv 值}}(第 483-490 行):当\sn的值为pib时,对\sv组内的 token(即{\pict...})继续处理,其它属性名(如shapeType、wzDescription、wzName)则被忽略; - 遇到
{\pict ...}组(第 496-497 行)时,调用handlePict完成图片数据收集。
也就是说,读取器只在pib属性中发现图片,形状的位置、环绕等布局信息(\shptop、\shpwr2等)不会进入 Pandoc AST——这符合 Pandoc 面向内容而非版式的设计。
handlePict:收集图片元数据与字节流
handlePict(第 1143-1180 行)通过getPictData对\pict组内的 token 做左折叠,提取以下字段:
| RTF 控制字 | 存入字段 | 说明 |
|---|---|---|
emfblip/pngblip/jpegblip | picType | 图片类型枚举,分别对应 EMF / PNG / JPEG |
picw/pich | picWidth/picHeight | 原始像素尺寸 |
picwgoal/pichgoal | picWidthGoal/picHeightGoal | 目标显示尺寸(twips) |
\binN二进制数据 | picBytes | 二进制模式(picBinary = True) |
| 连续十六进制文本 | picData | 文本模式,每两个字符解码为一个字节 |
在本用例中,JPEG 数据以连续十六进制文本形式直接出现在\pict组内(如ffd8ffe000104a46...),因此走UnformattedText分支累积到picData,随后通过T.chunksOf 2分组、hexToWord逐对解码为字节串。
解码完成后,handlePict根据picType决定 MIME 类型与扩展名:
Just Emfblip -> (Just "image/x-emf", ".emf") Just Pngblip -> (Just "image/png", ".png") Just Jpegblip -> (Just "image/jpeg", ".jpg")随后调用insertMedia(来自Text.Pandoc.Class)将解码后的字节流存入文档媒体包,文件名由 SHA-1 哈希生成:
let pictname = show (hashWith SHA1 $ BL.toStrict bytes) <> ext insertMedia pictname (Just mt) bytes这正是期望输出中9ea9761885249909bcd8a610706eef51b54dc351.jpg的来源:对图片字节串计算 SHA-1,转成十六进制字符串后追加.jpg扩展名。媒体入库后,gImage属性被设置为该图片,同时addText "image"写入固定 alt 文本。
尺寸换算与 AST 生成
图片最终在addFormatting(第 352-384 行)中转换为 PandocImage节点。当文本带有gImage属性时,构造:
let attr = ("", [], (case picWidthGoal pict of Nothing -> [] Just w -> [("width", tshow (fromIntegral w / 1440 :: Double) <> "in")]) ++ (case picHeightGoal pict of Nothing -> [] Just h -> [("height", tshow (fromIntegral h / 1440 :: Double) <> "in")])) in B.imageWith attr (picName pict) "" . B.text可以看到:picwgoal/pichgoal除以 1440 后以英寸(in)为单位写入width/height属性;picName即 SHA-1 文件名作为图片src;alt 文本取自前面的[Str "image"]。整条链路与用例的期望输出一一对应。
命令测试框架:这种用例如何被运行
test/command/10145.md属于 pandoc 的命令测试(Command Test)。测试运行器位于 test/Tests/Command.hs,其格式约定如下(第 13-31 行):
- 代码块第一行以
%开头,之后是要执行的命令; - 后续若干行为传给命令的标准输入(stdin);
- 输入以独立一行
^D结束; - 之后的行是期望的标准输出;
- 若期望 stderr,则每行以
2>前缀;若期望非零退出码,末行以=>加状态码。
测试发现逻辑(第 83 行)会扫描test/command目录下所有.md文件并逐一解析其中的命令块;实际执行时(execTest)用test-pandoc --emulate模拟pandoc命令行,将真实输出与期望输出做 golden 比对。因此10145.md既是回归测试,也是一份“RTF 图片解析行为”的可执行文档。
运行该用例的方式(需先构建项目,当前仓库使用 cabal / stack 管理,构建与测试命令详见 INSTALL.md):
# 方式一:运行全部测试(包含 test/command 下的用例) cabal test # 方式二:只运行命令行测试组 cabal test --test-options='-p Command'若某用例失败,Tests.Command会输出期望与实际的 diff,便于定位读取器行为变化。
手动验证:把测试用例变成可复现的实验
无需修改仓库,即可用已构建的 pandoc 复现本用例:
# 将 10145.md 中 % 行之后的 RTF 内容保存为 10145.rtf pandoc -f rtf -t native 10145.rtf预期输出与test/command/10145.md中^D之后的内容一致。若改用其它输出格式,例如-t html5,图片会以data:URI 或外部媒体文件形式输出,取决于是否使用--self-contained等选项;而-t markdown则会看到image{width="0.2777777777777778in" height="0.3055555555555556in"}形式的图片语法。
此外,MANUAL.txt 中记录了 RTF 作为输入/输出格式的支持情况(第 280、364 行),以及 RTF 输出模板变量fontsize(第 3619-3622 行,如12pt),可供进一步查阅 RTF 相关的格式选项。
小结
通过test/command/10145.md这一个用例,可以完整掌握 Pandoc RTF 读取器处理内嵌图片的机制:
- 结构识别:
\shp→\sp{\sn pib}→\pict的嵌套路径,是读取器定位图片数据的入口; - 数据解码:
\pict内的十六进制文本按两字符一组解码为原始字节,\binN二进制模式同样支持; - 尺寸换算:
\picwgoal/\pichgoal(twips)除以 1440 得到英寸,写入Image节点的width/height属性; - 资源命名:图片字节流的 SHA-1 摘要作为文件名,扩展名由
jpegblip/pngblip/emfblip类型决定; - 测试机制:命令测试以
% 命令 + 输入 + ^D + 期望输出的格式沉淀行为规范,是理解 Pandoc 各读取器细节的最佳学习材料。
如果你需要调试或扩展 RTF 图片解析逻辑,核心代码集中在 src/Text/Pandoc/Readers/RTF.hs 的handlePict、addFormatting与processTok三处,配合 test/command 下的命令用例即可快速验证。
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考