Tvarované glyfy sa renderujú ako .notdef boxy, keď font subsetter ponechá len glyfy dosiahnuteľné z vydaných code pointov. HotPDF, natívna VCL PDF komponenta pre Delphi a C++Builder, niesla presne túto chybu až do verzie 2.435.0: výstup OpenType GSUB bol zaznamenaný do internej usage bitmapy, o ktorej subsetter tvrdil, že ju bude rešpektovať, a potom ju nikdy nečítal
Toto je iné zlyhanie než to, ktoré popisuje EndDoc bug, ktorý potichu vyradil font subsetting. Ten bug bol o kedy subsetting bežal vzhľadom na serializáciu a vyradil subsetting úplne. Tento je o čo subset obsahuje, keď subsetting beží presne podľa plánu. Pipeline sa spustí v správnom okamihu, šesťpísmenová predpona subsetu sa objaví na /BaseFont presne tak, ako to vyžaduje ISO 32000-1 §9.6.4, súbor sa zmenší, každá latinská stránka sa vytlačí čisto, a arabská stránka vyjde ako rad prázdnych obdĺžnikov. Chyby v poradí sú hlučné, len čo sa pozriete. Chyby v closure ostávajú ticho navždy, pretože subset je štrukturálne platný a chybný je len jeho zoznam členstva
Prečo sa tvarované glyfy renderujú ako .notdef?
Pretože množina code pointov, ktoré dokument vydáva, nie je množina glyfov, ktoré dokument kreslí, a subsetter, ktorý tieto dve zamieňa, zahodí každý glyf vyprodukovaný shapingom. Text shaping premení logickú sekvenciu znakov na pozicionovanú sekvenciu glyfov, a jeho celým účelom je vyprodukovať glyfy, na ktoré nemapuje žiadny jednotlivý vstupný znak: arabské mediálne heh, ligatúra fi, dévanágarský konjunkt, kontextová alternatíva vybraná funkciou rclt. Každé z toho je ID glyfu, ktoré vyrobilo GSUB lookup, nie také, ktoré vám cmap tabuľka dá pre akýkoľvek znak vo vašom reťazci. Subsetter riadený čisto cmap teda prechádza nesprávnym indexom. Verne si ponechá každý glyf, ktorý text mohol použiť pred shapingom, a zahodí presne tie glyfy, ktoré text používa po shapingu. Renderer sa potom opýta embedovaného fontu na GID 1847, subset vynuloval tento záznam v loca, a namiesto neho sa vráti glyph index 0. Glyph index 0 je podľa definície OpenType .notdef, čo je dôvod, prečo je podpis zlyhania prázdny box, nie zlé písmeno alebo pád. Nič v PDF nie je zle formované; font jednoducho neobsahuje glyf, ktorý content stream požadoval
Code pointy nie sú glyfy: tri zdroje subsetu
Správne subset closure musí zjednotiť tri nezávislé zdroje, každý s vlastným akumulátorom. Prvým je množina odvodená z code pointov: HotPDF akumuluje FUnicodeUsedCps počas vydávania znakov BMP a FUnicodeSmpUsed pre znaky doplnkovej roviny dosiahnuté cez surrogate páry, potom mapuje každý cez FUnicodeCpToGid na ID glyfu. Druhým je množina odvodená zo shapingu, ID glyfov, ktoré vyprodukovala GSUB substitúcia, zaznamenaná cez MarkUnicodeGlyphUsed a EnableShapingFeatureForSubset do FUnicodeExtraUsedGlyphs. Tretím je zloženinové (composite) closure: glyf, ktorého numberOfContours je -1 v glyf, je zostavený z komponentných ID glyfov, a ponechanie zloženiny pri zahodení jej komponentov vyprodukuje prázdny obrys namiesto .notdef, čo je diskutabilne horšie, pretože sa to číta ako chyba medzier
HotPDF vždy spracoval prvý a tretí. BuildAndApplyUnicodeFontSubset, vstupný bod subsettingu, ktorý EndDoc volá pred serializáciou, naplní pole použitých glyfov s GID 0, prejde code pointy BMP, prejde zoznam použitia SMP a odovzdá pole subset builderu, ktorý interne vyrieši komponenty zloženín. Druhý zdroj bol napísaný, ale nikdy sa nespotreboval, a pretože tri zdroje zlyhávajú na rôznom obsahu, medzera sa môže roky skrývať v kódovej báze, ktorej regresný korpus je prevažne latinský
Pole, ktoré bolo zapísané a nikdy prečítané
Kontrakt bol dokumentovaný na troch miestach a dodržaný na žiadnom z nich. Deklarácia FUnicodeExtraUsedGlyphs tvrdila, že subsetter EndDoc ho zjednotí s usage odvodeným z code pointov; komentár hlavičky na ApplyArabicGSUBRefinement sľuboval, že každé vydané náhradné GID prejde cez MarkUnicodeGlyphUsed, takže subsetter vtiahne glyf do embedovaného fontu; ten istý sľub sa objavuje doslovne na ApplyArabicGSUBContextualRefinement pre cestu rclt. Obaja volajúci naplnili svoju polovicu. Grep cez každú referenciu na pole vyriešil druhú polovicu za asi deväťdesiat sekúnd: jedna deklarácia, jedna alokácia SetLength vnútri RegisterUnicodeTTF, a zápisy v dvoch označovacích rutinách. Ani jedno čítanie. To je diagnostika, ktorú sa oplatí zvnútorniť, pretože sa dobre zovšeobecňuje ďaleko za fonty. Keď pole zapisuje niekoľko volacích miest a nečíta žiadne, funkcia, ktorú reprezentuje, neexistuje, nech je akokoľvek dôkladne okomentovaná. Krok 1 subsetteru je dostatočne malý na prečítanie na jednej obrazovke, a medzera je zjavná, len čo viete, kde ju hľadať
// Step 1: derive the used-glyph set (as it stood before 2.435.0)
SetLength(UsedGlyphs, FUnicodeNumGlyphs);
for I := 0 to FUnicodeNumGlyphs - 1 do
UsedGlyphs[I] := False;
UsedGlyphs[0] := True; // .notdef is always present
for Cp := 0 to $FFFF do // source 1a: BMP code points
if (Cp < Length(FUnicodeUsedCps)) and FUnicodeUsedCps[Cp]
and (Cp < Length(FUnicodeCpToGid)) then
begin
GID := FUnicodeCpToGid[Cp];
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
for I := 0 to High(FUnicodeSmpUsed) do // source 1b: SMP code points
begin
GID := FUnicodeSmpUsed[I].GID;
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
// source 2 was missing here: nothing ever consulted FUnicodeExtraUsedGlyphs
Oprava jednou slučkou a ručné označovanie glyfov
Oprava je zjednotenie (union), a jej bezpečnostný argument pochádza zo smeru operácie: nastavuje len bity, nikdy ich nečistí, takže žiadny glyf, ktorý predtým subset prežil, nemôže odteraz začať byť zahodený
// v2.435.0: pull GSUB-derived extra glyphs into the subset.
// MarkUnicodeGlyphUsed / EnableShapingFeatureForSubset record GIDs that
// shaping produced but that no emitted code point maps to directly.
for I := 0 to FUnicodeNumGlyphs - 1 do
if (I < Length(FUnicodeExtraUsedGlyphs)) and FUnicodeExtraUsedGlyphs[I] then
UsedGlyphs[I] := True;
Tri vlastnosti robia z tohto zmenu s nízkym rizikom namiesto prepisu font enginu. Je monotónna, ako vyššie. Je no-op na fontoch, ktoré nikdy nič netvarovali, keďže FUnicodeExtraUsedGlyphs ostáva celé False a bajtový výstup pre čisto latinský dokument je nezmenený. A pristáva pred krokom 2, takže oba subset buildery ju zdedia: sparse builder, ktorý zachováva pôvodné číslovanie GID, a compact builder _BuildCompactSubsetTTF, ktorý HotPDF volí pod PDF/A na prečíslovanie ponechaných glyfov do hustého rozsahu, zmenšenie maxp.numGlyphs, a vydanie mapovania starý-na-nový ako stream /CIDToGIDMap vyžadovaný ISO 32000-1 §9.7.4.2. Obaja interne volajú _TTFWalkCompositeClosure, takže tvarovaný glyf, ktorý sa náhodou stane zloženinou, teraz tiež stiahne so sebou svoje komponenty. Composite closure nikdy nebolo pokazené; jednoducho sa nikdy nedostalo k týmto ID glyfov, pretože tieto ID glyfov neboli v množine, ktorú prechádza. Ak riadite GSUB engine priamo namiesto spoliehania sa na vstavané refinement passy, closure je vašou zodpovednosťou, a každé náhradné ID glyfu, ktoré vydáte, musí byť označené skôr, než EndDoc zmrazí množinu použitých glyfov
var
Pdf: THotPDF;
GIDs: array[0..1] of Word;
LigGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'shaped.pdf';
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoNaskhArabic-Regular.ttf');
Pdf.ShapingFeatures := [sfArabicGSUB, sfStandardLigatures,
sfContextualAlternates];
GIDs[0] := Pdf.GetUnicodeGlyphForCodepoint($0644); // lam
GIDs[1] := Pdf.GetUnicodeGlyphForCodepoint($0627); // alef
if Pdf.ApplyLigatureSubstitution(GIDs, 0, 'liga', LigGID) then
Pdf.MarkUnicodeGlyphUsed(LigGID); // omit this and you get .notdef
Pdf.EnableShapingFeatureForSubset('rclt');
Pdf.CurrentPage.RtLTextOut(50, 700, 0, WideString(ArabicText));
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
EnableShapingFeatureForSubset je dávkový náprotivok volania s jedným GID, a je zámerne konzervatívny. Prejde zoznam GSUB lookupov pripojených k jednému štvorbajtovému feature tagu pod aktuálne vybraným script a language path, a označí náhradné ID glyfov, ktoré tieto lookupy môžu vyprodukovať. Je to defenzívny no-op, keď font nenesie žiadnu tabuľku GSUB alebo keď feature na danej ceste chýba, takže volať ho bezpodmienečne je bezpečné. Je to tiež zámerná nadmerná aproximácia: môže ponechať glyfy, ktoré daný dokument nikdy nekreslí. Pre subsetting stojí nadmerné zahrnutie bajty a nedostatočné zahrnutie stojí správnosť, čo z toho obchodu robí ľahkú voľbu. Štruktúra týchto lookupov, a coverage tabuľky rozhodujúce, ktoré glyfy sa zúčastňujú, je pokrytá v prehľade štylistických alternatív GSUB v čistom Delphi
Ako dokázať, že glyf je skutočne v subsete?
Čítaním vydaného fontu, nie posudzovaním stránky pohľadom vo vieweri, ktorý môže na pozadí nahrádzať systémovým fontom. Kontrola, ktorá zachytí celú túto triedu chýb, je mechanická: extrahujte stream /FontFile2 z výstupného PDF, parsujte loca, a potvrďte, že ID glyfu, ktoré očakávate, nesie neprázdny záznam, čo znamená, že jeho počiatočný a koncový offset sa líšia. Prázdny záznam je subsetter, ktorý rozhodol, že glyf sa nepoužíva. Dva zvyky potom robia oveľa ťažším vypustiť túto chybu znovu. Udržujte stránku s tvarovaným písmom v automatizovanom smoke korpuse, nie len v manuálnej sade proofingu, pretože arabčina, dévanágarí a khmérčina precvičujú cesty closure, ktorých sa žiadne množstvo latinského pokrytia nedotkne. A kedykoľvek existuje akumulátor, assertujte, že niečo ho spotrebuje, keďže pole len na zápis je funkcia, ktorá sa skompiluje, testuje zeleno na nesprávnom korpuse a nerobí nič
Kde sa oprava zastaví
Subset closure je nevyhnutné na to, aby sa tvarovaný glyf vykreslil, a nie je postačujúce. Glyf musí byť tiež adresovateľný z content streamu, čo je samostatný problém s vlastnou hranicou. Vstavané arabské refinement passy HotPDF potvrdia substitúciu len vtedy, keď je každé náhradné ID glyfu dosiahnuteľné cez Unicode presentation-form code point cez reverse cmap scan cez zhruba 690 code pointov v U+FB50 až U+FDFF a U+FE70 až U+FEFF. Keď náhrada pristane na ID glyfu mimo tohto rozsahu, vstupné okno prejde nezmenené namiesto vydania niečoho, čo reader nedokáže adresovať; font-špecifické alternatívy na ľubovoľných ID glyfov potrebujú syntetický private-use code point alokovaný v U+E000 až U+F8FF, aby ich preniesol cez emit cestu. Takže poctivé zhrnutie je, že oprava 2.435.0 odstránila tvrdú prekážku namiesto dokončenia príbehu. Pred ňou mohol byť glyf správne tvarovaný, správne vydaný, a stále zmiznúť pri subsettingu, čo znamenalo, že shaping engine sa nedal dôverovať od začiatku do konca bez ohľadu na to, aké dobré boli jeho lookupy. Čo zostáva, je adresovateľnosť, a toto obmedzenie aspoň zlyhá viditeľne v bode emisie namiesto potichu v build kroku, ktorý beží po tom všetkom, čo ste sledovali. Pre emit stranu tej istej pipeline pozri sprievodcu arabským a RTL text shapingom v Delphi PDF
Font subsetting, GSUB engine a shaping komplexných písiem popísané tu sa dodávajú v štandardnej HotPDF Component pre Delphi a C++Builder; produktová stránka nesie kompletnú API referenciu pre Unicode font a shaping volania menované vyššie