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的群組視為可見,因為它不參與檢視意圖的可見性 - 最後跑所選組態的
/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 的物件編號、名稱、intents、三個 Usage 狀態、語言、縮放範圍、Locked 旗標、選項按鈕群組索引與算好的 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 鍵會被移除,開啟、列印或匯出事件就沒辦法把圖層翻回去
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 會移除只在紙上出現的浮水印圖層這類花招;把 DisableAutomaticState 設成 False 可以保留那些規則,代價是接受它們可能在該事件上壓過您要求的狀態。好的一面是,PDF/A-2(ISO 19005-2 條款 6.9)與 PDF/UA(ISO 14289-1 條款 7.10)都禁止組態字典裡出現 /AS,所以預設輸出少了一項您的 PDFium Component 的 PDF/A 預檢驗證原本會回報的問題
圖層控制在 Delphi PDF 檢視器裡的位置
在檢視器裡,圖層控制是一份由清單驅動、再加一次重載已存結果的核取清單。用 Groups 填出核取清單、停用 Locked 的項目、把共享 RadioGroupIndex 的成員當成互斥,套用時寫進 TMemoryStream 再把那個串流載回 TPdf,讓檢視畫出新狀態。TPdf 與 TPdfView 之間的接線見用 Delphi 的 PDFium VCL 打造功能完整的 PDF 檢視器。授權、試用版下載與其餘功能清單在 PDFium Component for Delphi 產品頁