PDF 沒有可變字型這個概念。內嵌在 PDF 檔案中的字型是一組固定外框搭配固定量度,因此可變字型在放進文件之前必須先化簡為一個靜態實例。HotPDF 在內部完成這項實例化:你檢視可變字型的座標軸,選定諸如字重 620 或字寬 87.5 之類的座標值,函式庫便會把這些數值烘焙成一個完整、自成一體的字型程式,任何符合規範的 PDF 閱讀器都能轉譯
這件事之所以重要,是出於實務考量而非理論探討。字型廠商越來越傾向只出一個可變字型檔,而不是十幾個靜態字重,設計團隊選用的數值往往沒有任何一個具名實例能提供。若沒有實例化,報表產生器要嘛退回預設實例(等於捨棄設計決策),要嘛把整個可變字型內嵌進去,寄望檢視器能理解它根本無從得知的座標軸座標,而沒有任何閱讀器被規範要求這麼做
實例化究竟要重建些什麼?
OpenType 可變字型為每個字符儲存一個預設外框,加上一組依設計空間位置索引的差異值。套用座標軸座標並不是把數字寫進標頭那麼簡單,而是要走訪 gvar 資料表、對指定位置內插差異值、移動控制點,然後重新計算所有由那些控制點衍生出來的一切。HotPDF 會重建字符外框、長版的 loca 表、完整的水平與垂直量度、整體字型邊界框,以及 sfnt 檢查碼調整值
同樣重要的是要移除什麼。靜態實例不得保留 fvar、avar、gvar、HVAR、VVAR、MVAR、STAT 或 cvar,過時的 DSIG 也必須移除,因為被簽署的位元組已不存在。若留下這些資料表中的任何一個,會產生一個自稱可變、卻帶有已經被移動過的外框的字型,而確實會套用變化的閱讀器就會再套用一次
虛擬控制點與重複套用的陷阱
整個流程中最細微的規則跟量度有關。在 gvar 中,一個字符的控制點數涵蓋外框控制點(或複合字符的元件控制點),再加上四個編碼左側邊距、前進寬度以及其垂直對應值的虛擬控制點。這些虛擬控制點本身也會受差異值影響
所以當字型有 gvar 資料表時,HotPDF 會從已內插的虛擬控制點推導水平與垂直量度,不會再額外套用 HVAR 或 VVAR。兩者都加是典型錯誤:同一份變化被套用兩次,每個前進寬度都會稍微偏寬,在對齊排版的文字行中,這會表現為文字逐漸向右偏移。只有在字型沒有 gvar 時,函式庫才會把量度變化儲存直接烘焙進 hmtx 或 vmtx
還有兩個細節維持了幾何的正確性。虛擬控制點從不參與輪廓內插,因此對簡單字符未明確列出的控制點,會依每個輪廓以 IUP 方式推算,虛擬控制點則排除在外。複合字符的差異值會套用到使用 XY 參數的元件偏移量上,之後再遞迴重新計算子項邊界。這種遞迴的深度有上限並會檢查循環,因為惡意或單純有問題的元件圖若不加以限制,可能導致無止盡遞迴
選定之前先檢視設計空間
任何實例化流程的第一次呼叫都是 InspectVariableFont,它會回報座標軸以及字型廠商定義的具名實例。座標軸記錄帶有四位元組標籤、最小值、預設值與最大值、旗標與名稱 ID;具名實例則帶有子字族名稱 ID、旗標、選用的 PostScript 名稱 ID,以及每個座標軸各一個的座標值:
var
Pdf: THotPDF;
Axes: THPDFVariableFontAxisArray;
Instances: THPDFVariableFontNamedInstanceArray;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
begin
for I := 0 to High(Axes) do
Writeln(Format('%s min=%.1f default=%.1f max=%.1f',
[string(Axes[I].Tag), Axes[I].MinimumValue,
Axes[I].DefaultValue, Axes[I].MaximumValue]));
Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
end
else
Writeln('not a variable font - embed it as an ordinary TrueType face');
finally
Pdf.Free;
end;
end;
回報座標軸範圍很重要,因為座標軸數值會被限制在字型宣告的範圍內,而不是限制在你介面提供的範圍內。若滑桿讓使用者能對一個 wght 座標軸最大只到 900 的字型要求字重 1000,這種矛盾應該在介面層修正,而不是在字型層悄悄修正,否則印出的成品會與預覽不一致
選定座標並輸出文件
座標軸的選定具有狀態性,會套用到之後才註冊的字型。SetVariableFontAxis 接受一個四位元組可列印 ASCII 標籤與一個有限數值,任何其他輸入都會拋出例外而非默默忽略。ClearVariableFontAxes 會重設選定內容,GetVariableFontAxisSelections 則回報目前待處理的內容,這在多條程式路徑可能都動過同一個文件物件的報表引擎中,值得記錄下來。字族本身透過 SetFont 以名稱選定,方式與其他任何內嵌 TrueType 字面完全相同:
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.SetVariableFontAxis('wght', 620); // 半粗體,並非具名實例
Pdf.SetVariableFontAxis('wdth', 87.5); // 稍微窄縮
Pdf.CurrentPage.SetFont('Inter', [], 11);
Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');
Pdf.ClearVariableFontAxes; // 回到預設實例
Pdf.CurrentPage.SetFont('Inter', [], 10);
Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
OnVariableFontInstance 事件會在每個實例產生時觸發,並回報所用的座標軸數值,這是在日誌中證明某份 PDF 究竟含有什麼內容最省事的方法。由於每一組不同的座標值都會產生一個不同的字型程式,請把座標軸選定視為字型快取鍵的一部分;快取機制的細節說明於 持久性字型子集快取 一文
實例化如何與子集化及塑形互動?
實例化在子集化之前執行,而這個順序是對的。實例化後的字型是一個普通的靜態 TrueType 字面,因此一般子集化程序會像對待其他字型一樣對待它:計算字符封閉集合,保留文件實際使用的字符並捨棄其餘部分。需要留意的互動是,同一字族的兩種不同座標軸選定,是兩個不同的字型程式,因此混用字重 400 與字重 620 的文件,會內嵌兩個子集,而不是一個共用兩種實例的字面
塑形原則上不受影響,實務上仍值得驗證。版面配置功能存在於 GSUB 與 GPOS,實例化會保留這些資料表,因此連字與風格替換字符會繼續正常運作,如 OpenType GSUB 風格替換字符 一文所述。改變的是定位:窄縮實例的前進寬度比預設值窄,因此任何在實例化之前量測文字的版面配置,量到的都是錯誤的寬度。用你即將轉譯所用的同一組座標軸選定來量測,這個落差就會消失
最後有一個來自實作面的防禦性提醒,對任何延伸這條路徑的人都有用。沒有垂直量度的字型,在 Delphi 呼叫端仍然會求值動態陣列引數,因此剖析階段用到的陣列一律會配置記憶體,而不是依賴 HasVerticalMetrics 檢查來提前跳過一個空索引。這正是那種會把看似受保護的分支,變成剛好在你沒測過的字型上發生存取違規的語言層級細節
可變字型支援與內嵌、子集化及字符封閉集合共用同一條字型流程,詳情見 字型子集封閉集合與已塑形字符 一文。適用 Delphi 與 C++Builder 的完整字體排印功能清單,列於 HotPDF Delphi PDF 元件頁面