Oblikovani glifovi se renderuju kao .notdef pravougaonici kada font subsetter zadrži samo glifove dostupne od emitovanih kod poena. HotPDF, nativna VCL PDF komponenta za Delphi i C++Builder, nosio je tačno taj defekt sve do verzije 2.435.0: OpenType GSUB izlaz se beležio u internu bitmapu upotrebe koju je subsetter izjavio da će poštovati, a zatim je nikada zapravo nije čitao
Ovo je drugačiji kvar od onog opisanog u EndDoc bagu koji je tiho onemogućavao subsetovanje fontova. Taj bag se ticao kada se subsetovanje izvršava u odnosu na serijalizaciju, i onemogućavao je subsetovanje u potpunosti. Ovaj se tiče šta subset sadrži kada se subsetovanje izvrši savršeno po rasporedu. Pipeline se okine u pravom trenutku, prefiks subseta od šest slova se pojavljuje na /BaseFont tačno kako ISO 32000-1 §9.6.4 zahteva, fajl postaje manji, svaka latinična stranica prolazi čisto, a arapska stranica izađe kao red praznih pravougaonika. Bagovi u redosledu su glasni čim pogledate. Bagovi zatvaranja ostaju tihi zauvek, jer je subset strukturno validan i pogrešan samo po pitanju sopstvene liste članstva
Zašto se oblikovani glifovi renderuju kao .notdef?
Zato što skup kod poena koje dokument emituje nije skup glifova koje dokument crta, a subsetter koji ta dva pomeša izbacuje svaki glif koji je proizvelo oblikovanje. Oblikovanje teksta pretvara logičku sekvencu karaktera u pozicioniranu sekvencu glifova, a čitava njegova svrha je da proizvede glifove na koje nijedan pojedinačan ulazni karakter ne mapira: arapsko medijalno heh, fi ligaturu, devanagari konjunkt, kontekstualnu alternativu izabranu funkcijom rclt. Svaki od njih je ID glifa koji je proizveo GSUB lookup, ne onaj koji vam cmap tabela daje za bilo koji karakter u vašem stringu. Subsetter vođen isključivo cmap-om zato obilazi pogrešan indeks. Verno zadržava svaki glif koji je tekst mogao koristiti pre oblikovanja i odbacuje upravo one glifove koje tekst koristi posle oblikovanja. Renderer zatim traži od ugrađenog fonta GID 1847, subset je tu stavku u loca obnulio, i vraća se indeks glifa 0 umesto toga. Indeks glifa 0 je .notdef po OpenType definiciji, zato je potpis kvara prazan pravougaonik, a ne pogrešno slovo ili pad programa. Ništa u PDF-u nije loše formirano; font jednostavno ne sadrži glif koji je tok sadržaja tražio
Kod poeni nisu glifovi: tri izvora subseta
Ispravno zatvaranje subseta mora da spoji tri nezavisna izvora, svaki sa sopstvenim akumulatorom. Prvi je skup izveden iz kod poena: HotPDF akumulira FUnicodeUsedCps dok se BMP karakteri emituju i FUnicodeSmpUsed za karaktere dopunske ravni dostupne preko surogat parova, zatim mapira svaki kroz FUnicodeCpToGid u ID glifa. Drugi je skup izveden iz oblikovanja, ID-evi glifova koje je proizvela GSUB supstitucija, zabeleženi kroz MarkUnicodeGlyphUsed i EnableShapingFeatureForSubset u FUnicodeExtraUsedGlyphs. Treći je kompozitno zatvaranje: glif čiji je numberOfContours -1 u glyf je sastavljen od ID-eva komponentnih glifova, a zadržavanje kompozita uz odbacivanje njegovih komponenata daje prazan obris umesto .notdef, što je diskutabilno gore jer se čita kao bag razmaka
HotPDF je uvek obrađivao prvi i treći. BuildAndApplyUnicodeFontSubset, ulazna tačka subsetovanja koju EndDoc poziva pre serijalizacije, zaseva niz korišćenih glifova sa GID 0, obilazi BMP kod poene, obilazi listu upotrebe SMP-a, i predaje niz builderu subseta koji interno razrešava kompozitne komponente. Drugi izvor je bio napisan, ali nikad konsumiran, a pošto ta tri izvora otkazuju na različitom sadržaju, propust može ostati skriven godinama u kodnoj bazi čiji je regresioni korpus uglavnom latiničan
Niz koji je bio zapisan, a nikad pročitan
Ugovor je bio dokumentovan na tri mesta, a poštovan nigde. Deklaracija FUnicodeExtraUsedGlyphs tvrdila je da EndDoc subsetter spaja taj niz sa upotrebom izvedenom iz kod poena; komentar u zaglavlju ApplyArabicGSUBRefinement obećavao je da se svaki emitovan zamenski GID prosleđuje kroz MarkUnicodeGlyphUsed tako da subsetter uvuče glif u ugrađeni font; isto obećanje se pojavljuje bukvalno na ApplyArabicGSUBContextualRefinement za rclt putanju. Oba pozivaoca su ispunila svoju polovinu. Grep preko svake reference na to polje rešio je drugu polovinu za oko devedeset sekundi: jedna deklaracija, jedna SetLength alokacija unutar RegisterUnicodeTTF, i zapisi u dve rutine za obeležavanje. Nijedno čitanje. To je dijagnoza vredna usvajanja, jer se dobro generalizuje daleko izvan fontova. Kada polje zapisuje nekoliko poziva, a ne čita ga nijedan, funkcija koju predstavlja ne postoji, koliko god temeljno bila komentarisana. Korak 1 subsettera je dovoljno kratak da se pročita na jednom ekranu, a propust je očigledan čim znate šta da tražite
// 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
Popravka u jednoj petlji, i ručno obeležavanje glifova
Popravka je unija, a njen argument bezbednosti dolazi iz smera operacije: postavlja samo bitove, nikad ih ne briše, pa nijedan glif koji je ranije preživljavao subset ne može sada početi da se izbacuje
// 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 osobine čine ovo izmenom niskog rizika, a ne prepravkom font engine-a. Monotona je, kao gore. No-op je na fontovima koji nikad ništa nisu oblikovali, jer FUnicodeExtraUsedGlyphs ostaje sav-False i izlaz u bajtovima za dokument koji je samo latiničan je nepromenjen. I slaže se pre Koraka 2, pa je nasleđuju oba buildera subseta: retki builder koji čuva originalnu numeraciju GID-a, i kompaktni builder _BuildCompactSubsetTTF koji HotPDF bira pod PDF/A da prenumerira zadržane glifove u gust opseg, smanji maxp.numGlyphs, i emituje mapiranje staro-u-novo kao tok /CIDToGIDMap koji zahteva ISO 32000-1 §9.7.4.2. Oba interno pozivaju _TTFWalkCompositeClosure, pa oblikovan glif koji je slučajno kompozitan sada takođe povlači svoje komponente. Kompozitno zatvaranje nikad nije bilo pokvareno; jednostavno nikad nije dosegnuto za te ID-eve glifova, jer ti ID-evi glifova nisu bili u skupu koji ono obilazi. Ako pokrećete GSUB engine direktno umesto da se oslanjate na ugrađene prolaze rafinacije, zatvaranje postaje vaša odgovornost, i svaki ID zamenskog glifa koji emitujete mora biti obeležen pre nego što EndDoc zamrzne skup korišćenih 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 batch pandan pozivu za pojedinačni GID, i namerno je konzervativan. Obilazi listu GSUB lookupova za lookupove povezane sa jednim četvorobajtnim tagom funkcije pod trenutno izabranom putanjom pisma i jezika, i obeležava ID-eve zamenskih glifova koje ti lookupovi mogu proizvesti. Defanzivno je no-op kada font ne nosi GSUB tabelu ili kada je funkcija odsutna na toj putanji, pa je bezuslovno pozivanje bezbedno. Takođe je namerno preterana aproksimacija: može zadržati glifove koje dati dokument nikad ne crta. Za subsetovanje, prekomerno uključivanje košta bajtove, a nedovoljno uključivanje košta ispravnost, što tu trampu čini lakom. Struktura ovih lookupova, i tabele pokrivenosti koje odlučuju koji glifovi učestvuju, pokrivena je u pregledu GSUB stilskih alternativa u čistom Delphiju
Kako dokazati da je glif zaista u subsetu?
Čitanjem emitovanog fonta, ne piljenjem u stranicu u čitaču koji možda iza vaših leđa zamenjuje sistemskim fontom. Provera koja hvata čitavu ovu klasu bagova je mehanička: izvucite tok /FontFile2 iz izlaznog PDF-a, parsirajte loca, i potvrdite da ID glifa koji očekujete nosi nepraznu stavku, što znači da se njeni početni i krajnji ofset razlikuju. Prazna stavka znači da je subsetter odlučio da glif nije korišćen. Dve navike zatim čine mnogo težim da se ovaj kvar ponovo isporuči. Držite stranicu oblikovanog pisma u automatizovanom smoke korpusu, ne samo u ručnom setu za proofing, jer arapski, devanagari, i kmerski vežbaju putanje zatvaranja koje nikakva količina latinične pokrivenosti neće dotaći. I kad god akumulator postoji, potvrdite da nešto to konsumira, jer polje samo-za-pisanje je funkcija koja se kompajlira, prolazi zeleno na pogrešnom korpusu, i ne radi ništa
Gde se popravka zaustavlja
Zatvaranje subseta je neophodno da bi se oblikovani glif renderovao, ali nije dovoljno. Glif takođe mora biti adresibilan iz toka sadržaja, što je odvojen problem sa sopstvenom granicom. Ugrađeni prolazi arapske rafinacije u HotPDF-u potvrđuju supstituciju samo kada je svaki ID zamenskog glifa dostupan kroz Unicode kod poen prezentacione forme putem povratnog cmap skena preko otprilike 690 kod poena u U+FB50 do U+FDFF i U+FE70 do U+FEFF. Kada zamena završi na ID-u glifa van tog opsega, ulazni prozor prolazi nepromenjen umesto da emituje nešto što čitač ne može adresirati; alternative specifične za font na proizvoljnim ID-evima glifova zahtevaju sintetički kod poen privatne upotrebe alociran u U+E000 do U+F8FF da bi ih preneo kroz emit putanju. Zato je iskren rezime da je popravka 2.435.0 uklonila tvrdu prepreku, a ne dovršila priču. Pre nje, glif je mogao biti ispravno oblikovan, ispravno emitovan, i i dalje nestati u trenutku subsetovanja, što je značilo da se shaping engine ne može verovati od početka do kraja, koliko god njegovi lookupovi bili dobri. Ono što ostaje je adresibilnost, a to ograničenje bar otkazuje vidljivo u trenutku emitovanja, a ne tiho u koraku builda koji se izvršava posle svega što ste posmatrali. Za stranu emitovanja istog pipelinea, pogledajte vodič za arapsko i RTL oblikovanje teksta u Delphi PDF-ovima
Subsetovanje fontova, GSUB engine, i oblikovanje kompleksnih pisama opisani ovde isporučuju se u standardnoj HotPDF Component za Delphi i C++Builder; proizvodna stranica nosi kompletnu API referencu za Unicode font i pozive oblikovanja pomenute gore