Tehnični članak

Zaprtje podnabora pisave: izgubljeni oblikovani znaki v Delphi PDF-jih

Oblikovani znaki se upodobijo kot polja .notdef, kadar podnabiralec pisave obdrži le znake, dosegljive iz izdanih kodnih točk. HotPDF, izvorna komponenta VCL PDF za Delphi in C++Builder, je natanko to napako nosil do različice 2.435.0: izhod OpenType GSUB je bil zabeležen v notranjo bitno mapo uporabe, za katero je podnabiralec izjavil, da jo bo upošteval, nato pa je nikoli dejansko prebral

To je drugačna napaka od tiste, opisane v hrošču EndDoc, ki je tiho onemogočil podnabor pisave. Tisti hrošč je zadeval kdaj se je podnabor izvedel glede na serializacijo in je podnabor v celoti onemogočil. Ta zadeva kaj podnabor vsebuje, ko podnabor teče popolnoma po urniku. Cevovod se sproži ob pravem trenutku, šeststrikovna predpona podnabora se pojavi na /BaseFont natanko tako, kot zahteva ISO 32000-1 §9.6.4, datoteka je manjša, vsaka latinska stran se korekturno bere čisto, arabska stran pa izpade kot vrsta praznih pravokotnikov. Napake vrstnega reda so glasne, takoj ko pogledaš. Napake zaprtja ostanejo tihe za vedno, ker je podnabor strukturno veljaven in narobe le glede lastnega seznama članstva

Zakaj se oblikovani znaki upodobijo kot .notdef?

Ker nabor kodnih točk, ki jih dokument izda, ni nabor znakov, ki jih dokument izriše, podnabiralec, ki oba zamenja, pa izpusti vsak znak, ki ga je proizvedlo oblikovanje. Oblikovanje besedila spremeni logično zaporedje znakov v pozicionirano zaporedje znakov, njegov celoten namen pa je proizvesti znake, ki jih noben posamezen vhodni znak ne preslika: arabska medialna heh, ligatura fi, devanagarski konjunkt, kontekstualna alternativa, ki jo izbere funkcija rclt. Vsak od njih je ID znaka, ki ga je izdelalo iskanje GSUB, ne pa tisti, ki vam ga preda tabela cmap za kateri koli znak v vašem nizu. Podnabiralec, voden izključno s cmap, torej prehodi napačen indeks. Zvesto obdrži vsak znak, ki bi ga besedilo lahko uporabilo pred oblikovanjem, in izpusti natanko tiste znake, ki jih besedilo uporablja po oblikovanju. Upodabljalnik nato vdelano pisavo prosi za GID 1847, podnabor pa je ta vnos v loca ničelil, zato se vrne indeks znaka 0. Indeks znaka 0 je po definiciji OpenType .notdef, zato je podpis napake prazno polje, ne pa napačna črka ali sesutje. Nič v PDF-ju ni napačno oblikovano; pisava preprosto ne vsebuje znaka, ki ga je vsebinski tok zahteval

Kodne točke niso znaki: trije viri podnabora

Pravilno zaprtje podnabora mora združiti tri neodvisne vire, vsakega s svojim akumulatorjem. Prvi je nabor, izpeljan iz kodnih točk: HotPDF akumulira FUnicodeUsedCps, ko se izdajo znaki BMP, in FUnicodeSmpUsed za znake dopolnilne ravnine, dosežene prek nadomestnih parov, nato pa vsakega preslika prek FUnicodeCpToGid v ID znaka. Drugi je nabor, izpeljan iz oblikovanja, ID-ji znakov, ki jih je proizvedla substitucija GSUB, zabeleženi prek MarkUnicodeGlyphUsed in EnableShapingFeatureForSubset v FUnicodeExtraUsedGlyphs. Tretji je sestavljeno zaprtje: znak, katerega numberOfContours je -1 v glyf, je sestavljen iz ID-jev sestavnih znakov, obdržanje sestavljenega znaka ob izpustitvi njegovih komponent pa da prazen obris namesto .notdef, kar je verjetno slabše, ker se bere kot napaka razmikanja

