標記內容是 ISO 32000-1 §14.6 為標記頁面內容而定義的機制,而標記 PDF 與 PDF/UA 都建立在它之上。PDFium Component 直接對此公開:PageObjectMarks 從某個頁面物件讀取每一個 BDC 標記與其屬性表、AddPageObjectMark 寫入一個、RemovePageObjectMark 刪除一個,而 PageObjectMarkedContentID 則回報把內容連到結構樹的 MCID
在結構樹能與它所描述的內容接回來之前,可存取性工具都只是猜測。結構樹說「這是一個標題」;MCID 說那個標題實際上是哪些頁面上的哪些標記。兩半都必須可讀,應用程式才能檢查、修復或回報標記
一個標記,用位元組來看,是什麼?
一個帶著標記名稱與選用屬性表的 BDC 運算子,由 EMC 關閉。在內容串流裡它看起來像 /P <</MCID 3>> BDC ... EMC:標記 /P 命名角色、字典承載屬性,而運算子之間的一切都是標記內容。落在這段範圍內的頁面物件承載著這個標記,而這正是 PDFium 交回來的東西,也是 PDFium Component 轉成一筆記錄的東西
TPdfContentMark 持有一個 handle、標記的 Name,以及一個 TPdfContentMarkParam 陣列。每一個參數帶有一個 Key、一個 Kind,以及一個由該 kind 選定的有意義值欄位:pmpInt、pmpFloat、pmpString 或 pmpBlob。這個 kind 來自 PDFium 自身的型別回報,而不是來自哪個 getter 湊巧成功,而這正是讀取屬性表與猜測屬性表之間的差別
var
Marks: TPdfContentMarks;
M: TPdfContentMark;
P: TPdfContentMarkParam;
I: Integer;
begin
Pdf.PageNumber := 1; // PageNumber is 1-based
for I := 0 to Pdf.ObjectCount - 1 do // page object indexes are 0-based
begin
Marks := Pdf.PageObjectMarks(I);
for M in Marks do
begin
Memo1.Lines.Add('mark ' + M.Name +
' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
for P in M.Params do
case P.Kind of
pmpInt: Memo1.Lines.Add(' ' + P.Key + ' = ' + IntToStr(P.IntValue));
pmpString: Memo1.Lines.Add(' ' + P.Key + ' = ' + P.StringValue);
pmpFloat: Memo1.Lines.Add(' ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
pmpBlob: Memo1.Lines.Add(' ' + P.Key + ' = ' +
IntToStr(Length(P.BlobValue)) + ' bytes');
end;
end;
end;
end;
為什麼 pmpUnknown 代表兩種不同意義
當 PDFium 回報 FPDF_OBJECT_UNKNOWN 時會回傳 pmpUnknown,而 PDFium 對一個不存在的金鑰也會回傳這個值。在這一層無法區分這兩種情況,而假裝能區分,會比承認無法區分更糟
對你的程式碼而言,實際後果是:把 pmpUnknown 當作「這裡沒有可用值」,而不是一個你也許還能解碼的型別。如果某個屬性對你的工作流程很重要,請用你認得的 kind 驗證它存在,而且不要從 unknown 推斷出不存在——一個你無法讀取屬性表的標記,是一個你應該回報的標記,而不是你應該默默接受的標記
一筆標記記錄是快照,不是你擁有的 handle
Handle 欄位屬於函式庫。它在標記被移除、頁面物件被摧毀或頁面被卸載的那一刻就失效,所以這筆記錄是一個壽命很短的唯讀快照。跨頁面切換快取它,你持有的就是一個指向引擎已回收記憶體的指標
這是 PDFium 中對頁面物件 handle 一體適用的同一條紀律,而且它在同一個地方抓到人:一個以標記記錄填滿的清單控制項、使用者切換到另一頁,以及一次看起來與導覽無關的當機。把你需要的值複製出來——名稱、金鑰、數字——然後放手讓 handle 離開。頁面物件 handle 在轉換後失效的筆記涵蓋了通用規則以及它在其他地方如何咬人
加入一個標記,以及容易漏掉的儲存步驟
AddPageObjectMark 接受頁面物件索引、一個標記名稱與一份完整的參數集合。參數以一個集合寫入,而不是一次修補一個金鑰,這正是 TPdfContentMarkParam 沒有 Has* 哨兵值的原因——它們本來要守護的「更新既有記錄中單一欄位」情況並不會發生
值得明說的部分:加入一個標記會重建頁面內容串流,好讓該標記能在儲存後存活。這一點必須明說,因為 SaveAs 本身不會重新產生內容——只存在於物件模型中的變更會被丟棄,而儲存後的檔案看起來會跟一開始那份完全一樣。如果你曾經把某個東西加到 PDFium 頁面上、卻發現輸出裡沒有它,這通常是原因
var
Params: TPdfContentMarkParams;
begin
SetLength(Params, 1);
Params[0].Key := 'MCID';
Params[0].Kind := pmpInt;
Params[0].IntValue := NextMcid;
Pdf.AddPageObjectMark(ObjectIndex, 'P', Params); // rebuilds the content stream
Pdf.UpdatePage;
Pdf.SaveAs('tagged-out.pdf');
end;
這能為文件做到什麼,又做不到什麼
光靠標記,做不出一份標記 PDF。一份符合規範的標記文件,需要一棵其元素參照這些 MCID 的結構樹、一個宣告文件已標記的 /MarkInfo 項目,以及意義如同標準所言的角色名稱。寫一個帶著 MCID、卻沒有任何結構元素指向它的 /P 標記,你得到的是一份宣稱已標記的內容,以及一棵從不提及它的結構樹
在這個層級,標記內容真正發揮價值的地方,是在檢查與修復:稽核哪些頁面物件已標記、找出本應被標記為工件的項目,或把 MCID 對照結構樹以找出孤兒。關於這項工作的結構樹那一半,請見 PDF/UA 結構樹驗證的逐步解說;而對於標記最終所服務的閱讀體驗,請見打造 Delphi 中的可存取 PDF 閱讀器的筆記
PDFium Component 為 Delphi、C++Builder 與 Lazarus 應用程式,在 PDFium 引擎之上提供一套高階 VCL API,標記內容、結構樹與可存取性驗證都能從普通的 Pascal 程式碼觸及——完整的 API 面請見 PDFium Component 產品頁