Technický článek

Odhalování chybějících glyfů PDF při kreslení v Delphi

Chybějící glyf v PDF není chyba. Producent si vyžádá znak, který vybrané písmo nedokáže namapovat, písmo vrátí glyf index nula a soubor, který vyjde, je strukturálně platný, otevírá se všude a ukazuje prázdný rámeček tam, kde má být jméno nebo částka. Nikdo v generujícím potrubí se to nedozví. Příjemce ano. HotPDF tuto smyčku zavírá pomocí TrackUnresolvedGlyphs: zapněte ji a cesta kreslení textu zaznamená každý kódový bod, jehož vyhledání glyfu se rozřeše na index nulu, a spustí OnUnresolvedGlyph jednou za unikátní zjištění s kódovým bodem, písmem, na němž selhalo, psací soustavou, do níž patří, a návrhem písem, která by jej pokryla

Detekce je polovina odpovědi. Druhou polovinou je SetFontFallbackChain, která registruje uspořádaný seznam písem na psací soustavu, takže běžné případy se rozřeší samy a k vašemu handleru dorazí jen skutečné mezery. Společně mění třídu defektů, o kterých se dříve dozvídal zákazník, na kontrolu při sestavení

Proč chybějící glyf nic nevyvolá?

Protože ISO 32000 neklade producentovi žádnou povinnost ověřovat pokrytí a glyf index nula je legitimní glyf. Je to .notdef, jehož obrys volí návrhář písma: obvykle prázdný nebo dutý obdélník, někdy vůbec nic. Prohlížeč, který jej nakreslí, se chová správně. Extrakce textu může dokonce vrátit správné znaky, protože mapování /ToUnicode se zapisuje ze zdrojového textu, nikoli z obrysů, takže automatizovaná kontrola zpáteční cesty ráda projde dokument, jehož viditelný text má díry

Diagram, proč chybějící glyf PDF zůstává potichu, když glyf nula kreslí prázdný rámeček a extrakce ToUnicode projde kontrolami zpáteční cesty
Glyf nula je legitimní odpověď .notdef a /ToUnicode se zapisuje ze zdrojového textu, takže o mezeře se v potrubí nedozví nic

Praktickým důsledkem je, že pokrytí musí být kontrolováno v okamžiku kreslení, kdy knihovna ještě ví, který kódový bod byl vyžádán a který glyf písmo skutečně nabídlo. Poté je informace pryč

Detektor musí hlídat stav subsetu, nikoli device context

Právě zde se zvolvila první implementace a důvod stojí za pochopení, protože se vztahuje na jakoukoli kontrolu pokrytí přišroubovanou na textové potrubí. HotPDF má dvě textové cesty. Jedna vypouští přes registrované písmo Unicode TrueType s mapou znaků v paměti postavenou v okamžiku registrace. Druhá je legacy cesta GDI, která vytváří čerstvý device context a handle písma na každý běh znaků

Posuzovat pokrytí z cesty GDI je beznadějné. Její mapování není mapování, které skončí ve vypouštěném content streamu, a obojí není synchronizováno, takže detektor čtoucí výsledky GDI hlásí celý tisknutelný rozsah ASCII jako nevyřešený. Autoritativní odpověď žije v registrovaném písmu: mapě znaků, kterou parsuje RegisterUnicodeTTF a dotazuje se přes GetUnicodeGlyphForCodepoint. Detektor je proto bráněn na stavu subset-ready, nikoli na jakékoli podmínce GDI, a na dokumentech, která nikdy neregistrovala písmo Unicode, prostě neběží, což je správně, protože tyto dokumenty jsou stejně omezeny na standardní kódování

Vedle ní sedí druhá past. Rodinný název písma GDI a PostScript název vytěžený z binárního souboru písma při registraci jsou různé řetězce a ne tak, že byste je mohli normalizovat: rodina zvaná Arial Unicode MS nese PostScript název ArialMT. Jakákoli brána napsaná jako „je aktuálně vybrané písmo to, které jsme registrovali“, srovnávaná podle názvu, je mrtvý kód, který se nikdy nespustí. Braňte na stavu, nikdy na názvech písem

Tok detekce nevyřešených glyfů HotPDF ukazující bránu stavu subsetu, vyhledávání GetUnicodeGlyphForCodepoint a zapojení události OnUnresolvedGlyph
Pokrytí se posuzuje z mapy registrovaného písma Unicode, nikoli z GDI, a každý unikátní kódový bod spustí jednu událost s návrhem písma

Netestujte detektor glyfů emoji

Zjevný testovací případ je usmívající se obličej a přesvědčí vás, že detektor je rozbitý. Běžné emoji kódové body v astrálních rovinách se rozřešou skrze syntézní cestu private use, která je namapuje přímo na index glyfu, takže se nikdy nedostanou do obecné větve pokrytí. Detektor se chová správně a test měří špatnou cestu