HotPDF je vedno obravnaval prvega in tretjega. BuildAndApplyUnicodeFontSubset, vstopna točka podnaboriranja, ki jo EndDoc pokliče pred serializacijo, seje polje uporabljenih znakov z GID 0, prehodi kodne točke BMP, prehodi seznam uporabe SMP in polje preda graditelju podnabora, ki interno razreši sestavljene komponente. Drugi vir je bil napisan, a nikoli porabljen, ker pa trije viri odpovedo pri različni vsebini, se lahko vrzel skriva leta v bazi kode, čigar regresijski korpus je večinoma latinski

Polje, ki je bilo zapisano in nikoli prebrano

Pogodba je bila dokumentirana na treh mestih in spoštovana na nobenem od njih. Deklaracija FUnicodeExtraUsedGlyphs je navajala, da ga podnabiralec EndDoc združi z uporabo, izpeljano iz kodnih točk; komentar glave na ApplyArabicGSUBRefinement je obljubljal, da je vsak izdan nadomestni GID posredovan skozi MarkUnicodeGlyphUsed, tako da podnabiralec znak potegne v vdelano pisavo; ista obljuba se dobesedno pojavi na ApplyArabicGSUBContextualRefinement za pot rclt. Oba klicatelja sta izpolnila svojo polovico. Grep čez vsako referenco na polje je drugo polovico razrešil v približno devetdesetih sekundah: ena deklaracija, ena dodelitev SetLength znotraj RegisterUnicodeTTF in zapisi v obeh označevalnih rutinah. Niti eno branje. To je diagnostika, vredna ponotranjenja, ker se dobro posplošuje daleč onkraj pisav. Kadar je polje zapisano na več klicnih mestih in prebrano na nobenem, funkcija, ki jo predstavlja, ne obstaja, ne glede na to, kako temeljito je komentirana. Korak 1 podnabiralca je dovolj majhen za branje na enem zaslonu, vrzel pa je očitna, takoj ko veš, da jo moraš iskati

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

Popravek z eno zanko in ročno označevanje znakov

Popravek je združitev, njegov varnostni argument pa izhaja iz smeri operacije: nastavlja le bite, nikoli jih ne počisti, zato noben znak, ki je prej preživel podnabor, ne more začeti biti izpuščen

// 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 lastnosti to naredijo za nizko tvegano spremembo namesto prepisa pisavnega pogona. Monotona je, kot zgoraj. Je brez učinka na pisavah, ki nikoli niso ničesar oblikovale, ker FUnicodeExtraUsedGlyphs ostane povsod False, izhod bajtov za dokument samo v latinici pa je nespremenjen. In pristane pred korakom 2, tako da jo podedujeta oba graditelja podnabora: redki graditelj, ki ohrani izvirno oštevilčenje GID, in kompaktni graditelj _BuildCompactSubsetTTF, ki ga HotPDF izbere pod PDF/A za preštevilčenje obdržanih znakov v gost razpon, skrčitev maxp.numGlyphs in izdajo preslikave stari-v-nov kot tok /CIDToGIDMap, ki ga zahteva ISO 32000-1 §9.7.4.2. Oba interno pokličeta _TTFWalkCompositeClosure, tako da oblikovan znak, ki je slučajno sestavljen, zdaj s seboj potegne tudi svoje komponente. Sestavljeno zaprtje nikoli ni bilo pokvarjeno; zanj preprosto nikoli ni bilo doseženo za te ID-je znakov, ker ID-ji znakov niso bili v naboru, ki ga prehodi. Če pogon GSUB vodite neposredno namesto da bi se zanašali na vgrajene prehode izpopolnjevanja, zaprtje postane vaša odgovornost, vsak nadomestni ID znaka, ki ga izdate, pa mora biti označen, preden EndDoc zamrzne nabor uporabljenih znakov

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 paketni protipol enoznakovnega klica in je namerno konzervativna. Prehodi seznam iskanj GSUB za tista iskanja, povezana z eno štiribajtno oznako funkcije pod trenutno izbrano potjo pisave in jezika, ter označi ID-je nadomestnih znakov, ki jih ta iskanja lahko proizvedejo. Je obrambno brez učinka, kadar pisava ne nosi tabele GSUB ali kadar funkcija na tisti poti ni prisotna, zato je klicanje brez pogojev varno. Je tudi namerno prevelika ocena: lahko obdrži znake, ki jih dani dokument nikoli ne izriše. Pri podnaboriranju prevelika vključenost stane bajte, premajhna vključenost pa stane pravilnost, kar to menjavo naredi enostavno. Struktura teh iskanj in tabele pokritja, ki odločajo, kateri znaki sodelujejo, sta obravnavani v sprehodu skozi slogovne alternative GSUB v čistem Delphiju

