技术文章

Delphi 中的 PDF Block 模板:文本、图像与 PDF 填充

PDFlibPas(losLab PDF Library)从 Delphi 填充 PDF block 模板:block 是模板页上一个有名字的矩形,类型为文本、图像或 PDF,FillTextBlock、FillImageBlock 和 FillPDFBlock 把可变数据画进去。block 以 PDFlibBlock:Text、PDFlibBlock:Image 和 PDFlibBlock:PDF 页面注解的形式存在,或位于 /PieceInfo 下的页面片段字典里

用例是那种无聊但量大的活。设计师把证书、活动票或催款信排版一次,程序把姓名、座位号、签名图像或预渲染的条码页盖进固定槽位,一盖一万份。表单字段能干一部分,但 AcroForm 字段是交互对象,有自己的外观流和自己的规则,你最后多半还是得把它压平。block 更简单:一个矩形加几个样式提示,消费一次就扔

PDFlibPas 里的 PDF block 到底是什么?

PDFlibPas 的 block 是一个页面注解,/Subtype 为 PDFlibBlock:Text、PDFlibBlock:Image 或 PDFlibBlock:PDF,/Rect 是占位区域,/Contents 字符串里装着 name;key=value;key=value。第一段是 block 名,其后全是属性列表。TPDFlib.AddBlock 写出这个注解时带 /F 4(Print)且没有外观流,所以未填充的 block 在屏幕和纸面上都不占像素

PDFlibPas 面向 Delphi 可变数据的 block 模板解剖:页面注解带 /Subtype PDFlibBlock:Text、Image 或 PDF,/Rect 占位区,/Contents 字符串含名字和 key=value 属性,/F 4 Print 且无外观流,/PieceInfo /PDFlib /Blocks 页面片段字典是第二个来源
block 按页从 1 起枚举,注解在前、页面片段块在后;数组条目命名为 Block1、Block2,/Custom 键展平为属性,Image、PDF、Page 之外的类型一律按文本处理

读取侧还有第二个来源。枚举完 block 注解后,PDFlibPas 回退到页面字典路径 /PieceInfo << /PDFlib << /Blocks ... >> >> 及其变体 /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>。页面片段字典是标准 PDF 的一部分,ISO 32000-1 §14.5 把它定义为应用存放页面私有数据的地方,外部模板制作工具产出的 block 定义通常就住在那里。/Blocks 容器可以是以 block 名为键的字典,也可以是普通数组;数组形态下 PDFlibPas 把条目命名为 Block1、Block2 等,除非 block 自带 /Name

页面片段块内部,/Subtype(或 /Type)取值 Image 映射为图像块,PDF 或 Page 映射为 PDF 块,其他一律按文本处理。除 /Type、/Subtype、/Name、/Rect 和 /Custom 之外的每个键都变成属性,嵌套 /Custom 字典的键也被展平进来,所以 /Custom << /department (Finance) >> 条目可以读作 GetBlockProperty(Page, Index, 'department')。block 索引按页从 1 起计,注解块在前,页面片段块在后

用 AddBlock 制作 block 模板

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) 向当前选中页面添加一个 block,成功返回 1。名字为空、宽或高为零或负、或 BlockType 不是 'text'、'image'、'pdf'(大小写不敏感)时返回 0。Properties 以分号接在名字后面,所以它用的是与存储字符串相同的 key=value;key=value 语法

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // 静态图案:边框、logo、固定文案 ...
    Lib.DrawText(72, 200, 'This certifies that');

    Lib.AddBlock('text', 'recipient', 72, 230, 450, 44,
      'fontname=Times-Bold;fontsize=30;fillcolor={cmyk 0 0.55 1 0.1};alignment=center');
    Lib.AddBlock('text', 'course', 72, 290, 450, 24,
      'fontname=Helvetica;fontsize=14;alignment=center;fitmethod=word');
    Lib.AddBlock('image', 'signature', 360, 600, 160, 60, '');
    Lib.AddBlock('pdf', 'seal', 72, 580, 100, 100, '');

    if Lib.GetBlockCount(1) <> 4 then
      raise Exception.Create('Template blocks missing');
    Lib.SaveToFile('certificate-template.pdf');
  finally
    Lib.Free;
  end;
end;

属性解析器刻意宽松。键匹配不区分大小写,值做首尾修剪、其余原样返回(写成 {top left} 的值带着花括号回来),缺键返回空字符串。唯一的硬限制是分隔符:值里不能有分号,因为分号结束一段

FillTextBlock 怎么应用字体、颜色与对齐?

TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) 读取 block 属性,把它们应用到绘图状态,用 FitTextBox 把文本装进 block 矩形,然后在 finally 块里恢复调用者的字体、字号、文字色和对齐。这个恢复在合并循环里很要紧:一个红色 30 pt 的标题块不能把样式漏进你在同一页上做的下一次 DrawText 调用

  • fontname(或 font):先复用文档里已按该名加载的字体,再映射 Helvetica-Bold、Times-Roman、Courier 等 Standard 14 名字,最后用该名尝试 AddTrueTypeFont
  • fontsize(或 size):传给排版器的字号,装不下时由它缩小
  • fillcolor(或 textcolor、color):#RRGGBB、{rgb r g b}、{cmyk c m y k}、{gray g}、裸的 3 或 4 个数字列表,或 black、white、red、green、blue、gray 这些名字;大于 1 的 RGB 分量按 0–255 读
  • alignment、align 和 position 一起读取:left、right、center(或 centre)和 justify 设水平对齐,top 和 bottom 设垂直位置,默认垂直居中
  • fitmethod:word 或 nofitsplit 阻止长词被拆断,scale 或 expand 允许字号放大以填满框

CMYK 路径是印刷厂在意的原因。{cmyk 0 0.55 1 0.1} 调 SetTextColorCMYK,可变文本以 DeviceCMYK 发出,在印刷机上直接命中品牌色,不用绕 RGB 一圈。FillTextBlock 在文本被绘制时返回 1;Text 为空、block 不存在或不是文本块时返回 0,且这些情况下 block 原样留在原地

PDFlibPas 在 Delphi 里 FillTextBlock 的处理顺序:block 属性 fontname、fontsize、fillcolor 和 alignment 先应用到绘图状态,CMYK 值经 SetTextColorCMYK 路由,文本用 FitTextBox 装框,最后在 finally 块里恢复调用者的字体、字号、颜色与对齐
block 样式应用一次即回滚,红色 30 pt 标题不会漏进下一次 DrawText;文本绘制成功返回 1,空文本或非文本块返回 0 且 block 原地保留

填充图像与 PDF 块,以及 Options 位

FillImageBlock(Page, BlockIndex, ImageID, Options) 绘制此前添加的图像,FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) 把另一个已加载文档的一页作为 Form XObject 导入并绘制,印章、二维码页或信头这类矢量内容得以保持全矢量。两者都保持源纵横比并把结果居中放进 block 矩形。三个 fill 函数共用同一套 Options 位:位 1(值 1)在填充成功后移除 block——删除注解或从 /Blocks 容器里摘除条目;位 2(值 2)把图像与 PDF 内容拉伸到精确矩形而不是按比例装下

function FillCertificate(const TemplateFile, SealFile, SignatureFile,
  OutFile: WideString; Values: TStrings): Boolean;
var
  Lib: TPDFlib;
  SealID, SignatureID, I: Integer;
  BlockName, BlockType: WideString;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SealFile, '') <> 1 then Exit;
    SealID := Lib.SelectedDocument;          // 真正的文档 ID
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // 模板现在是选中的文档
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // 倒着走:位 1 会移除每个 block,后面的索引随之重排
    for I := Lib.GetBlockCount(1) downto 1 do
    begin
      BlockName := Lib.GetBlockName(1, I);
      BlockType := Lib.GetBlockType(1, I);
      if BlockType = 'text' then
        Lib.FillTextBlock(1, I, Values.Values[BlockName], 1)
      else if (BlockType = 'image') and (SignatureID <> 0) then
        Lib.FillImageBlock(1, I, SignatureID, 1)
      else if BlockType = 'pdf' then
        Lib.FillPDFBlock(1, I, SealID, 1, 1);
    end;
    Result := Lib.SaveToFile(OutFile) = 1;
  finally
    Lib.Free;
  end;
end;

这个循环里有两个细节容易踩坑。循环从最高索引往下走,因为移除 block 2 会让原来的 block 3 变成 block 2;带着位 1 向上数会隔一个跳一个。FillPDFBlock 的源文档要先加载、其 ID 要在加载模板之前捕获,因为图像 ID 属于选中的文档,AddImageFromFile 必须在模板处于选中状态时运行。对每个记录都要换印章或信头的批量任务,PDFlibPas 的 Form XObject 页面拼接与可组合水印模板在整页上用的是同一套捕获再绘制机制

为什么 block 坐标会翻出来?

代码把 PDFlibPas 打交道的两套坐标系混用时,block 矩形就会翻出来。注解 /Rect 存在 PDF 用户空间里,从页面底部起量(ISO 32000-1 §12.5.2),而 TPDFlib 绘图 API 从顶部起量。AddBlock 接收基于顶部的输入,把下边缘存成 PageHeight - Top - Height;GetBlockRect 和三个 fill 函数把矩形读回来时用 PageHeight - URy 换算

