HotPDF bäddar in OpenType-variable fonts med CFF2-konturer genom att konvertera dem, vid de valda axelvärdena, till ett CID-keyed CFF1-fontprogram som PDF-läsare redan förstår. Vanlig nativ text registrerad via RegisterUnicodeTTF använder fontens egna cmap- och hmtx-tabeller, och skrivaren bäddar in det konverterade programmet under /FontFile3 med /Subtype /CIDFontType0C, med varje CID lika med det ursprungliga glyph-ID:t
Den uppenbara vägen är värd att beskriva, för den fallerar utan att säga till någon. Parsa och cachaa CFF2-tabellerna, och låt sedan den vanliga textvägen slå upp fonten via Windows GDI med en TrueType-only-matchning: GDI har inte en aning om vad det ska göra med CFF2-konturer och byter i smyg ut mot ArialUnicodeMS. Allt nedströms som läser substitutet får fel konturer, fel bredder och fel extrahering för tecken på supplementalplanen. PDF:en öppnas fint. Den är bara inte satt i den font du bad om, och därför måste textvägen jobba från de registrerade bytena i stället för en GDI-uppslagning
Varför kan inte en PDF bädda in en CFF2-font direkt?
PDF har ingen plats för ett CFF2-program. ISO 32000-2 §9.9 listar de inbäddade fontprogramtyper en läsare måste hantera: Type 1 under /FontFile, TrueType under /FontFile2, och under /FontFile3 ett bart CFF-program (Type1C eller CIDFontType0C) eller en OpenType-wrapper runt TrueType- eller CFF-konturer. Varje CFF-flavor i den listan betyder CFF version 1, formatet läsare har parsat i två decennier
CFF2 är ett annat format som råkar dela namn. Det släpper Name- och String-INDEX, släpper charset:en, flyttar glyph-advanser ut ur CharStrings och lämnar dem till hmtx, och lägger till blend-operatorer så att en enda kontur kan beskriva hela designrymden. En CFF1-parser som möter en CFF2-tabell har inget underlag för att gissa, och ingenting i PDF-specifikationen ålägger den att försöka. Att svepa in CFF2-fonten i en OpenType-ström och hoppas på det bästa är precis den sortens fil som renderas på en maskin och misslyckas på nästa, så HotPDF gör inte det
Konverteringen måste också vara statisk. Som artikeln om statisk instansiering av variable fonts förklarar är en PDF-font en fix uppsättning konturer med fixa metriker, och ingen läsare krävs tillämpa axelkoordinater. HotPDF löser därför variationen först och skriver CFF1 först därefter
Skriva nativ text i en CFF2-font
För applikationskod beter sig en CFF2-font som vilken annan registrerad Unicode-font som helst: välj axelvärden, registrera filen, välj familjen vid namn, och rita. RegisterUnicodeTTF behåller en separat kopia av den ursprungliga namntabellen, så SetFont matchar familjenamnet, fullständiga namnet, PostScript-namnet eller det typografiska familjenamnet och väljer de frysta statiska fontbytena, utan någon Windows-installation eller GDI-uppslagning
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'brochure.pdf';
Pdf.BeginDoc;
// Axelvärden gäller fonts registrerade efter det här anropet
Pdf.SetVariableFontAxis('wght', 650);
Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
// Family-, full-, PostScript- eller typografiskt family-namn från namntabellen
Pdf.CurrentPage.SetFont('Acme Serif VF', [], 12, DEFAULT_CHARSET);
Pdf.CurrentPage.UnicodeTextOut(72, 100, 0, 'Variable weight 650, CFF2 outlines');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Glyph-ID:n kommer från den registrerade cmap:en och advanser från den registrerade hmtx:en. En glyph vars advans faktiskt är noll stannar noll i stället för att falla tillbaka till en GDI-mätning, och ett UTF-16-surrogatpar ger exakt en advans, inte två. BMP-tecken, supplementtecken och syntetiska shaping-bärare behåller per-font Unicode-mappningar, så ett byte till en annan inbäddad font eller en ny sida låter inte den andra fonten uppdatera denna fonts subset-tillstånd eller låna dess glyph-ID:n
Mätning följer samma byte. THPDFPage.GetWideTextWidth och THPDFPage.GetWideCharAdvances läser metriken för den valda byte-baserade fonten, så radbrytning och högerjustering stämmer med det som ritas. En ordningsdetalj snärjer folk: SetVariableFontAxis-ändringar efter registreringen påverkar nästa font du registrerar, inte instansen som redan är fryst. För en andra vikt, sätt det nya värdet och registrera igen
Hur bygger HPDFConvertCFF2ToCFF det CID-keyed-programmet?
HPDFConvertCFF2ToCFF producerar ett rått CID-keyed CFF1-program med en enda Font DICT, där CID är lika med det ursprungliga glyph-ID:t för varje plats, även tomma och oanvända. Den identiteten är hela designen. Fontnamnet, PDF:ns /W-bredder, CFF-advanserna och ToUnicode-CMapen syftar alla på samma numrering, så inget lager behöver översätta mellan glyph-ID:n och CIDs, och ingenting kan komma ur steg
På PDF-sidan är ättlingen en CIDFontType0-font med Identity-H- eller Identity-V-kodning, och programströmmen ligger under /FontFile3 med /Subtype /CIDFontType0C. Eftersom CID redan är lika med GID bär ättlingen ingen /CIDToGIDMap, och strömmen undantas från den senare stream-ersättningshaken som används för registrerade TrueType-fonts. I motsats till /Subtype /OpenType-wrappern höjer den konverterade strömmen inte PDF 1.6-versionskravet
Bredderna behövde ett litet trick. En CFF1 Type 2 CharString lagrar advansen som en valfri första operand relativt nominalWidthX, och heltalsoperander är begränsade till det signerade 16-bitarsområdet. En OpenType-advans är ett osignerat 16-bitarsvärde, upp till 65535. HotPDF sätter nominalWidthX till 32768 och defaultWidthX till 0, och skriver sedan varje advans explicit som advance - 32768. En nolladvans blir -32768 och en 65535-advans blir 32767, så hela uint16-området är representerbart utan att klampa en enda glyph
Geometrin får samma omsorg. FontMatrix är [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0], så en font med 2048 enheter behåller sina nativa koordinater. Relativa flyttar och linjer för stora för en operand delas upp i representerbara steg, och kubiska kurvor delas på ett sätt som bevarar deras form. De ursprungliga copyright- och licensposterna, den ursprungliga namntabellen och inbäddningsflaggorna följer med i CFF Notice-strängen
Anropa konverteraren utanför ett dokument
Konverteraren är en publik funktion i uniten HPDFOpenTypeAdvanced, skriven i Pascal, utan HarfBuzz-beroende och utan något systemfontinstallationssteg. Det gör den användbar på egen hand, till exempel för att förkonvertera en font vid byggtid eller inspektera vad PDF-skrivaren kommer bädda in
uses
System.SysUtils, System.IOUtils, HPDFOpenTypeAdvanced;
procedure ConvertWeight650;
var
Source, Cff: TBytes;
Axes: THPDFVariableFontAxisValueArray;
begin
Source := TFile.ReadAllBytes('C:\Fonts\AcmeSerifVF.otf');
SetLength(Axes, 1);
Axes[0].Tag := 'wght';
Axes[0].Value := 650;
Cff := HPDFConvertCFF2ToCFF(Source, Axes);
if Length(Cff) = 0 then
raise Exception.Create('Not convertible: not CFF2, embedding denied or bad axes');
TFile.WriteAllBytes('AcmeSerif-650.cff', Cff);
end;
Variabel indata går genom geometri-only statisk instansiering vid de givna koordinaterna, som täcker både konturer och advanser. En statisk CFF2-font accepterar bara ett tomt urval; att skicka axelvärden till den behandlas som ett fel i stället för att ignoreras. Resultatet är ett bart CFF1-program, inte en sfnt-container, så ge den inte till kod som väntar sig en OpenType-fil
Fel blir en tom TBytes, inte ett exception, för de förväntade fallen: indata som inte är CFF2, ett axelval som instansieringen inte accepterar, icke-ändliga värden som NaN, och fonts vars OS/2 fsType sätter restricted-license-inbäddning (0x0002) eller bitmap-only-inbäddning (0x0200). Testa alltid längden. Inuti EndDoc gör skrivaren det åt dig och kastar ”StoreFont: CFF2 outline conversion failed”, vilket är rätt beslut: ett tomt fontprogram i en levererad PDF är betydligt värre än ett synligt fel i loggen
Budgeter och avbrytning för serverarbetslaster
Den valfria tredje parametern, THPDFVariableFontControl, sätter gränser för minnet och låter dig avbryta en lång konvertering. Utan den använder konverteraren sina standarder: en gräns på 128 MiB för fontindata och utdata, en kumulativ arbetsbudget på 1 GiB, och CFF1-gränsen på 65535 byte per CharString
type
TFontJob = class
private
FCancelled: Boolean;
procedure CheckCancel;
public
function Convert(const Source: TBytes): TBytes;
property Cancelled: Boolean read FCancelled write FCancelled;
end;
procedure TFontJob.CheckCancel;
begin
if FCancelled then
Abort; // propagerar ut ur HPDFConvertCFF2ToCFF
end;
function TFontJob.Convert(const Source: TBytes): TBytes;
var
Control: THPDFVariableFontControl;
begin
Control := THPDFVariableFontControl.Create(
32 * 1024 * 1024, // maximalt antal utbyte
Int64(256) * 1024 * 1024, // kumulativ arbetsbudget
CheckCancel);
try
// nil-val: standardinstans för ett variable font, eller en statisk CFF2-font
Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
finally
Control.Free;
end;
end;
Budgetöverskridanden och avbrytning rapporteras som exceptions, medvetet åtskilda från vägen med tomt resultat. En tom array betyder att fonten inte går att konvertera; ett exception betyder att det här försöket stoppades. Håll de två isär i felhanteringen, för att försöka igen med en icke-konverterbar font och större budget hjälper inte
Hur mycket väger en inbäddad CFF2-font?
Det viktigaste att planera för är filstorlek: HotPDF bäddar in den kompletta konverterade glyph-uppsättningen för CFF2-fonts, oavsett om EnableFontSubsetting är på eller av. En font med flera tusen glypher lägger till alla i varje dokument som använder den, även om sidan visar en enda rubrik. För en broschyr är det acceptabelt. För en rapportserver som skriver tusentals små PDF:er är det en verklig kostnad du bör mäta innan du bestämmer dig
De relaterade storleksfunktionerna följer samma regel. Konverterade CFF2-fonts bäddas in hela, så varken de kompakta fontsubsets som CompressDocument aktiverar eller den persisterande font-subset-cachen gäller dem. TrueType-fonts i samma dokument subsettas och cachas som vanligt ändå. Den ena fördelen med att behålla varje glyph är att fonts vars licens förbjuder subsetting bäddas in i en form som respekterar den
Resursgränserna är ändliga konverteringsgränser, och CFF1 själv takar vad en enda CharString kan rymma. En glyph vars konverterade CharString överskrider 65535 byte, eller en font som bränner igenom arbetsbudgeten, faller synligt i stället för att skrivas med en tom kontur. Det är den avsedda avvägningen: ett högtligt misslyckande på en exotisk font slår ett dokument med tyst saknade glypher
Nativ CFF2-text och den portabla CFF1-konverteraren följer med HotPDF Delphi PDF-komponenten för Delphi och C++Builder, tillsammans med instansiering av variable fonts, color glyphs och font subsetting som tas upp på annat håll i bloggen