HotPDF vkládá OpenType variabilní fonty s CFF2 outline tak, že je při zvolených hodnotách os převede na CID-keyed CFF1 font program, kterému už PDF čtečky rozumí. Běžný nativní text registrovaný přes RegisterUnicodeTTF používá vlastní tabulky cmap a hmtx fontu a writer vkládá převedený program pod /FontFile3 s /Subtype /CIDFontType0C, přičemž každé CID zůstává rovné původnímu glyph ID
Zjevnou cestu stojí za to popsat, protože selže, aniž by to komukoli řekla. Zparsejte a cacheujte CFF2 tabulky a pak nechť prostá textová cesta najít font přes Windows GDI s matchem jen na TrueType: GDI nemá žádnou představu, co s CFF2 outline, a tiše dosadí ArialUnicodeMS. Cokoliv dalšího, co si náhradu přečte, dostane špatné outline, špatné šířky a špatnou extrakci znaků ze supplementary plane. PDF se otevře v pořádku. Jen není sázené fontem, který jste si přáli — a proto musí textová cesta pracovat z registrovaných bajtů místo GDI lookupu
Proč PDF neumí vložit font CFF2 přímo?
PDF nemá slot pro program CFF2. ISO 32000-2 §9.9 vyjmenovává typy vložených font programů, které musí čtečka zvládnout: Type 1 pod /FontFile, TrueType pod /FontFile2 a pod /FontFile3 holý CFF program (Type1C nebo CIDFontType0C) nebo OpenType obálka kolem TrueType či CFF outline. Každá CFF varianta v tom seznamu znamená CFF verze 1, formát, který čtečky parsují už dvě dekády
CFF2 je jiný formát, který jen shodou okolností sdílí název. Zahazuje Name a String INDEX, zahazuje charset, bere glyph advance z CharStrings a nechává je hmtx a přidává blend operátory, takže jediný outline dokáže popsat celý design space. CFF1 parser, který potká CFF2 tabulku, nemá na co se odvolat a nic v PDF specifikaci ho k pokusu nezavazuje. Zabalit CFF2 font do OpenType streamu a doufat v nejlepší je přesně ten typ souboru, který na jednom stroji renderuje a na dalším selže, takže HotPDF to nedělá
Konverze navíc musí být statická. Jak vysvětluje článek o static instancing variabilních fontů, PDF font je fixní sada outline s pevnými metrikami a žádná čtečka nemá povinnost aplikovat souřadnice os. HotPDF proto nejdřív vyřeší variaci a teprve potom zapíše CFF1
Psaní nativního textu fontem CFF2
Pro aplikační kód se font CFF2 chová jako jakýkoli jiný registrovaný Unicode font: vyberte hodnoty os, registrujte soubor, vyberte family podle názvu a kreslete. RegisterUnicodeTTF si ukládá samostatnou kopii původní tabulky name, takže SetFont matchuje family name, full name, PostScript name nebo typografický název family a vybere zamrzlé statické bajty fontu, bez instalace do Windows a bez GDI lookupu
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'brochure.pdf';
Pdf.BeginDoc;
// Hodnoty os se týkají fontů registrovaných po tomhle volání
Pdf.SetVariableFontAxis('wght', 650);
Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
// Family, full, PostScript nebo typografický název family z tabulky name
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 přichází z registrovaného cmap a advancy z registrovaného hmtx. Glyfa, jejíž advance je doopravdy nula, zůstává nula místo návratu k GDI měření a UTF-16 surrogate pair dá přesně jeden advance, ne dva. BMP znaky, supplementary znaky a syntetické shaping nosiče si drží Unicode mapování po jednotlivých fontech, takže přepnutí na jiný vložený font nebo začátek nové stránky nedá tomu druhému fontu šanci aktualizovat subset stav tohohle fontu nebo si vypůjčit jeho glyph ID
Měření jde po těch samých bajtech. THPDFPage.GetWideTextWidth a THPDFPage.GetWideCharAdvances čtou metriky vybraného bajtově podloženého fontu, takže lámání řádků a pravé zarovnání souhlasí s tím, co se nakreslí. Na pořadí jedna věc lidi chytá: změny přes SetVariableFontAxis provedené po registraci se týkají příštího fontu, který zaregistrujete, ne už zamrzlé instance. Pro druhý řez máte nastavit novou hodnotu a registrovat znovu
Jak HPDFConvertCFF2ToCFF staví CID-keyed program?
HPDFConvertCFF2ToCFF vyrábí holý CID-keyed CFF1 program s jediným Font DICT, ve kterém CID rovná původnímu glyph ID pro každý slot, včetně prázdných a nepoužitých. Ta identita je celý design. Název fontu, PDF šířky /W, CFF advancy i ToUnicode CMap odkazují na totéž číslování, takže mezi glyph ID a CID nemusí nic překládat a nic nemůže sjíždět z kroku
Na straně PDF je potomek font CIDFontType0 s kódováním Identity-H nebo Identity-V a program stream sedí pod /FontFile3 s /Subtype /CIDFontType0C. Protože CID už rovná GID, potomek nenese žádné /CIDToGIDMap a stream je vyňatý z pozdějšího hooku nahrazování streamů používaného pro registrované TrueType fonty. Na rozdíl od obálky /Subtype /OpenType převedený stream nezvyšuje požadavek na verzi PDF 1.6
Šířky potřebovaly malý trik. CharString typu 2 v CFF1 ukládá advance jako volitelný první operand relativní k nominalWidthX a celočíselné operandy se omezují na znaménkový 16bitový rozsah. OpenType advance je hodnota bez znaménka, až 65535. HotPDF nastaví nominalWidthX na 32768 a defaultWidthX na 0 a pak zapíše každý advance explicitně jako advance - 32768. Nulový advance se stane -32768 a advance 65535 se stane 32767, takže celý rozsah uint16 je reprezentovatelný bez seškrtání jediné glyfy
Geometrii čeká stejná péče. FontMatrix je [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0], takže font s 2048 jednotkami si nechává nativní souřadnice. Relativní pohyby a úsečky příliš velké pro jeden operand se dělí na reprezentovatelné kroky a kubické křivky se dělí způsobem, který zachovává jejich tvar. Původní záznamy copyrightu a licence, původní tabulka name i embedding příznaky putují dál v CFF řetězci Notice
Volání konvertoru mimo dokument
Konvertor je veřejná funkce v unitě HPDFOpenTypeAdvanced, napsaná v Pascalu, bez závislosti na HarfBuzz a bez kroku instalujícího systémový font. To z něj dělá nástroj použitelný samostatně, třeba pro předkonvertování fontu při buildu nebo pro zkoumání toho, co PDF writer vloží
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;
Variabilní vstup prochází static instancingem omezujícím se na geometrii při zadaných souřadnicích, pokrývajícím outline i advancy. Statický CFF2 font přijímá jen prázdný výběr; předání hodnot os bere jako chybu, ne ignoruje. Výsledkem je holý CFF1 program, ne sfnt kontejner, takže ho nedávejte kódu čekajícímu OpenType soubor
Selhání je prázdný TBytes, ne výjimka, a to pro očekávané případy: vstup, který není CFF2, výběr os, který instancer nepřijímá, nekonvergentní hodnoty typu NaN a fonty, jejichž OS/2 fsType nastavuje embedding s omezenou licencí (0x0002) nebo jen bitmapový embedding (0x0200). Vždy testujte délku. Uvnitř EndDoc to za vás udělá writer a vyhodí „StoreFont: CFF2 outline conversion failed", což je správná volba: prázdný font program v odeslaném PDF je o dost horší než viditelná chyba ve vašem logu
Rozpočty a zrušení pro serverové workloady
Volitelný třetí parametr, THPDFVariableFontControl, ohraničuje paměť a dovoluje dlouhou konverzi zrušit. Bez něj konvertor používá defaulty: limit 128 MiB na fontový vstup i výstup, kumulativní pracovní rozpočet 1 GiB a CFF1 limit 65535 bajtů na 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; // propaguje se ven z HPDFConvertCFF2ToCFF
end;
function TFontJob.Convert(const Source: TBytes): TBytes;
var
Control: THPDFVariableFontControl;
begin
Control := THPDFVariableFontControl.Create(
32 * 1024 * 1024, // maximum výstupních bajtů
Int64(256) * 1024 * 1024, // kumulativní pracovní rozpočet
CheckCancel);
try
// nil výběr: defaultní instance variabilního fontu, nebo statický font CFF2
Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
finally
Control.Free;
end;
end;
Překročení rozpočtu a zrušení se hlásí jako výjimky, záměrně odlišné od cesty prázdného výsledku. Prázdné pole znamená, že font se konvertovat nedá; výjimka znamená, že tento pokus se zastavil. Oboje ve zpracování chyb držte odděleně, protože opakování nekonvertovatelného fontu s větším rozpočtem nepomůže
Kolik váží vložený font CFF2?
Hlavní věc, kterou je potřeba plánovat, je velikost souboru: HotPDF vkládá kompletní převedenou sadu glyfů pro fonty CFF2, ať je EnableFontSubsetting zapnutý, nebo ne. Font s několika tisíci glyfy přidá všechny do každého dokumentu, který ho použije, i když stránka ukazuje jeden titulek. Pro brožuru to přijatelné je. Pro report server píšící tisíce malých PDF je to reálná cena, kterou si před závazkem změřte
Příbuzné velikostní funkce drží totéž pravidlo. Převedené fonty CFF2 se vkládají celé, takže na ně neplatí ani kompaktní font subsety zapínané přes CompressDocument, ani perzistentní cache font subsetů. TrueType fonty ve stejném dokumentu se dál běžně subsetují a cachují. Jediný přínos držení všech glyfů je, že fonty, kterým subsetování zakazuje licence, se vkládají v podobě, která ji respektuje
Limit prostředků jsou konečné konverzní limity a CFF1 sám omezuje, co unese jediný CharString. Glyfa, jejíž převedený CharString přesáhne 65535 bajtů, nebo font, který propálí pracovní rozpočet, selže viditelně místo zápisu s prázdným outline. To je zamýšlený kompromis: hlasité selhání na exotickém fontu bije dokument s tiše chybějícími glyfy
Nativní text CFF2 i přenositelný CFF1 konvertor dodává HotPDF Delphi PDF component pro Delphi a C++Builder, spolu s instancingem variabilních fontů, color glyph a font subsetting funkcemi popsanými jinde na tomhle blogu