PDFium Component 在 Delphi 里通过两个 TPdf 方法控制 PDF 可选内容图层(OCG):InspectOptionalContent 列出每个图层以及 PDFium 实际会渲染的可见性,SaveAsOptionalContentConfigured 则写出一份验证过的副本,你选中的图层在其中被打开或关掉。第二个方法还会把 Usage 和 /AS 规则一并中和掉,否则它们会悄悄撤销你的修改。两个方法都作用于 TPdf 里已经打开的文档,所以不需要第二个解析器去和视图显示的东西保持同步
这个需求通常来自 CAD 或 GIS 行业:图纸集把尺寸标注、注释和标题栏放在不同图层上,客户要在发给供应商之前拿一份隐藏尺寸的副本。PDFium 渲染可选内容是正确的,但它的公开 ABI 没有枚举 OCG、挑配置或翻转图层状态的函数。于是你下沉到对象层,改 /OCProperties,保存,重新加载,图层还在那儿。原因在 PDFium 的可见性逻辑,动任何字节之前值得先搞懂它
为什么改 /ON 和 /OFF 改不动 PDFium 渲染的东西?
光改配置字典的 /ON 和 /OFF 数组不够,因为 PDFium 让 OCG 自己 /Usage 字典里的显式状态压过这两个数组,而一条 /AS 自动状态规则又能把两者都覆盖掉。ISO 32000-1 §8.11.4 把配置和 usage 字典描述成两套独立机制;PDFium 的渲染器把它们折成一个决定,InspectOptionalContent 按这个顺序复现它:
- 从配置的
/BaseState出发,/ON和/Unchanged都算可见,只有/OFF隐藏 - 套用配置的
/ON数组,再套/OFF数组,所以同时出现在两个数组里的组最终隐藏 - 对请求的 usage 套用组的显式 Usage 状态,比如
/Usage << /View << /ViewState /OFF >> >>,它压过上面的一切 - 把
/Intent里既无/View也无/All的组当作可见,因为它不参与 view intent 的可见性 - 最后跑所选配置的
/AS数组,其中匹配事件的条目会设置它们所列组的状态
第三步是最烧人的。排版工具存出的文件常常每个 OCG 都带着 /ViewState /ON,PDFium 于是无视你精心编辑的 /OFF 数组:保存成功,文件重新打开也没问题,图层照画。对 Print 和 Export,OcExplicitUsageState 先读 PrintState 或 ExportState,特定条目缺席时退回 ViewState,所以单独一个 ViewState /ON 会把图层在打印时也钉死。引用 OCMD(§8.11.2.2)的标记内容随后按这些逐组结果解析,走 /P 策略,有 /VE 可见性表达式时则走表达式
怎么列出 PDFium 实际会显示的图层?
TPdf.InspectOptionalContent 返回一个 TPdfOptionalContentInventory,其 Groups 数组带每个 OCG 的对象号、名称、intent、三个 Usage 状态、语言、缩放范围、Locked 标志、radio-group 索引以及算出来的 EffectiveVisible。方法先让 PDFium 把当前内存中的文档保存一遍,展开对象流,再扫描结果,所以会话早前的编辑都会反映出来。配置索引 0 永远是默认的 /D 字典,/Configs 的条目从索引 1 排起;默认参数 -1 选索引 0。没有 /OCProperties 的文档让方法返回 False、原因放在 ErrorMessage 里,而不是抛异常
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
G: TPdfOptionalContentGroup;
begin
// Usage 默认 ocuView;-1 选配置 0,即 /D 字典
if not Pdf.InspectOptionalContent(Inv) then
begin
Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
Exit;
end;
Memo1.Lines.Add(Format('Configuration %d: %s',
[Inv.SelectedConfigurationIndex,
string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
Memberships 数组报告每个 OCMD 及其 Policy(ocmpAnyOn、ocmpAllOn、ocmpAnyOff、ocmpAllOff)、原始 VisibilityExpression 文本和它自己的 EffectiveVisible。几条边界规则是刻意的。/P 默认 /AnyOn,没有组的 OCMD 算作可见。引用一个不是已知 OCG 的对象号时按可见处理,而不让整个表达式失败。/VE 求值在嵌套深度 32 处停止,更深的一律当隐藏,免得一个恶意或自引用的表达式把检查变成栈溢出
用 SaveAsOptionalContentConfigured 写入新的图层状态
TPdf.SaveAsOptionalContentConfigured 接受一个 TPdfOptionalContentStateChange 记录数组(组对象号加 Visible),写出一份文档,其中所选配置恰好产出你要的状态。所选配置得到 /BaseState /ON 加覆盖每个组的完整 /ON 与 /OFF 数组,而每个已有 Usage 字典的 OCG 会收到一个与新状态匹配的显式 ViewState(或按 Options.Usage 给 PrintState / ExportState)。用 TPdfOptionalContentConfigureOptions.Default 时,所选配置的 /AS 键会被移除,这样 open、print 或 export 事件就没法把图层翻回去
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
SetLength(Changes, 2);
Changes[0].GroupObjectNumber := DimensionsObj;
Changes[0].Visible := False;
Changes[1].GroupObjectNumber := NotesObj;
Changes[1].Visible := True;
// 配置 0、ocuView、DisableAutomaticState 与 EnforceRadioGroups 为 True
Options := TPdfOptionalContentConfigureOptions.Default;
if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
Changes, Options, Report) then
raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);
Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
[Report.ChangedGroupCount, Report.GroupCount,
Report.UpdatedUsageStateCount,
BoolToStr(Report.RemovedAutomaticState, True)]));
end;
写入路径把 PDFium 自己保存的输出逐字节当作前缀,只追加重写过的配置宿主对象和带 Usage 字典的 OCG 对象,后面跟新的 xref 节和 trailer。在任何一个字节到达目的地之前,结果会先在一个独立的 TPdf 里按严格加载策略重新打开,交叉引用表校验不过方法就失败。文件重载还多走一步:先写到目标旁边的临时文件,验证成功后才替换目标,所以被拒的更新绝不会留下半张写坏的图。这正是 PDFium Component 的 PDF 名称树与编号树编辑器用的同一套验证式增量修订
配置化保存拒绝做什么?
配置化保存拒绝文档本身禁止或无法安全表达的任何变更,而且每次拒绝都发生在目的地被碰之前。不在 /OCGs 里的对象号直接失败。改配置 /Locked 数组里列出的组会失败,尽管重申其当前值是允许的。开着 EnforceRadioGroups 时,任何最终会多于一个可见成员的 /RBGroups 集合都会被拒,而不是悄悄把其他成员关掉。加密文档被拒,因为明文增量对象承载不了当前的安全处理器。签名文档会抛 EPdfError,除非你传 AllowSignedDocument = True——改变页面显示的内容可能破坏签名覆盖或认证策略
function TFormMain.SavePrintPreset(Target: TStream;
const Changes: TPdfOptionalContentStateChanges): Boolean;
var
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
Options := TPdfOptionalContentConfigureOptions.Default;
Options.Usage := ocuPrint; // 写出 /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // /Configs 的第一个条目,不是 /D
try
Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
Report); // AllowSignedDocument 保持 False
if not Result then
ShowMessage(Report.ErrorMessage);
except
on E: EPdfError do
begin
ShowMessage(E.Message); // 签名文件:Target 里什么都没写
Result := False;
end;
end;
end;
接进批处理作业之前先认清取舍。追加的修订坐在 PDFium 完整再保存之上,而不是你原始的文件字节上——这正是签名输入需要显式同意的原因。重写还会把所选配置归一化成 /BaseState /ON,作者的 /Unchanged 或 /OFF 基线被替换成结果可见性相同的显式数组。去掉 /AS 会移除只在纸上出现的水印图层这类 print 专属戏法;把 DisableAutomaticState 设成 False 可以保留这些规则,代价是接受它们可能在对应事件上覆盖你要的状态。好的一面是,PDF/A-2(ISO 19005-2 第 6.9 条)和 PDF/UA(ISO 14289-1 第 7.10 条)都禁止配置字典里的 /AS,所以默认输出顺手消掉了 PDFium Component 的 PDF/A preflight 校验本来会报的一个问题
图层控制在 Delphi PDF 查看器里的位置
在查看器里,图层控制是一份由 inventory 驱动、配合保存结果重载的清单。用 Groups 填充清单,禁用 Locked 的条目,共享 RadioGroupIndex 的成员按互斥处理,应用时写进一个 TMemoryStream 再把流加载回 TPdf,让视图画出新状态。TPdf 与 TPdfView 之间的接线见用 Delphi 的 PDFium VCL 打造功能完整的 PDF 查看器。授权、试用下载和其余功能集见 PDFium Component for Delphi 产品页面