news 2026/9/18 16:33:58

Pandoc RTF 读取器图片解析实战:从 `\shp\pict` 内嵌图像到 Pandoc Image 节点的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pandoc RTF 读取器图片解析实战:从 `\shp\pict` 内嵌图像到 Pandoc Image 节点的完整链路

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 对象组,包裹形状及其属性外层容器
\shpinstShape 实例说明(\*表示可选目的地)形状实例
\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...ffd9JPEG 文件的十六进制字节流(以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" ] -- 段落中的普通文本 ]

值得注意的三个细节:

  1. 尺寸换算picwgoal=400twips 除以 1440(1 英寸的 twips 数)得到0.2777777777777778inpichgoal=440得到0.3055555555555556in。这个换算关系与源码中fromIntegral w / 1440的计算完全一致。
  2. alt 文本:图片的替代文本被固定为字符串image,因此输出为[Str "image"]
  3. 资源命名:图片被存储为以 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...})继续处理,其它属性名(如shapeTypewzDescriptionwzName)则被忽略;
  • 遇到{\pict ...}组(第 496-497 行)时,调用handlePict完成图片数据收集。

也就是说,读取器只在pib属性中发现图片,形状的位置、环绕等布局信息(\shptop\shpwr2等)不会进入 Pandoc AST——这符合 Pandoc 面向内容而非版式的设计。

handlePict:收集图片元数据与字节流

handlePict(第 1143-1180 行)通过getPictData\pict组内的 token 做左折叠,提取以下字段:

RTF 控制字存入字段说明
emfblip/pngblip/jpegblippicType图片类型枚举,分别对应 EMF / PNG / JPEG
picw/pichpicWidth/picHeight原始像素尺寸
picwgoal/pichgoalpicWidthGoal/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 行):

  1. 代码块第一行以%开头,之后是要执行的命令;
  2. 后续若干行为传给命令的标准输入(stdin);
  3. 输入以独立一行^D结束;
  4. 之后的行是期望的标准输出;
  5. 若期望 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 的handlePictaddFormattingprocessTok三处,配合 test/command 下的命令用例即可快速验证。

【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 16:24:24

Win10+CUDA环境配置:硬件-驱动-编译器协同原理与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华