Oblikovani glifovi renderiraju se kao .notdef okviri kad podskupljivač fonta zadrži samo glifove dostupne iz emitiranih kôdnih točaka. HotPDF, nativna VCL PDF komponenta za Delphi i C++Builder, nosila je upravo tu manu do verzije 2.435.0: OpenType GSUB izlaz bio je zabilježen u internoj bitmapi korištenja koju je podskupljivač izjavio da će poštovati, a zapravo je nikad nije pročitao
Ovo je drukčiji kvar od onog opisanog u bugu EndDoc-a koji je tiho onemogućio podskupljivanje fonta. Taj bug se ticao kada se podskupljivanje pokretalo u odnosu na serijalizaciju, i u potpunosti je onemogućio podskupljivanje. Ovaj se tiče čega podskup sadrži kad se podskupljivanje pokrene savršeno prema rasporedu. Cjevovod se pokreće u pravom trenutku, šesteroslovni prefiks podskupa pojavljuje se na /BaseFont točno kako ISO 32000-1 §9.6.4 zahtijeva, datoteka se smanjuje, svaka latinična stranica ispada čisto, a arapska stranica izlazi kao red praznih pravokutnika. Bugovi u redoslijedu su glasni čim ih pogledate. Bugovi u zatvaranju ostaju tihi zauvijek, jer je podskup strukturno valjan i pogrešan samo u vlastitom popisu članstva
Zašto se oblikovani glifovi renderiraju kao .notdef?
Zato što skup kôdnih točaka koje dokument emitira nije skup glifova koje dokument crta, a podskupljivač koji poistovjećuje ta dva ispušta svaki glif koji je proizvelo oblikovanje. Oblikovanje teksta pretvara logički niz znakova u pozicionirani niz glifova, a njegova cijela svrha je proizvesti glifove koje nijedan pojedinačni ulazni znak ne mapira: medijalni arapski heh, ligatura fi, devanagari spoj, kontekstualna alternativa odabrana značajkom rclt. Svaki od njih je ID glifa koji je izradio GSUB lookup, a ne onaj koji vam cmap tablica daje za bilo koji znak u vašem nizu. Podskupljivač vođen isključivo cmap-om stoga prolazi kroz pogrešan indeks. Vjerno zadržava svaki glif koji je tekst mogao koristiti prije oblikovanja, a odbacuje upravo glifove koje tekst koristi nakon oblikovanja. Renderer zatim pita ugrađeni font za GID 1847, podskup je poništio taj unos u loca, i vraća se indeks glifa 0. Indeks glifa 0 je .notdef prema definiciji OpenType-a, zato je potpis kvara prazan okvir, a ne pogrešno slovo ili pad. Ništa u PDF-u nije loše oblikovano; font jednostavno ne sadrži glif koji je tok sadržaja tražio
Kôdne točke nisu glifovi: tri izvora podskupa
Ispravno zatvaranje podskupa mora unirati tri neovisna izvora, svaki sa svojim akumulatorom. Prvi je skup izveden iz kôdnih točaka: HotPDF akumulira FUnicodeUsedCps dok se emitiraju BMP znakovi i FUnicodeSmpUsed za znakove dopunske ravnine dosegnute preko surogatnih parova, zatim mapira svaki kroz FUnicodeCpToGid u ID glifa. Drugi je skup izveden iz oblikovanja, ID-ovi glifova koje je proizvela GSUB supstitucija, zabilježeni kroz MarkUnicodeGlyphUsed i EnableShapingFeatureForSubset u FUnicodeExtraUsedGlyphs. Treći je kompozitno zatvaranje: glif čiji je numberOfContours -1 u glyf sastavljen je od komponentnih ID-ova glifova, a zadržavanje kompozitnog dok se ispuštaju njegove komponente daje praznu konturu umjesto .notdef, što je vjerojatno gore jer se čita kao bug razmaka
HotPDF je oduvijek obrađivao prvi i treći izvor. BuildAndApplyUnicodeFontSubset, ulazna točka podskupljivanja koju EndDoc poziva prije serijalizacije, popunjava polje korištenih glifova s GID-om 0, prolazi kroz BMP kôdne točke, prolazi kroz popis korištenja SMP-a i predaje polje graditelju podskupa koji interno razrješava kompozitne komponente. Drugi izvor je bio napisan, ali nikad konzumiran, a budući da tri izvora ne uspijevaju na različitom sadržaju, taj se propust može skrivati godinama u bazi kôda čiji je regresijski korpus uglavnom latinični
Polje koje je bilo zapisano, a nikad pročitano
Ugovor je bio dokumentiran na tri mjesta, a poštovan ni na jednom. Deklaracija FUnicodeExtraUsedGlyphs navodila je da ga EndDoc podskupljivač unira s korištenjem izvedenim iz kôdnih točaka; komentar zaglavlja na ApplyArabicGSUBRefinement obećavao je da se svaki emitirani zamjenski GID prosljeđuje kroz MarkUnicodeGlyphUsed tako da ga podskupljivač povuče u ugrađeni font; isto obećanje pojavljuje se doslovno na ApplyArabicGSUBContextualRefinement za put rclt. Oba pozivatelja ispunila su svoju polovicu. Grep preko svake reference na to polje riješio je drugu polovicu za otprilike devedeset sekundi: jedna deklaracija, jedna SetLength alokacija unutar RegisterUnicodeTTF i zapisi u dvije funkcije označavanja. Nijedno čitanje. To je dijagnostika koju vrijedi usvojiti, jer se dobro generalizira izvan fontova. Kad je polje zapisano na više mjesta poziva, a čitano ni na jednom, značajka koju predstavlja ne postoji, koliko god temeljito bila komentirana. Korak 1 podskupljivača dovoljno je malen da se pročita na jednom ekranu, a propust je očit čim znate gdje ga tražiti
// 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
Popravak u jednoj petlji i ručno označavanje glifova
Popravak je unija, a njegov sigurnosni argument dolazi iz smjera operacije: postavlja samo bitove, nikad ih ne briše, pa nijedan glif koji je nekad preživio podskup ne može odjednom početi biti ispuštan
// 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 svojstva čine ovo niskorizičnom promjenom, a ne prepravkom motora fonta. Monotona je, kao gore. Bez učinka je na fontovima koji nikad ništa nisu oblikovali, budući da FUnicodeExtraUsedGlyphs ostaje potpuno False, a izlaz bajtova za dokument samo na latinici je nepromijenjen. I dolazi prije Koraka 2, pa je oba graditelja podskupa naslijede: rijetki graditelj koji čuva izvorno numeriranje GID-a, i kompaktni graditelj _BuildCompactSubsetTTF koji HotPDF bira pod PDF/A da ponovno numerira zadržane glifove u gust raspon, smanji maxp.numGlyphs i emitira staro-u-novo mapiranje kao tok /CIDToGIDMap koji zahtijeva ISO 32000-1 §9.7.4.2. Oba pozivaju _TTFWalkCompositeClosure interno, pa oblikovani glif koji je slučajno kompozitan sada također povlači svoje komponente. Kompozitno zatvaranje nikad nije bilo pokvareno; jednostavno nikad nije bilo dosegnuto za te ID-ove glifova, jer ID-ovi glifova nisu bili u skupu kroz koji ono prolazi. Ako sami vozite GSUB motor umjesto da se oslanjate na ugrađene prolaze pročišćavanja, zatvaranje postaje vaša odgovornost, a svaki zamjenski ID glifa koji emitirate mora biti označen prije nego EndDoc zamrzne skup korištenih glifova
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 serijski pandan pozivu s jednim GID-om, i namjerno je konzervativan. Prolazi kroz popis GSUB lookup-ova povezanih s jednom četveroznakovnom oznakom značajke pod trenutno odabranim putem skripte i jezika, i označava zamjenske ID-ove glifova koje ti lookup-ovi mogu proizvesti. Obrambeno je bez učinka kad font ne nosi GSUB tablicu ili kad značajka nije prisutna na tom putu, pa je pozivanje bez uvjeta sigurno. Također je namjerno prevelika procjena: može zadržati glifove koje dani dokument nikad ne crta. Za podskupljivanje, previše uključivanja košta bajtove, a premalo uključivanja košta točnost, što tu razmjenu čini lakom odlukom. Struktura tih lookup-ova, i tablice pokrivenosti koje odlučuju koji glifovi sudjeluju, obrađena je u pregledu GSUB stilskih alternativa u čistom Delphiju
Kako dokazati da je glif zapravo u podskupu?
Čitanjem emitiranog fonta, a ne bacanjem oka na stranicu u čitaču koji možda iza vaših leđa zamjenjuje sistemskim fontom. Provjera koja hvata cijelu ovu klasu bugova je mehanička: izdvojite tok /FontFile2 iz izlaznog PDF-a, parsirajte loca i potvrdite da ID glifa koji očekujete nosi neprazan unos, odnosno da se njegovi početni i krajnji pomaci razlikuju. Prazan unos znači da je podskupljivač odlučio da je glif nekorišten. Dvije navike tada uvelike olakšavaju da se ovaj kvar ponovno ne isporuči. Zadržite stranicu s oblikovanom skriptom u automatiziranom smoke korpusu, a ne samo u ručnom skupu za dokazivanje, jer arapski, devanagari i kmerski isprobavaju putove zatvaranja koje nikakva količina latinične pokrivenosti neće dotaknuti. I kad god akumulator postoji, tvrdite da nešto njime konzumira, jer je polje samo za pisanje značajka koja se kompajlira, testira zeleno na pogrešnom korpusu i ne radi ništa
Gdje se popravak zaustavlja
Zatvaranje podskupa nužno je da bi se oblikovani glif renderirao, i nije dovoljno. Glif također mora biti adresibilan iz toka sadržaja, što je zaseban problem sa svojom vlastitom granicom. Ugrađeni prolazi arapskog pročišćavanja u HotPDF-u obvezuju supstituciju samo kad je svaki zamjenski ID glifa dostupan kroz Unicode prezentacijsku kôdnu točku putem obrnutog cmap skena preko otprilike 690 kôdnih točaka u rasponu U+FB50 do U+FDFF i U+FE70 do U+FEFF. Kad zamjena sleti na ID glifa izvan tog raspona, ulazni prozor prolazi nepromijenjen umjesto da emitira nešto što čitač ne može adresirati; alternative specifične za font na proizvoljnim ID-ovima glifova trebaju sintetsku kôdnu točku privatne upotrebe dodijeljenu u U+E000 do U+F8FF da bi ih prenijele kroz put emitiranja. Pošten sažetak je stoga da je popravak u 2.435.0 uklonio tvrdu blokadu umjesto da je dovršio priču. Prije njega, glif se mogao ispravno oblikovati, ispravno emitirati, a ipak nestati u trenutku podskupljivanja, što je značilo da se motoru oblikovanja nije moglo vjerovati do kraja koliko god dobri bili njegovi lookup-ovi. Ono što ostaje je adresibilnost, a to ograničenje barem ne uspijeva vidljivo u trenutku emitiranja, umjesto tiho u koraku izgradnje koji se pokreće nakon svega što ste promatrali. Za stranu emitiranja istog cjevovoda, pogledajte vodič o arapskom i RTL oblikovanju teksta u Delphi PDF-ovima
Podskupljivanje fontova, GSUB motor i oblikovanje složenih skripti opisani ovdje isporučuju se u standardnom HotPDF Component za Delphi i C++Builder; produktna stranica sadrži potpunu API referencu za gore navedene pozive Unicode fonta i oblikovanja