XFA(XML 表单架构)已被弃用。ISO 32000-1 第 12.7 节包含它,但附带说明它已从 PDF 2.0 中删除,现代阅读器也接连放弃了它们的 XFA 引擎。但这都没能清空档案。在将近二十年的时间里,政府接收表单、保险申请和银行对账单都是用 XFA 编写的,今天这些文件仍陆续进入收件箱和文档处理管线。过去用来渲染它们的阅读器一旦停止支持,表单就会变成只剩“请在其他阅读器中打开”占位符的空白页。长久之计是将 XFA 展平(flatten),也就是把它们转换为任何阅读器都能绘制的静态 PDF 内容
展平过程中最困难的部分不是表单字段。文本框和复选框映射为 AcroForm 控件相对直截了当。真正的难点是 XFA 存储在 draw 元素中、包含在 <exData contentType="text/html"> 块里的富文本。这个块是带有内联样式的 HTML 子集,并且通常还包含锚点(anchors)。将其放置到页面上意味着必须同时重现样式化的文本和有效的超链接,而大多数实现在处理超链接这一步时通常会悄悄放弃
XFA 富文本的真实面貌
exData 主体是 XHTML 的一小片断。一个段落是一个 <p>;一串带样式的字符是一个带有其自身内联 CSS(用于控制字重、字形姿态、颜色和大小)的 <span>;而一个超链接则是一个包裹了其可见文本的 <a href="...">。单行文本可能连续包含好几个 span,每一个都带有不同的样式,并且其中一个可能是锚点。这些样式不是可以随意丢弃的装饰。作为法律警告、以红色加粗字体呈现的条款,在展平后必须保持红色加粗,否则展平后的文档会曲解原文
因此,展平引擎不能把这个块当作一个字符串来处理。它必须遍历内联结构,通过将 span 的内联 CSS 叠加在 draw 元素的基础字体之上,来解析每个序列的有效样式,然后把各个文本序列接连排列在一行中。HotPDF 将每个这样排布好的片段都构建为一个内部的 TXFARichRun 记录(record)进行管理。该记录携带该序列的文本、其解析出的样式、测量出的方框,以及,如果是锚点,其指向的 Href
将文本序列从左到右排版
定位是富文本从解析问题演变成排版问题的环节。这些序列(runs)共用一行,因此每个序列都在前一个序列结束的地方开始。没有任何标记记录这些位置;它们必须通过测量获得。引擎的内部 LayoutRichText 过程使用它随后用来进行绘制时的字体度量信息来测量每个序列,然后将该序列的水平偏移量设置为之前所有序列宽度的累计和。第一个序列从 draw 元素的起点开始,第二个序列起始于第一个序列末端的宽度处,第三个起始于前两个结合处的总宽度,以此类推穿越这一整行
这就是为什么测量字体的对齐方式如此重要。布局阶段(layout pass)测量的是字形的跨距(advances);独立的渲染阶段绘制字形。如果这两个阶段在字体上存在分歧,布局计算出的方框就不会刚好落在渲染器绘制出来的字形下方。为了保持一致,HotPDF 通过内部协助函数 RunStyleToFontSpec,将每个序列解析出来的样式映射到一个同渲染引擎自身的默认设置(Arial 10 pt)相匹配的字体定义上。这样测量出的宽度与实际绘制的内容就能对齐,并且序列算出的方框就能真切地涵盖读者看到的所有字符
// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
TRichRunInfo = record
Dx, Dy : Double; // top-left, relative to the draw-box origin
W, H : Double; // measured run box (width from the layout pass)
Text : AnsiString; // the run's visible characters
Href : AnsiString; // URI target for an <a> run, '' otherwise
end;
从锚点序列到 PDF Link 注释
在一个完成生成的 PDF 文件里,超链接并非页面内容的一部分。它是一个独立的对象:根据 ISO 32000-1 的 §12.5.6.5 小节所描述的 Link 注释。该注释有一个定义了页面上可点击矩形区域的 /Rect 参数,以及当该区域被点击时触发的动作(action)。对于外部链接,该动作是 URI action:即包含作为目标网址的 /URI 字符串的 /S /URI。底部的可见文字仅是普通的页面内容;该注释则是叠加在它之上的那个不可见的触发热区(hot zone)
展平路线准确无误地遵循了这种模型。当序列带有 Href 时,HotPDF 引擎先绘制具有样式的字,紧接着建立这个基于计算所得包围该序列外边框的 Link 注释区域。向此区域发起公共添加动作使用的页面类方法是 AddURILink,这就创建出了包含 /URI 的 /Type /Annot /Subtype /Link 的对象实例。那套边框由前边局部坐标换算成了最终绘制出的版式座标并做传入的返回值传回该函数内调用处了。而最终所实现出来的则是完全不超越该目标文案边缘的,精确锚定位置的精准链接效果
// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
LinkRect: TRect;
Annot: THPDFDictionaryObject;
begin
LinkRect := Rect(72, 690, 268, 706); // page-space hit box for the run
Annot := Pdf.CurrentPage.AddURILink(LinkRect,
'https://www.example.gov/appeal', '在线提交上诉');
end;
为何热区(hit box)必须来自于测量好的宽度
人们可能很容易去想,找这个链接的话不妨直接检索这页里边的能够看见的字的字眼所在的地方,然后直接围着找出的这些字划拉一圈就行了。但这行不通;造成这种结果的原因是底层有关如何保管已展平文本(flattened text)根本性问题。那些带有样式的文字序列是通过被嵌于文件内的内嵌缩减字体(embedded subset fonts)描绘而出的。而一个缩减字体改变了要保留下用来作为最终被画出来的其拥有的那些图形对应的原字号代号;这样文件字节串内保管的是重编过的按十六进制表示的 CID 码而不是原始内码了。因此画出来的字就不是人类眼见读出来的原文意思了也就不带有搜查原始文字价值。那么搜索那个锚点的标题内容是不可能有结果的;流里压根就不保存那一份直接明面文案信息
唯一能够用于圈选位置热区建立外接矩形方框尺寸的依据是这布局跑下来阶段提前算出备用的几何边界尺码。每一次渲染序列起始处的间隔以及跑定距离的数据均在该序列未对其中字体符号编号篡改前提早的在折行计算时获得。这就描述了文字内容将会现身出现的具体几何位置。HotPDF 所用的招式也确实正来源于布局好定格好的此等界线方框去直接提取该区域而不必依靠在里边进行什么搜索查字。就因为之前量的时候用的即是后面画的时一样的渲染字体,则不论里面缩减字体机制怎样这块空间都一定是完美咬合的。不管它怎样编排改版都不会使得排定的空间几何座标发生损坏和遗忘。保留这种算法也是唯一可行做法;也是导致试图仅仅依照通过反查文案去硬套链接方式常常失败偏离找不到点的缘由所在
如何利用您的代码驱动展平操作
对于早已经被包有 XFA 协议文件的 PDF,只需要用到 FlattenLoadedXFA。先加载文件然后唤出这过程最终存档完成整个任务。其中的 Editable 布尔类型形参将断定制约各种形态框接下来的命运去向:要是填 True 将依旧作为可填写交互性质的控件维持在内而赋予以 AcroForm 为依托的操作性质,给到 False 则是使其一切交互只具备展示而处于不可被编辑变动的被“锁死冻结的只读记录”这最后状态中。而不管是这两者之间到底是什么状况,这些具备各式排印格式连同样式修长,还有带有指向链接注释热区的全部带富有信息含量的长宽段的画定好的模块内容,通统都必将会得到生成渲染。这函数最终带回返给我们它转化之后得到输出发放到内边的所有控件数量值做为了参考
var
Pdf: THotPDF;
Emitted, i: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('xfa_appeal_form.pdf');
// True keeps fields fillable; False freezes them read-only.
Emitted := Pdf.FlattenLoadedXFA(True);
// Anything the engine could not map is reported, not raised.
for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);
Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
Writeln('Widgets emitted: ', Emitted);
finally
Pdf.Free;
end;
end;
每次唤用结束后永远去拜读翻翻那 XFAFlattenWarnings 名单。这大榜名单自被发动开始便予以重新刷新空白并且去汇总着去容纳一切那些个因为受其限从而不得被其渲染进去处理而出的东西(比如:不兼容不受拥戴不支持种属的一类、在画图上出现无法被去进行解压破解的画作乃至没有附上可使用的能排字序列跨度那样的 exData)。这一堆不怎么配合顺利的内容并无法引起阻断停止从而引发报错触发报警(exceptions),它只会不留余力通透记下它们这表示您收到的全都是顺应当妥帖内容时这里名单该就是张空单;若是有货也就是正好清楚通报向你,哪里个特定内容点需要仔细地去探勘把关复察。如果是使用 XDP 直接拿着底层字节的话也不妨去用 ApplyXFAAsAcroForm 这个同门的方法:一并承接着那一套操作处理规则也带着此警告通报清单逻辑方式操作直接怼那大一堆二进制数据里即可执行出来同一逻辑。还有一个配套的反着来玩的叫做 AddXFAPacket 法门,那就是用于如何在这你手里构建的一 PDF 里面,去植嵌入 XFA 数据进去
通过阅读器里做验收确定它效果结果的落实情况
通过使用 Acrobat 这类哪怕其它的任何最新软件在查验此平化成品之物进行翻阅去印证查对这下方列具出两样之事,便可以敲准:其一:富文档样式等都已被连同保留:粗体字就实实在在是加粗了的,带染色的照原样色彩带出;排列着顺序一顺儿呆在线性队伍间没造出超格外框或产生遮拦。其二就是,其间带上的该指向能够起效存活着:用鼠标停到它那里会于底部显影出了被设指去的网址出来:且当它被点下的时候就真能通过 URI 执行被弹跳导播出去到达。并借助软件查错内视工具确认那一票皆真为正规编制合矩于界定标准而实为货真价实的 /Link 注释且它的方框是完全包揽压实在它之上的 /Rect,它的下头就只是普普通通字画着色内容而非通过表单里边所画就出来的 XFA;只有同时存在带有样式之固静态呆版文案配合在最精准处摆稳带其准确边框属性的正式 Link 注释结合体之时才能造出真正使经平整展铺平过得文献能跳脱抛除废却那个现在所早已没有必用到 XFA 这依靠引擎的环境而得以脱胎成自由存世流芳的状态
把这些表单等那些属于用来填写或是用来挑着选之所用来勾着点着的这类,它身边伴随着的种种外貌物件平展开来化作展平之事在 我们的将 XFA 表单展平为 AcroForm 控件的攻略中被做了讲解讨论;如果在它这些引擎生成出的之外想要用手工自己去搭注释与放注释的位置之类这些更加铺开深谈这事,可以见 在 HotPDF 内关于玩转处理操作 PDF 注释的话题。一切这一切全是依托构建在同一那用于支持在 Delphi 和 C++Builder 下跑转发挥能力之 HotPDF Component 所配给而下随同相赠这大系同本同宗表单架构跟注解管理那一体系上去了