Technický článek

Vkládání variabilních fontů CFF2 do PDF s HotPDF

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

Pipeline HotPDF pro vkládání variabilního fontu CFF2 v Delphi jako CID-keyed CFF1 pod FontFile3, s tichou objížďkou přes GDI substituci, které se cesta registrovaných bajtů vyhýbá
HotPDF zamrazí variaci na zvolených hodnotách os a zapíše CID-keyed CFF1 program, ve kterém každé CID rovná původnímu glyph ID, takže mezi číslováními nic nepřekládá

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

Tři sloty pro vložené fonty v PDF, všechny omezené na CFF verze 1, a chybějící slot pro CFF2, kvůli kterému HotPDF před vkládáním konvertuje
každá CFF varianta, kterou musí PDF čtečky zvládnout, je CFF verze 1, takže variabilní font CFF2 nemá slot, dokud se nekonvertuje

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

Jak HotPDF mapuje 16bitové advancy bez znaménka na znaménkové CFF1 operandy s nominalWidthX 32768, takže každý advance fontu ve výstupu PDF v Delphi zůstane bez seškrtání
nominalWidthX 32768 posune okno operandů tak, že nulový advance se uloží jako -32768 a 65535 jako 32767, bez seškrtání jakékoli 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