一套图纸送来,尺寸标注单独占一层,想藏起它们最直接的办法是把该组从默认配置的 /ON 数组挪进 /OFF。文件保存重开,尺寸线照样印在标题栏上。原因就在隔壁对象里——组字典内部:/Usage << /View << /ViewState /ON >> >>,渲染器读取时这条显式状态排在配置数组之前
图层状态在 PDF 里可以待在好几个地方,改 /OCProperties 只碰到其中一处。Google 的 PDFium 引擎渲染可选内容没有问题,但它的公开 C API 没有枚举可选内容组(OCG)、选择配置或开关组的功能。驱动图层的 Delphi 代码只能自己重写对象,并严格按渲染器的判定顺序重演,否则图层列表报一种状态、页面显示另一种
为什么改 /ON 和 /OFF 改不动 PDFium 渲染的东西?
/ON 和 /OFF 数组只是五步中的第二步,后面还有两步可以推翻它们。ISO 32000-1 §8.11.4.3 定义可选内容配置字典,含 /BaseState、/ON、/OFF、/Locked、/RBGroups 和 /AS;§8.11.4.4 定义 OCG 可以携带的 Usage 字典。在规范里,Usage 描述组是干什么的,/AS 里的自动状态条目把这份描述变成 ON 或 OFF。PDFium 走的路更短:为某个目的渲染时,组自己针对该目的的显式状态获胜。合在一起,顺序如下:
- 从配置的
/BaseState出发,/ON和/Unchanged都算可见,只有/OFF隐藏 - 应用
/ON数组,再应用/OFF数组,两边都列出的组最终隐藏 - 应用组针对所请求目的的显式 Usage 状态,例如
/Usage << /View << /ViewState /OFF >> >>,它覆盖以上一切 /Intent既不含/View也不含/All的组视为可见,因为它不参与视图意图的可见性- 最后执行所选配置的
/AS数组,让匹配事件的条目设定其所列组的状态
第三步解释了开头那份文件:一些导出器在每个新建组里都写上 /ViewState /ON,从此改数组怎么也藏不住这些图层。打印和导出遵循同一条规则,外加一个细节:渲染器先读 /PrintState 或 /ExportState,缺了才回退 /ViewState。所以孤零零一条 /ViewState /ON 也能把图层钉在纸上。第四步更安静:标着 /Intent /Design 的组完全置身视图意图可见性之外,无论数组如何都绘制;没有 /Intent 键的组则按 /View 处理,这是规范给的默认
用 InspectOptionalContent 列出图层及其渲染状态
PDFium Component 在 TPdf.InspectOptionalContent 里重演这条顺序。方法填一份 TPdfOptionalContentInventory,成功返回 True;Usage 参数默认 ocuView,ConfigurationIndex 默认 -1,即选索引 0。配置索引 0 是默认的 /D 字典,/Configs 的条目从索引 1 起按文件顺序排。扫描前,方法让 PDFium 把打开的文档存进内存并展开所有对象流,会话中先前的修改因此能在结果里看到。没有 /OCProperties 的文件,或超出可用范围的配置索引,让调用返回 False、原因写进 ErrorMessage;这两种情况都不抛异常
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
Cfg: TPdfOptionalContentConfiguration;
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;
Cfg := Inv.Configurations[Inv.SelectedConfigurationIndex];
Memo1.Lines.Add(Format('Configuration %d "%s", %d groups, /AS present: %s',
[Inv.SelectedConfigurationIndex, string(Cfg.Name), Length(Inv.Groups),
BoolToStr(Cfg.HasAutomaticState, True)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %-24s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
每个 TPdfOptionalContentGroup 带着对象号、名称、Intents 和三个显式状态 ViewState、PrintState、ExportState,取值为 ocsUnspecified、ocsOn 或 ocsOff。当某个状态读出 ocsOn 而组又躺在 /OFF 数组里,你就抓到了开头那段说的陷阱。Locked 和 RadioGroupIndex 来自所选配置;不属于任何 /RBGroups 集合的组,RadioGroupIndex 为 -1
Usage 字典的其余部分按文件所写如实上报:Language 与 LanguagePreferred、UserType 与 UserNames、PageElementSubtype,以及 HasZoomRange 标志背后的 MinimumZoom 与 MaximumZoom。EffectiveVisible 不依赖其中任何一项,随缩放变化的显示留给你的查看器。每个 TPdfOptionalContentConfiguration 暴露名称、BaseState、数组内容和 RadioGroups,外加把 /Order 摊平成组引用、跳过嵌套标签数组文本标签的 OrderGroupObjectNumbers
打印和导出可能与屏幕不一致
Usage 参数会改变答案,这正是它存在的意义。在 ocuPrint 下,清单读 /PrintState(带回退到 /ViewState),只有 /Event 为 /Print 的 /AS 条目参与。为只在纸上出现而建的水印层,在 ocuView 下报隐藏、在 ocuPrint 下报可见。两趟对比能在打印件惊到任何人之前找出这类图层
procedure TFormMain.ReportPrintOnlyLayers;
var
OnScreen, OnPaper: TPdfOptionalContentInventory;
I: Integer;
begin
if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
Exit;
// 两趟读同一个 /OCGs 数组,组顺序因此一致
for I := 0 to High(OnScreen.Groups) do
if OnScreen.Groups[I].EffectiveVisible <> OnPaper.Groups[I].EffectiveVisible then
Memo1.Lines.Add(Format('%s: screen=%s, print=%s',
[string(OnScreen.Groups[I].Name),
BoolToStr(OnScreen.Groups[I].EffectiveVisible, True),
BoolToStr(OnPaper.Groups[I].EffectiveVisible, True)]));
end;
OCMD 可见性表达式怎么求值?
可选内容成员字典(OCMD,ISO 32000-1 §8.11.2.2)通过 /P 策略或 /VE 可见性表达式把一段内容拴到多个组上,后者用 /And、/Or、/Not 组合。规范给 /P 默认值 /AnyOn,规定 /VE 存在时优先于 /OCGs 和 /P,并说没有组的 OCMD 不影响可见性
清单的 Memberships 数组按这些规则、对照同一趟的逐组结果求值,你请求的 usage 因此贯穿始终。每个成员报告组对象号、Policy(ocmpAnyOn、ocmpAllOn、ocmpAnyOff、ocmpAllOff,四个之外的名字报 ocmpUnknown)、原始 VisibilityExpression 文本和自己的 EffectiveVisible。没有组的 OCMD 算可见;指向未知 OCG 对象号的引用也算可见,一个悬空引用不会把周围全藏起来。/VE 求值在嵌套深度 32 处截断,更深视为隐藏,自引用表达式无法撑爆栈。扫描列出每个以间接对象存储、带 /Type /OCMD 的 OCMD,对象流里的也不例外
用 SaveAsOptionalContentConfigured 写入新的图层状态
TPdf.SaveAsOptionalContentConfigured 接受一组 TPdfOptionalContentStateChange 记录,每条带 GroupObjectNumber 和 Visible 标志。它写出一份副本,让所选配置在五步顺序下产生完全一致的状态。你没提到的组保持清单为它们算出的可见性
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer;
ForPrint: Boolean);
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;
Options := TPdfOptionalContentConfigureOptions.Default;
if ForPrint then
begin
Options.Usage := ocuPrint; // 写出 /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // 第一个 /Configs 条目,/D 保持原样
end;
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 名称树与编号树编辑器用的是同一套经校验的增量修订。要确认打印预设,重新加载输出并调 InspectOptionalContent(Inv, ocuPrint, 1);各组应报出你传入的状态
配置化保存拒绝做什么?
配置化保存拒绝任何文档不允许或无法安全承载的更改,且每次拒绝都发生在写目的地之前。检查按固定顺序执行:签名文档最先抛 EPdfError,除非传 AllowSignedDocument = True;加密文档其次被拒,因为明文增量对象带不动现役安全处理器。然后逐条检查请求的更改:不在 /OCGs 里的对象号直接失败;对列入配置 /Locked 数组的组所做的更改失败,重申其当前值则放行。最后,EnforceRadioGroups 打开时,会让可见成员超过一个的 /RBGroups 集合被拒。除签名之外的所有拒绝都返回 False,原因在 Report.ErrorMessage
其中两道闸门比规范严格要求的走得更远。§8.11.4.3 把 /Locked 描述为防查看器用户界面更改,把单选组描述为开一个、关其余。PDFium Component 对程序性更改同样适用这两条,单选组则直接拒绝而不是替你挑成员去关。要求同时改互斥集合的两个成员,库无从知道你指的是哪一个,猜一个就违背了你的请求。在同一个 Changes 数组里把其余成员一并关掉,保存即可通过
签名文档闸门的存在原因是:追加的修订叠在 PDFium 完整重存的上面,那是新的字节序列,已有签名覆盖的字节范围不能假设原样幸存。只在能接受丢掉该签名时才传 AllowSignedDocument = True
批处理作业里有三个副作用要留意。其一,重写把所选配置归一化为 /BaseState /ON,作者的 /Unchanged 或 /OFF 基线变成显式数组而可见性结果不变。其二,去掉 /AS 也顺带移除了只在纸上出现的水印层这类打印专用把戏;设 DisableAutomaticState 为 False 可保留这些规则,但要接受它们可能在自己关心的事件上覆盖你请求的状态。其三,/ON 和 /OFF 数组按为 Options.Usage 算出的状态写出,所以存进配置 0 的打印预设也会改变屏幕上所有没有 Usage 字典的组的显示——打印预设请放进 /Configs 条目,/D 留给屏幕。合规方面,PDF/A-2(ISO 19005-2 第 6.9 条)和 PDF/UA(ISO 14289-1 第 7.10 条)都禁止配置字典里的 /AS,默认输出因此提前清掉了 PDFium Component 的 PDF/A 预检验证本会报出的一条发现
在 Delphi 查看器里应用图层更改
在查看器里,图层控制是一份用清单填出来的复选框列表,保存进内存再重载。复选框用 Groups 填充,Locked 置位的项禁用,共享 RadioGroupIndex 的成员按互斥处理,应用时经流重载写到 TMemoryStream。TPdf.LoadDocument 要求组件处于非活动状态,先关掉它;缓冲加载会复制字节,流可以立刻释放
procedure TFormMain.ApplyLayerChecklist(
const Changes: TPdfOptionalContentStateChanges);
var
Report: TPdfOptionalContentConfigureReport;
Output: TMemoryStream;
KeepPage: Integer;
begin
Output := TMemoryStream.Create;
try
try
if not Pdf.SaveAsOptionalContentConfigured(Output, Changes,
TPdfOptionalContentConfigureOptions.Default, Report) then
begin
ShowMessage('Layer update rejected: ' + Report.ErrorMessage);
Exit;
end;
except
on E: EPdfError do
begin
ShowMessage(E.Message); // 签名文件:Output 仍是空的
Exit;
end;
end;
KeepPage := Pdf.PageNumber;
Pdf.Active := False;
Pdf.LoadDocument(Output); // Buffered 默认 True
Pdf.PageNumber := KeepPage;
finally
Output.Free;
end;
end;
重载之后,PDFium 按新配置绘制,屏幕和下一次 InspectOptionalContent 调用从此一致。TPdf 与 TPdfView 之间的接线见 用 PDFium VCL 在 Delphi 中构建功能完整的 PDF 查看器。文档还没有图层而你需要添加时,在 Delphi 中用 OCG 和 OCMD 创建 PDF 图层一文带你走一遍 SaveAsOptionalContentCreated
定价、试用版和组件的其他文档功能列在 PDFium Component for Delphi 产品页