PDF 中的每個可見字元都攜帶對繪製它的字型的引用,而 PDFium 元件允許您沿著該引用追蹤回字型物件並讀取它所知道的資訊;存取的單位是字元,而不是文件:您透過分頁文字中的索引選擇字元,並查詢其家族名稱、基本名稱、粗細、斜體角度以及底層字型是否實際包含在檔案內部;最後一個屬性是大多數分析真正追求的,因為嵌入的字型會隨文件一起傳輸,而未嵌入的字型則是對閱讀器電腦剛好安裝了相同字型的假設
該元件透過您用於算繪和文字擷取的相同 TPdf 和 TPdfView 物件公開這些屬性;沒有單獨的「字型表」物件需要開啟;分頁文字被解析後,字型屬性就掛在字元索引上,您可以一次讀取一個字形;這種設計符合 PDF 最初儲存資訊的方式:單個分頁可以切換字型數十次,而對「這份文件使用的是什麼字型」的唯一誠實回答是「這取決於您指的是哪個字元」
讀取單個字元背後的字型
最小且有用的操作是獲取字元索引,並傾印出 PDFium 能告訴您的有關其字型的一切資訊;TPdf 和 TPdfView 上的每個字型屬性都按字元位置進行索引,因此該索引會穿過所有屬性;分頁也必須是目前分頁,以便索引針對正確的文字進行解析,一旦您移過了第一頁,這點就很重要了
procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
Report: TStringList;
PtSize: Single;
begin
Report := TStringList.Create;
try
PtSize := Pdf.FontSize[CharIndex];
Report.Add('Character : ' + Pdf.Character[CharIndex]);
Report.Add('Family : ' + Pdf.FontFamilyName[CharIndex]);
Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
Report.Add('Weight : ' + IntToStr(Pdf.FontWeight[CharIndex]));
Report.Add('Italic : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
Report.Add('Size : ' + FormatFloat('0.0', PtSize) + ' pt');
Report.Add('Ascent : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
Report.Add('Descent : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
Report.Add('Embedded : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));
ShowMessage(Report.Text);
finally
Report.Free;
end;
end;
其中一些特徵標記會讓來自其他函式庫的人感到驚訝;FontAscent 和 FontDescent 接受兩個引數:字元索引和點大小,因為 PDFium 在字形空間單位中報告這些度量,只有在您按設定文字的大小對其進行縮放後,它們才會變成像素;傳遞您已經從 FontSize[CharIndex] 讀取的值,您就會得到與其餘版面配置相同的點的上升高度和下降高度;下降高度返回為負值,因為它在基線以下進行測量;家族名稱和基本名稱故意分開為不同的字串:基本名稱是 PDF 中原始的 /BaseFont 項目,通常攜帶像 ABCDEF+ 這樣的子集字首,而家族名稱是算繪器將其解析為的清理後的名稱
將點選轉換為字元索引
在檢視器中,您很少預先知道索引;使用者點選一個字形,您必須將像素座標轉換為它下方的字元;CharacterIndexAtPos 精確地執行了該操作,它接受滑鼠位置和容差,並回傳最近字元的索引,當點選落在空白處或空白分頁時則回傳負值
procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Index: Integer;
begin
if not PdfView.Active then
Exit;
// 4 px of slack in each direction so a near-miss still hits the glyph.
Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
if Index < 0 then
Exit; // clicked between glyphs; leave the panel alone
PdfView.CurrentCharIndex := Index;
DescribeFontAt(PdfView.Pdf, Index);
end;
容差值得調整;太小會讓使用者覺得必須精確點選字母的線條,太大則會讓邊緣的點選捕捉到遠處無關的字元;對於螢幕檢視,三到五個裝置像素是合理的起點;回傳的索引是目前解析後分頁文字的索引,也就是所有字型屬性所期望的索引,因此您可以直接將其傳遞給上面的程式;在 CurrentCharIndex 中儲存它是選填的,但很方便:檢視器將其作為目前焦點字形,如果 UI 的其他部分想要讀取選取項目而不用重新計算,這將很有幫助
嵌入是至關重要的屬性
對於大多數實際工作,唯一值得回答的問題是每個字型是否已嵌入;一個字型全部包含在內的文件在印刷局的 RIP、同事的筆記型電腦以及完全沒有 GUI 的伺服器上算繪出來的效果是完全相同的;一個依賴於未嵌入 Helvetica 的文件是在賭每台電腦都剛好有相匹配的字型,當賭注失敗時,閱讀器會使用相近的字型替代,度量衡會發生變化,一個精心配置的表單會發生微調進而損壞;走訪分頁文字並按嵌入狀態對字型進行分組,可以廉價地為您提供該答案
procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
Embedded, External: TStringList;
I: Integer;
Name: string;
begin
Embedded := TStringList.Create;
External := TStringList.Create;
try
Embedded.Sorted := True;
Embedded.Duplicates := dupIgnore;
External.Sorted := True;
External.Duplicates := dupIgnore;
for I := 0 to Pdf.CharacterCount - 1 do
begin
Name := Pdf.FontBaseName[I];
if Name = '' then
Continue; // generated spaces and the like have no font
if Pdf.FontIsEmbedded[I] then
Embedded.Add(Name)
else
External.Add(Name);
end;
if External.Count > 0 then
ShowMessage(IntToStr(External.Count) +
' non-embedded font(s):' + sLineBreak + External.Text)
else
ShowMessage('All ' + IntToStr(Embedded.Count) +
' font(s) on this page are embedded.');
finally
Embedded.Free;
External.Free;
end;
end;
有兩個細節可以確保準確性;首先,CharacterCount 是按分頁計算的,因此整個文件的稽核意味著依次將 Pdf.PageNumber 設定為每個分頁並再次執行循環,然後合併結果;其次,文字層包含產生的字元,例如閱讀器在單字之間推斷出的空格,而這些字元背後沒有字型物件;基本名稱為空的檢查會跳過它們,而不是記錄一個幻影;基本名稱是此處去重的正確鍵,因為它攜帶的子集字首區分了同一個家族的兩個不同子集,這通常是您想要知道的
將嵌入的字型提取出來
當字型已嵌入時,您可以直接讀取其位元組;FontData 回傳原始的字型程式,即 PDF 攜帶的相同 TrueType 或 CFF 資料,這足以寫入獨立的字型檔案,或針對已知庫對該字型進行特徵碼識別;當字型未嵌入時,它會回傳一個空陣列,因此嵌入檢查和長度檢查共同保護了寫入操作
procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
const OutputFile: string);
var
Data: TBytes;
Stream: TFileStream;
begin
if not Pdf.FontIsEmbedded[CharIndex] then
begin
ShowMessage('That glyph''s font is not embedded; nothing to extract.');
Exit;
end;
Data := Pdf.FontData[CharIndex];
if Length(Data) = 0 then
Exit;
Stream := TFileStream.Create(OutputFile, fmCreate);
try
Stream.WriteBuffer(Data[0], Length(Data));
finally
Stream.Free;
end;
ShowMessage('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;
這些位元組是嵌入的子集,而不是原始的零售字型,因此您獲得的內容通常僅覆蓋文件實際使用的字形;這對於鑑識和驗證來說完全正確,但對於重複使用來說則不合適;一個包含三十個字形的 Times New Roman 子集不是您可以安裝並用來打字的字型;請將提取視為檢查交付內容的一種方式,而不是字型恢復工具;如果您需要匹配的基本名稱來標記輸出,請隨資料一起讀取 FontBaseName[CharIndex],如果想要純粹的家族名稱,請去除前導的子集標籤
理解粗細數值
FontWeight 回傳數值粗細類別,與 CSS 使用的 100 到 900 比例相同,其中 400 是常規,700 是粗體;PDFium 會報告字型宣告的任何內容,這並不總是整百的數值;字型可以宣告 350 或 650,將任何大於或等於 600 的內容視為「足夠粗以至於重要」,比測試精確的 700 更有意義;斜體角度是伴隨訊號:非零值(通常為負值)表示該字型是斜體或真實斜體設計,零表示直立;它們加在一起,可以讓您在不用算繪任何內容的情況下區分粗斜體與常規,這是發行前檢查或無障礙稽核想要批次進行的檢查
這些讀取都不需要算繪的點陣圖;它們來自解析後的文字層,因此在正確的分頁上開啟文件就是您所需的全部設定,這使得字型檢查在整個封存檔中執行的成本非常低;如果您將其與文字擷取配合使用,相同的字元索引與您拉出的文字對齊,因此字形的字型及其 Unicode 值是對一個索引的兩次讀取;關於 使用 PDFium 元件從 PDF 文件中擷取文字 的配套文章更深入地介紹了文字層的該方面
此處顯示的字型屬性是 PDFium Delphi VCL 元件 的一部分