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
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
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
// 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