实际含义是:需要槽位几何时用 GetBlockRect(Page, BlockIndex, Left, Top, Width, Height),比如画调试框或在 block 旁放说明文字;不要经通用注解调用读 /Rect 再直接喂给 DrawText。792 pt 信纸页上一个 /Rect [40 700 240 730] 的页面片段块,报出来的是 Top = 62,不是 700。它防的是那个经典 bug:所有填充值镜像着落到下边距附近,而没有任何报错

PDFlibPas 在 Delphi 里的 block 矩形坐标翻转:/Rect 值存在基于底部的 PDF 用户空间,AddBlock 接收基于顶部的输入并存 PageHeight 减 Top 减 Height,792 pt 信纸页上 /Rect 40 700 240 730 经 GetBlockRect 及其 PageHeight 减 URy 换算读回 Top 62
两套坐标系一混,所有填充值无一声报错地镜像到下边距;槽位几何要用 GetBlockRect 读,别把 /Rect 直接喂给 DrawText

值得记住的几个坑

block 代码里最有教育意义的 bug 是一个字符之差。block 注解靠对子类型做前缀测试识别,'PDFlibBlock:' 长 12 个字符。早期版本拿 Copy(Subtype, 1, 13) 去和这个 12 字符字面量比较,永远不会相等,于是 GetBlockCount 对每个模板都返回 0,每次填充都报 "no such block"。什么都没崩,什么都没记日志。教训可以推广到任何模板引擎:在测试里对一个已知模板断言 block 数量,因为静默的零匹配枚举和空页面看起来一模一样

第二个陷阱是文档 ID。TPDFlib.LoadFromFile 返回的是状态码——成功 1、失败 0——不是它刚加载的文档的 ID。把这个 1 当 SourceDocumentID 传给 FillPDFBlock 会失败,因为 1 不是合法文档 ID。加载成功后立刻调 SelectedDocument 拿真 ID;若之后才加载源文档,用 SelectDocument 切回模板

小一点的一个:FillTextBlock 传空字符串时什么都不做并保留 block。对带可选字段的邮件合并——比如缺第二个地址行——这通常正是你想要的,但未填充的注解会留在输出文件里。输出必须干净时,用 DeleteAnnotation 显式移除,或带位 1 填一个空格。所有 block 调用都会保存并恢复选中页面,所以在第 3 页上填充不会改变你下一次绘图调用的落点

block 注解的互操作边界在哪?

block 注解是私有约定,不是标准 PDF 特性。PDFlibBlock:Text 和它的兄弟们不在 ISO 32000-1 §12.5.6.1 列出的注解类型之列,阅读器把它们当未知注解。由于 Invisible 标志未置位且没有外观流,合规的阅读器不会为它们画任何东西,但有些阅读器可能仍会把它们列进评论面板,其他 PDF 工具则既不理解也不会保留其语义

  • 凡要离开你系统的输出都带位 1 填充,让最终文件只含普通页面内容
  • PDF/A 不允许 PDF 规范之外的注解类型(ISO 19005-1 §6.5.2),所以归档输出不得携带未填充的 block 注解
  • 页面片段块留在模板里更安全,因为 /PieceInfo 是标准的,消费方会忽略不属于自己的数据
  • block 不是表单字段:没有交互,文件里没有字段值,也不能导出成 FDF 或 XFDF;必须保持可编辑的数据见 PDFlibPas 的 FDF、XFDF 与 XFA 表单数据交换

如果模板必须以标准化、可被验证器检查的方式声明可变字段——比如印刷工作流——block 就不是对的工具。那是 PDF/VCR-1 的领域,PDFlibPas 的 PDF/VCR-1 模板占位清单讲了那些占位符如何住在结构树而不是注解里

FAQ

PDFlibPas 能填充其他工具创建的 block 模板吗?

能,条件是工具把 block 存在 /PieceInfo /PDFlib /Blocks 或 /PieceInfo /PDFlib /Private /Blocks 下,字典或数组形态皆可,/Rect 为四个数字。GetBlockCount 在所有 PDFlibBlock:* 注解之后清点这些块,自定义键经 GetBlockProperty 暴露

FillTextBlock 会改动调用前选好的字体吗?

不会。FillTextBlock 保存选中的字体、字号、文字色与对齐,应用 block 的 fontname、fontsize、fillcolor 和 alignment 属性,之后恢复原状态——即使装框失败也恢复

矢量 logo 或条码页怎么放进 block?

加载源 PDF,把 SelectedDocument 存成它的 ID,需要时重新选中模板文档,然后调 FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1)。页面以 Form XObject 导入,保持矢量并按纵横比装进 block,除非你加了位 2

block 模板、Form XObject 页面导入和其余可变数据工具都随 PDFlibPas,losLab PDF Library for Delphi 发布,覆盖 Delphi 与 C++Builder 项目的 PDF 创建、编辑与印刷生产