Použijte namísto toho nepřiřazený kódový bod. U+0378 je v Unicode trvale nealokován, takže jej žádné písmo legitimně nenamapuje a procvičí přesně větev, kterou chcete ověřit. Rozlišení mezi „funkce je rozbitá“ a „test zvolil vstup, který funkci obchází“ stojí skutečné hodiny a nepřiřazené kódové body jsou nejlevnější způsob, jak se mu vyhnout

type
  TCoverageAudit = class
  private
    FFindings: TStringList;
  public
    procedure Handle(Sender: TObject;
      const Info: THPDFUnresolvedGlyphInfo);
    property Findings: TStringList read FFindings;
  end;

procedure TCoverageAudit.Handle(Sender: TObject;
  const Info: THPDFUnresolvedGlyphInfo);
begin
  // Spustí se jednou za unikátní kódový bod, nikoli jednou za výskyt
  FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
    [Info.CodePoint, String(Info.FontName), Ord(Info.Script),
     String(Info.SuggestedFonts)]));
end;

// Zapojení do generující úlohy
Pdf := THotPDF.Create(nil);
try
  Pdf.TrackUnresolvedGlyphs := True;
  Pdf.OnUnresolvedGlyph := Audit.Handle;
  Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
  Pdf.EndDoc;
  if Audit.Findings.Count > 0 then
    // Nechte úlohu selhat, namísto odeslat stránku s rámečky
    raise Exception.Create(Audit.Findings.Text);
finally
  Pdf.Free;
end;

Fallback řetězce jsou na psací soustavu, nikoli na písmo

Důvodem, proč je fallback rozsazen podle psací soustavy, nikoli podle zdrojového písma, je, že mezery pokrytí se shlukují podle psacího systému. Latinské textové písmo postrádá Devanagari, thajštinu, han a emoji naráz a náhradou pro každé je jiné písmo. Deklarace jednoho řetězce na psací soustavu proto popisuje skutečné nasazení: jedno latinské písmo pro text, jedno písmo CJK, jedno písmo emoji, jedno záložní pro všechno

Diagram fallbacku písem podle psací soustavy mapující psací soustavy hfsCJK, hfsArabic, hfsEmoji a hfsOther na uspořádané řetězce náhradních písem v HotPDF
Každá psací soustava dostává vlastní uspořádaný řetězec, takže latinské textové písmo postrádající han, arabštinu nebo emoji spadne na písmo, které je pokryje
// THPDFFontScript pokrývá hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji a hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
  ['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);

Fallback a detekce jsou doplňkové, nikoli alternativní. Řetězce zvládnou pokrytí, které jste předvídali; detektor hlásí pokrytí, které jste nepředviděli, což je u systému zpracovávajícího libovolná zákaznická data ta zajímavá polovina. Vezměte na vědomí, že nahrazení písma mění metriky, takže odstavec, který spadne do zálohy, se může přeformátovat; pokud na layoutu záleží, stojí za nastudování uzávěr a chování subsetingu náhradního písma v článku o uzávěru podsady písma a psací soustavy potřebovující přeřazení či spojování řeší shaping fáze popsaná v shapingu textu komplexních psacích soustav

Jak dosadit chování dodatečně, aniž byste riskovali stávající cestu

Tentýž release přidal legacy fallback tabulky kern pro rozestup párů a způsob, jakým byl rozsazen, je vzor hodný okopírování. Namísto přidání nového rozhodovacího bodu do logiky kerningu visí fallback na větvi brzkého ukončení, která už existovala pro písma bez tabulky GPOS. Moderní písmo s GPOS se k ní nikdy nedostane, takže jeho chování je beze změny konstrukcí, nikoli testováním. Cesty neregistrující písmo Unicode produkují dva nulové rozestupy, takže jsou beze změny také

To je obecný tvar nízkorizikového dosazení v dospělé renderovací knihovně: najděte větev, která aktuálně neprodukuje nic, a nové chování umístěte tam. Mění „věříme, že to nic neregridovalo“ na „to nemohlo nic regridovat“, což je o textovém enginu, skrze který jdou faktury jiných lidí, mnohem lepší výrok

Učiňte z něj bránu, nikoli log

Zjištění pokrytí jsou užitečná jen tehdy, když na nich něco selže. Ve službě generující dokumenty je produktivní uspořádání nechat sledování zapnuté v noční regression úloze proti korpusu skutečných zákaznických jmen, adres a popisů produktů a nechat úlohu selhat na jakémkoli zjištění. Protože událost se spustí jednou za unikátní kódový bod, nikoli jednou za výskyt, výstup zůstává dost malý na čtení, i když celá psací soustava chybí

V produkci je lepší tentýž handler používat jako telemetrii: zaznamenejte kódový bod a písmo, dokument dále obsluhujte a nechte agregát říci, kterou psací soustavu přidat do sadu písem nasazení příště. Chování vykreslování pro vložená a nahrazená písma dále pokrývá vykreslování glyfů vložených písem a kompletní seznam vlastností včetně TrackUnresolvedGlyphs je dokumentován na stránce produktu HotPDF Delphi PDF component