Šriftuoti glifai atvaizduojami kaip .notdef stačiakampiai, kai šrifto poaibio kūrėjas palieka tik glifus, pasiekiamus iš išleistų kodo taškų. HotPDF, natyvus VCL PDF komponentas Delphi ir C++Builder, turėjo lygiai tą defektą iki 2.435.0 versijos: OpenType GSUB išvestis buvo įrašoma vidiniame naudojimo bitų masyve, kurį poaibio kūrėjas deklaravo, jog paisys, o paskui niekada faktiškai neskaitė
Tai kitokia nesėkmė nei aprašyta EndDoc klaidoje, tyliai išjungusioje šrifto poaibio kūrimą. Ta klaida buvo apie kada poaibio kūrimas vykdomas serializacijos atžvilgiu, ir ji visiškai išjungė poaibio kūrimą. Ši yra apie ką poaibis turi, kai poaibio kūrimas vyksta puikiai pagal grafiką. Konvejeris suveikia teisingu momentu, šešių raidžių poaibio priešdėlis pasirodo /BaseFont lygiai taip, kaip reikalauja ISO 32000-1 §9.6.4, failas sumažėja, kiekvienas lotyniškas puslapis švariai atsispausdina, o arabiškas puslapis išeina kaip tuščių stačiakampių eilutė. Tvarkos klaidos garsios, kai tik pažvelgi. Uždarumo klaidos lieka tylios amžinai, nes poaibis yra struktūriškai galiojantis ir klysta tik dėl savo pačios narystės sąrašo
Kodėl šriftuoti glifai atvaizduojami kaip .notdef?
Todėl, kad dokumento išleidžiamų kodo taškų aibė nėra dokumento piešiamų glifų aibė, o poaibio kūrėjas, sumaišantis abu, atmeta kiekvieną šriftavimo sukurtą glifą. Teksto šriftavimas paverčia loginę simbolių seką pozicionuota glifų seka, ir viso jo tikslas yra sukurti glifus, kurių joks vienas įvesties simbolis neatvaizduoja: arabiškas medialinis heh, fi ligatūra, devanagario konjunktas, kontekstinis alternatyvas, pasirinktas rclt savybės. Kiekvienas iš jų yra glifo ID, kurį sukūrė GSUB paieška, ne toks, kurį cmap lentelė duoda bet kuriam jūsų eilutės simboliui. Poaibio kūrėjas, valdomas vien cmap, todėl eina neteisingu indeksu. Jis ištikimai išsaugo kiekvieną glifą, kurį tekstas būtų galėjęs naudoti prieš šriftavimą, ir atmeta būtent tuos glifus, kuriuos tekstas naudoja po šriftavimo. Atvaizduotojas tada paprašo įdėto šrifto GID 1847, poaibis tą įrašą loca lentelėje nustatė nuliu, ir grįžta glifo indeksas 0. Glifo indeksas 0 yra .notdef pagal OpenType apibrėžimą, todėl nesėkmės ženklas yra tuščias stačiakampis, ne neteisinga raidė ar avarija. Niekas PDF faile nesuformuota blogai; šriftas tiesiog neturi glifo, kurio prašė turinio srautas
Kodo taškai nėra glifai: trys poaibio šaltiniai
Teisingas poaibio uždarumas turi sujungti tris nepriklausomus šaltinius, kiekvieną su savo kaupikliu. Pirmasis yra kodo taško kildinama aibė: HotPDF kaupia FUnicodeUsedCps, kai išleidžiami BMP simboliai, ir FUnicodeSmpUsed papildomos plokštumos simboliams, pasiektiems per surogatų poras, tada susieja kiekvieną per FUnicodeCpToGid su glifo ID. Antrasis yra šriftavimo kildinama aibė, GSUB pakeitimo sukurti glifų ID, įrašomi per MarkUnicodeGlyphUsed ir EnableShapingFeatureForSubset į FUnicodeExtraUsedGlyphs. Trečias yra sudėtinio glifo uždarumas: glifas, kurio numberOfContours yra -1 glyf lentelėje, surenkamas iš komponentų glifų ID, ir palikus sudėtinį, bet atmetus jo komponentus, gaunamas tuščias kontūras, o ne .notdef, kas galbūt netgi blogiau, nes atrodo kaip tarpo klaida
HotPDF visada tvarkė pirmąjį ir trečiąjį. BuildAndApplyUnicodeFontSubset, poaibio kūrimo įėjimo taškas, kurį EndDoc iškviečia prieš serializaciją, užpildo naudojamų glifų masyvą su GID 0, eina per BMP kodo taškus, eina per SMP naudojimo sąrašą ir perduoda masyvą poaibio kūrėjui, kuris viduje išsprendžia sudėtinius komponentus. Antras šaltinis buvo parašytas ir niekada nenaudotas, ir kadangi trys šaltiniai žlunga su skirtingu turiniu, spraga gali slėptis metų metus kodo bazėje, kurios regresijos rinkinys daugiausia lotyniškas
Masyvas, kuris buvo parašytas ir niekada neperskaitytas
Sutartis buvo dokumentuota trijose vietose ir nesilaikyta nė vienoje. FUnicodeExtraUsedGlyphs deklaracija teigė, kad EndDoc poaibio kūrėjas jį sujungia su kodo taško kildinamu naudojimu; antraštės komentaras ApplyArabicGSUBRefinement pažadėjo, kad kiekvienas išleistas pakeitimo GID perduodamas per MarkUnicodeGlyphUsed, kad poaibio kūrėjas įtrauktų glifą į įdėtą šriftą; tas pats pažadas pasikartoja žodis žodin ApplyArabicGSUBContextualRefinement dėl rclt kelio. Abi iškvietėjos pusės savo dalį atliko. Grep per kiekvieną nuorodą į lauką kitą pusę išsprendė per maždaug devyniasdešimt sekundžių: viena deklaracija, vienas SetLength paskirstymas viduje RegisterUnicodeTTF ir įrašai dviejose žymėjimo procedūrose. Nė vieno skaitymo. Tai diagnostika, kurią verta įsisavinti, nes ji gerai apibendrinama toli už šriftų ribų. Kai laukas rašomas keliose iškvietimo vietose ir neskaitomas niekur, funkcija, kurią jis atstovauja, neegzistuoja, kad ir kaip nuosekliai ji komentuota. 1 žingsnis poaibio kūrėjo yra pakankamai mažas, kad tilptų viename ekrane, ir spraga akivaizdi, kai tik žinai, kur ieškoti
// 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
Vieno ciklo taisymas ir glifų žymėjimas pačiam
Taisymas yra sujungimas, o jo saugumo argumentas kyla iš operacijos krypties: jis tik nustato bitus, niekada jų neišvalo, todėl joks glifas, kuris anksčiau išgyvendavo poaibio kūrimą, negali staiga pradėti būti atmetamas
// 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;
Trys savybės daro tai mažos rizikos pakeitimu, o ne šrifto variklio perrašymu. Tai monotoniška, kaip aukščiau. Tai niekinis veiksmas šriftams, kurie niekada nieko nešriftavo, nes FUnicodeExtraUsedGlyphs lieka visiškai False, o baitų išvestis grynai lotyniškam dokumentui nepasikeičia. Ir tai nusileidžia prieš 2 žingsnį, todėl abu poaibio kūrėjai jį paveldi: retas kūrėjas, išsaugantis originalią GID numeraciją, ir kompaktiškas kūrėjas _BuildCompactSubsetTTF, kurį HotPDF renkasi pagal PDF/A, kad pernumeruotų išlaikytus glifus į tankų intervalą, sumažintų maxp.numGlyphs ir išleistų sen-į-naują susiejimą kaip /CIDToGIDMap srautą, reikalaujamą ISO 32000-1 §9.7.4.2. Abu iškviečia _TTFWalkCompositeClosure viduje, todėl šriftuotas glifas, kuris atsitiktinai yra sudėtinis, dabar taip pat įtraukia savo komponentus. Sudėtinis uždarumas niekada nebuvo sugadintas; jis tiesiog niekada nebuvo pasiektas šiems glifų ID, nes glifų ID nebuvo aibėje, kurią jis eina. Jei valdote GSUB variklį tiesiogiai, o ne remiatės įtaisytais pagerinimo perėjimais, uždarumas tampa jūsų atsakomybe, ir kiekvienas pakeitimo glifo ID, kurį išleidžiate, turi būti pažymėtas prieš EndDoc užšaldant naudojamų glifų aibę
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 yra vienetinio GID iškvietimo paketinis atitikmuo, ir jis sąmoningai konservatyvus. Jis eina per GSUB paieškų sąrašą, ieškodamas paieškų, susietų su vienu keturių baitų savybės žymeniu pagal šiuo metu pasirinktą scenarijaus ir kalbos kelią, ir pažymi pakeitimo glifų ID, kuriuos tos paieškos gali sukurti. Tai apsauginis niekinis veiksmas, kai šriftas neturi GSUB lentelės arba kai savybė nedalyvauja tame kelyje, todėl jos besąlygiškas iškvietimas yra saugus. Tai taip pat yra tyčinis per-įtraukimas: jis gali išlaikyti glifus, kurių konkretus dokumentas niekada nepiešia. Poaibio kūrimui per-įtraukimas kainuoja baitus, o nepakankamas įtraukimas kainuoja teisingumą, kas šį mainą padaro lengvą. Šių paieškų struktūra ir aprėpties lentelės, sprendžiančios, kurie glifai dalyvauja, aprašytos GSUB stilistinių alternatyvų apžvalgoje grynoje Delphi
Kaip įrodyti, kad glifas iš tikrųjų yra poaibyje?
Perskaitant išleistą šriftą, ne stebint puslapį peržiūros programoje, kuri gali už jūsų nugaros pakeisti sistemos šriftu. Patikra, gaudanti visą šią klaidų klasę, yra mechaninė: ištraukite /FontFile2 srautą iš išvesties PDF, nagrinėkite loca ir patvirtinkite, kad tikimasi glifo ID turi netuščią įrašą, tai yra jo pradžios ir pabaigos poslinkiai skiriasi. Tuščias įrašas reiškia, kad poaibio kūrėjas nusprendė, jog glifas nenaudojamas. Du įpročiai tada gerokai apsunkina šios klaidos pakartotinį pristatymą. Laikykite šriftuoto scenarijaus puslapį automatizuotame smoke rinkinyje, ne tik rankiniame korektūros rinkinyje, nes arabų, devanagari ir khmerų kalbos naudoja uždarumo kelius, kurių jokia lotyniškos aprėpties apimtis nepalies. Ir kada tik egzistuoja kaupiklis, patvirtinkite, kad kažkas jį naudoja, nes tik rašomas laukas yra funkcija, kuri kompiliuojasi, testai žaliai praeina su neteisingu duomenų rinkiniu ir nieko nedaro
Kur taisymas sustoja
Poaibio uždarumas yra būtinas, kad šriftuotas glifas atsivaizduotų, bet nepakankamas. Glifas taip pat turi būti pasiekiamas iš turinio srauto, o tai atskira problema su savo riba. Įtaisyti HotPDF arabų pagerinimo perėjimai atlieka pakeitimą tik tada, kai kiekvienas pakeitimo glifo ID pasiekiamas per Unicode prezentacijos formos kodo tašką per atvirkštinį cmap skenavimą per apytiksliai 690 kodo taškų nuo U+FB50 iki U+FDFF ir U+FE70 iki U+FEFF. Kai pakeitimas nusileidžia ant glifo ID už to intervalo, įvesties langas praeina nepakeistas, o ne išleidžia kažką, ko skaitytuvas negali adresuoti; šriftui specifiniai alternatyvai savavaliuose glifų ID reikalauja sintetinio privataus naudojimo kodo taško, paskirto U+E000 iki U+F8FF, kad juos perneštų per išleidimo kelią. Todėl sąžininga santrauka yra ta, kad 2.435.0 pataisymas pašalino kietą blokatorių, o ne užbaigė istoriją. Prieš jį glifas galėjo būti teisingai šriftuotas, teisingai išleistas ir vis tiek išnykti poaibio kūrimo metu, o tai reiškė, kad šriftavimo variklis negalėjo būti patikimas nuo pradžios iki galo, kad ir kokios geros būtų jo paieškos. Kas lieka, tai adresuojamumas, ir tas apribojimas bent jau žlunga matomai išleidimo taške, o ne tyliai kūrimo žingsnyje, vykstančiame po visko, ką stebėjote. Dėl to paties konvejerio išleidimo pusės žr. gidą apie arabų ir RTL teksto šriftavimą Delphi PDF failuose
Šrifto poaibio kūrimas, GSUB variklis ir sudėtingo scenarijaus šriftavimas, aprašyti čia, pristatomi standartiniame HotPDF Component, skirtame Delphi ir C++Builder; produkto puslapyje pateikiama pilna Unicode šrifto ir šriftavimo iškvietimų API nuoroda, pavadintų aukščiau