Tehnički članak

Zatvaranje subseta fonta: oblikovani glifovi izgubljeni u Delphi PDF-ovima

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

HotPDF vremenska linija oblikovane arapske ligature čiji glyph ID nikad ne ulazi u podskup fonta, pa render pada na .notdef
Oblikovanje proizvodi GID 1847 koji nijedan cmap unos ne izlistava, dok zatvaranje podskupa hodi samo kodnim tačkama. Podskup ostaje strukturno validan čak i dok stranica štampa prazne pravougaonike

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

// Korak 1: izvođenje skupa korišćenih glifova (kako je stajalo pre verzije 2.435.0)
SetLength(UsedGlyphs, FUnicodeNumGlyphs);
for I := 0 to FUnicodeNumGlyphs - 1 do
  UsedGlyphs[I] := False;
UsedGlyphs[0] := True;                       // .notdef je uvek prisutan

for Cp := 0 to $FFFF do                      // izvor 1a: BMP kod poeni
  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       // izvor 1b: SMP kod poeni
begin
  GID := FUnicodeSmpUsed[I].GID;
  if (GID > 0) and (GID < FUnicodeNumGlyphs) then
    UsedGlyphs[GID] := True;
end;

// izvor 2 je ovde nedostajao: ništa nikada nije konsultovalo 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: uvlačenje dodatnih glifova izvedenih iz GSUB-a u subset.
// MarkUnicodeGlyphUsed / EnableShapingFeatureForSubset beleže ID-eve glifova koje
// je oblikovanje proizvelo, ali na koje nijedan emitovan kod poen direktno ne mapira.
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);   // izostavite ovo i dobićete .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

Tri izvora Delphi PDF podskupa fonta - cmap kodni znakovi, GSUB shaping akumulator i kompozitna zatvorenost - objedinjeni bitovski u skup korišćenih glyph-ova pre oba graditelja podskupa
Jedna petlja unije dodata u 2.435.0 savija glif ID-ove proizvedene oblikovanjem u skup korišćenih pre nego što bilo koji graditelj podskupa radi, i pošto samo postavlja bitove, dokumenti samo-latinski ostaju bajt-identični

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 Delphi Component za Delphi i C++Builder; proizvodna stranica nosi kompletnu API referencu za Unicode font i pozive oblikovanja pomenute gore

HotPDF: Workflow provere koji izvlači FontFile2 tok iz PDF-a, parsira loca ofsete, i proverava da li je očekivani oblikovani glyph ID zadržao neprazan unos
Dokažite zatvorenost raščlanjivanjem emitovanog fonta umesto verovanja pregledaču koji može zameniti sistemske fontove. Pomeri koji se razlikuju dokazuju da su pravi bajtovi kontura preživeli podskupljanje