Kako dokažete, da je znak dejansko v podnaboru?

Z branjem izdane pisave, ne pa z ogledovanjem strani v pregledovalniku, ki morda za vašim hrbtom nadomešča sistemsko pisavo. Preverjanje, ki ulovi ta cel razred hrošča, je mehansko: izvlecite tok /FontFile2 iz izhodnega PDF-ja, razčlenite loca in potrdite, da ID znaka, ki ga pričakujete, nosi neprazen vnos, kar pomeni, da se njegova začetna in končna odmika razlikujeta. Prazen vnos pomeni, da se je podnabiralec odločil, da znak ni uporabljen. Dve navadi nato bistveno otežita ponovno pošiljanje te napake v izdajo. Ohranite stran z oblikovano pisavo v samodejnem korpusu preverjanja z dimom, ne pa le v ročnem korekturnem naboru, ker arabščina, devanagarščina in kmerščina sprožijo poti zaprtja, ki jih nobena količina latinske pokritosti ne bo dosegla. In kadarkoli akumulator obstaja, potrdite, da ga nekaj porabi, ker je polje samo za pisanje funkcija, ki se prevede, testira zeleno na napačnem korpusu in ne stori ničesar

Kje se popravek ustavi

Zaprtje podnabora je potrebno, da se oblikovan znak upodobi, ni pa zadostno. Znak mora biti tudi naslovljiv iz vsebinskega toka, kar je ločena težava s svojo lastno mejo. Vgrajeni prehodi arabskega izpopolnjevanja HotPDF uveljavijo substitucijo le, kadar je vsak nadomestni ID znaka dosegljiv prek kodne točke unicode predstavitvene oblike prek obratnega skeniranja cmap čez približno 690 kodnih točk v U+FB50 do U+FDFF in U+FE70 do U+FEFF. Kadar nadomestek pristane na ID znaka zunaj tega razpona, vhodno okno preide nespremenjeno namesto da bi izdalo nekaj, česar bralnik ne more nasloviti; alternative, specifične za pisavo, na poljubnih ID-jih znakov potrebujejo sintetično kodno točko zasebne rabe, dodeljeno v U+E000 do U+F8FF, da jih prenese skozi pot izdaje. Pošten povzetek je torej, da je popravek 2.435.0 odstranil trdo oviro, ne pa dokončal zgodbe. Pred njim se je znak lahko pravilno oblikoval, pravilno izdal in kljub temu izginil ob podnaboriranju, kar je pomenilo, da pogonu oblikovanja ni bilo mogoče zaupati od začetka do konca, ne glede na to, kako dobra so bila njegova iskanja. Kar ostane, je naslovljivost, ta omejitev pa vsaj vidno odpove v točki izdaje, ne pa tiho v koraku gradnje, ki teče po vsem, kar ste opazovali. Za stran izdaje istega cevovoda glejte vodnik po oblikovanju arabskega in RTL besedila v Delphi PDF-jih

Podnabor pisave, pogon GSUB in oblikovanje kompleksnih pisav, opisani tukaj, so izdani v standardni HotPDF Component za Delphi in C++Builder; stran izdelka nosi celoten referenčni opis API-ja za zgoraj poimenovane klice unicode pisave in oblikovanja