A formázott glyphok .notdef dobozokként jelennek meg, amikor egy betűtípus-részhalmazoló csak a kibocsátott kódpontokból elérhető glyphokat tartja meg. A HotPDF, a Delphi és C++Builder natív VCL PDF komponense, pontosan ezzel a hibával rendelkezett a 2.435.0-s verzióig: az OpenType GSUB kimenet egy belső használati bitmapben lett rögzítve, amelyet a részhalmazoló kijelentett, hogy tiszteletben fog tartani, de valójában sosem olvasott ki
Ez más hiba, mint amit az EndDoc hiba, amely csendben letiltotta a betűtípus-részhalmazolást ír le. Az a hiba arról szólt, mikor fut a részhalmazolás a szerializációhoz képest, és teljesen letiltotta a részhalmazolást. Ez arról szól, mit tartalmaz a részhalmaz, amikor a részhalmazolás tökéletesen az ütemezés szerint fut. A pipeline a megfelelő pillanatban indul, a hatbetűs részhalmaz-előtag megjelenik a /BaseFont-on pontosan úgy, ahogy az ISO 32000-1 §9.6.4 megköveteli, a fájl kisebb lesz, minden latin oldal tisztán korrektúrázódik, és egy arab oldal üres téglalapok sorozataként jön ki. A sorrendi hibák hangosak, ha egyszer megnézed. A lezárási hibák örökre csendben maradnak, mert a részhalmaz strukturálisan érvényes, és csak a saját tagsági listájával kapcsolatban rossz
Miért jelennek meg a formázott glyphok .notdef-ként?
Mert a kódpontok halmaza, amelyet egy dokumentum kibocsát, nem ugyanaz, mint a glyphok halmaza, amelyet a dokumentum rajzol, és egy részhalmazoló, amely összekeveri a kettőt, eldob minden formázás által létrehozott glyphot. A szövegformázás egy logikai karaktersorozatot pozicionált glyphsorozattá alakít, és az egész célja, hogy olyan glyphokat állítson elő, amelyekre egyetlen bemeneti karakter sem képez le: egy arab medial heh, egy fi ligatúra, egy dévanágari konjunkt, egy kontextuális alternatíva, amelyet az rclt jellemző választott ki. Mindegyik olyan glyph-azonosító, amelyet egy GSUB keresés gyártott, nem olyan, amit a cmap tábla ad neked bármely karakterhez a sztringedben. Egy tisztán a cmap-ra épülő részhalmazoló ezért a rossz indexet járja be. Hűségesen megtart minden glyphot, amelyet a szöveg formázás előtt használhatott volna, és pontosan azokat a glyphokat dobja el, amelyeket a szöveg formázás után ténylegesen használ. A renderelő ezután kéri a beágyazott betűtípustól az 1847-es GID-et, a részhalmaz nullázta ezt a bejegyzést a loca-ban, és a 0-s glyph-index jön vissza helyette. A 0-s glyph-index az OpenType definíció szerint .notdef, ez az oka, hogy a hiba jele üres doboz, nem rossz betű vagy összeomlás. Semmi nincs rosszul formázva a PDF-ben; a betűtípus egyszerűen nem tartalmazza azt a glyphot, amelyet a tartalomfolyam kért
Kódpontok nem glyphok: egy részhalmaz három forrása
Egy helyes részhalmaz-lezárásnak egyesítenie kell három független forrást, mindegyiket saját akkumulátorral. Az első a kódpont-eredetű halmaz: a HotPDF felhalmozza a FUnicodeUsedCps-t, ahogy BMP karakterek kerülnek kibocsátásra, és a FUnicodeSmpUsed-et a szurrogátpárokon keresztül elért kiegészítő-síkú karakterekhez, majd mindegyiket a FUnicodeCpToGid-en keresztül egy glyph-azonosítóra képezi le. A második a formázás-eredetű halmaz, a GSUB helyettesítés által előállított glyph-azonosítók, a MarkUnicodeGlyphUsed és EnableShapingFeatureForSubset révén a FUnicodeExtraUsedGlyphs-ba rögzítve. A harmadik az összetett lezárás (composite closure): egy glyph, amelynek numberOfContours-a -1 a glyf-ben, komponens glyph-azonosítókból van összeállítva, és az összetett megtartása a komponenseinek eldobása mellett üres körvonalat eredményez, nem .notdef-et, ami vitathatóan még rosszabb, mert térköz-hibaként olvasódik
A HotPDF mindig kezelte az elsőt és a harmadikat. A BuildAndApplyUnicodeFontSubset, a részhalmazolási belépési pont, amelyet az EndDoc hív a szerializáció előtt, feltölti a használt-glyph tömböt a 0-s GID-vel, bejárja a BMP kódpontokat, bejárja az SMP használati listát, és átadja a tömböt egy részhalmaz-építőnek, amely belsőleg feloldja az összetett komponenseket. A második forrás meg volt írva, de sosem lett felhasználva, és mivel a három forrás eltérő tartalmon bukik el, a rés évekig rejtőzhet egy olyan kódbázisban, amelynek regressziós korpusza túlnyomórészt latin
A tömb, amelyet megírtak, és sosem olvastak ki
A szerződés három helyen volt dokumentálva, és egyiken sem lett tiszteletben tartva. A FUnicodeExtraUsedGlyphs deklarációja kimondta, hogy az EndDoc részhalmazoló egyesíti azt a kódpont-eredetű használattal; az ApplyArabicGSUBRefinement fejléc-kommentje megígérte, hogy minden kibocsátott helyettesítő GID átmegy a MarkUnicodeGlyphUsed-en, így a részhalmazoló behúzza a glyphot a beágyazott betűtípusba; ugyanez az ígéret szó szerint megjelenik az ApplyArabicGSUBContextualRefinement-en az rclt útvonalhoz. Mindkét hívó betartotta a saját felét. Egy grep minden hivatkozásra e mező körül nagyjából kilencven másodperc alatt rendezte a másik felet: egy deklaráció, egy SetLength allokáció a RegisterUnicodeTTF-en belül, és írások a két megjelölő rutinban. Egyetlen olvasás sem. Ez az a diagnózis, amelyet érdemes belsővé tenni, mert jóval e kódbázison túl is általánosít. Amikor egy mezőt több hívási pont ír, és egyik sem olvassa, az általa reprezentált funkció nem létezik, bármilyen alaposan is van kommentelve. A részhalmazoló 1. lépése elég kicsi ahhoz, hogy egy képernyőnyi legyen, és a rés nyilvánvaló, ha egyszer tudod, mit keress
// 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
Az egysoros javítás, és a glyphok saját kezű megjelölése
A javítás egy egyesítés, és a biztonsági érve a művelet irányából származik: csak bitet állít be, sosem töröl, így egyetlen olyan glyph sem, amely korábban túlélte a részhalmazt, nem kezdhet el kiesni
// 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;
Három tulajdonság teszi ezt alacsony kockázatú változtatássá egy betűtípus-motor újraírása helyett. Monoton, mint fentebb. Nincs hatása azokon a betűtípusokon, amelyek sosem formáztak semmit, mivel a FUnicodeExtraUsedGlyphs teljesen False marad, és egy csak-latin dokumentum bájtkimenete változatlan. És a 2. lépés előtt landol, így mindkét részhalmaz-építő örökli: a ritka (sparse) építő, amely megőrzi az eredeti GID-számozást, és a kompakt építő, a _BuildCompactSubsetTTF, amelyet a HotPDF PDF/A alatt választ ki, hogy a megtartott glyphokat sűrű tartományba számozza át, zsugorítsa a maxp.numGlyphs-t, és kibocsássa a régi-új leképezést az ISO 32000-1 §9.7.4.2 által megkövetelt /CIDToGIDMap streamként. Mindkettő belsőleg hívja a _TTFWalkCompositeClosure-t, így egy formázott glyph, amely éppen összetett, most magával húzza a komponenseit is. Az összetett lezárás sosem volt elromolva; egyszerűen sosem érte el ezeket a glyph-azonosítókat, mert azok nem voltak abban a halmazban, amit bejár. Ha közvetlenül vezérled a GSUB motort a beépített finomítási lépésekre támaszkodás helyett, a lezárás a te felelősséged, és minden helyettesítő glyph-azonosítónak, amit kibocsátasz, megjelölve kell lennie, mielőtt az EndDoc lefagyasztja a használt-glyph halmazt
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;
Az EnableShapingFeatureForSubset az egyedi GID-hívás kötegelt megfelelője, és szándékosan konzervatív. Bejárja a GSUB keresési listát azokért a keresésekért, amelyek egy négybetűs jellemzőcímkéhez vannak kötve a jelenleg kiválasztott script- és nyelvútvonal alatt, és megjelöli azokat a helyettesítő glyph-azonosítókat, amelyeket ezek a keresések előállíthatnak. Védelmi no-op, amikor a betűtípus nem hordoz GSUB táblát, vagy amikor a jellemző hiányzik abból az útvonalból, így feltétel nélküli hívása biztonságos. Szándékosan túl-becslés is: megtarthat olyan glyphokat, amelyeket egy adott dokumentum sosem rajzol meg. Részhalmazolásnál a túl-tartalmazás bájtokba kerül, az alul-tartalmazás pedig a helyességbe, ami könnyű cserévé teszi ezt. Ezeknek a kereséseknek a struktúráját, és a lefedettségi táblákat, amelyek eldöntik, mely glyphok vesznek részt, a tiszta Delphiben megvalósított GSUB stilisztikai alternatívák bemutatója tárgyalja
Hogyan bizonyítod, hogy a glyph tényleg benne van a részhalmazban?
A kibocsátott betűtípus olvasásával, nem egy olyan megjelenítőn való szemrevételezéssel, amely a háttérben rendszerbetűtípust helyettesíthet. Az ellenőrzés, amely elkapja ezt az egész hibaosztályt, mechanikus: húzd ki a /FontFile2 streamet a kimeneti PDF-ből, elemezd a loca-t, és erősítsd meg, hogy a várt glyph-azonosító nem-üres bejegyzést hordoz, azaz a kezdő és záró eltolásai különböznek. Egy üres bejegyzés azt jelenti, hogy a részhalmazoló úgy döntött, a glyph nem használt. Két szokás ezután sokkal nehezebbé teszi, hogy újra kiszállítsd a hibát. Tarts egy formázott script-oldalt az automatikus smoke-korpuszban, ne csak a kézi korrektúra-halmazban, mert az arab, dévanágari és khmer olyan lezárási útvonalakat gyakoroltat meg, amelyeket semennyi latin lefedettség nem ér el. És valahányszor egy akkumulátor létezik, biztosítsd, hogy valami felhasználja, mert egy csak-írható mező olyan funkció, amely lefordul, zöld tesztet ad a rossz korpuszon, és semmit sem csinál
Ahol a javítás megáll
A részhalmaz-lezárás szükséges egy formázott glyph megjelenítéséhez, de nem elégséges. A glyphnak elérhetőnek is kell lennie a tartalomfolyamból, ami egy külön probléma saját határral. A HotPDF beépített arab finomítási lépései csak akkor véglegesítenek egy helyettesítést, ha minden helyettesítő glyph-azonosító elérhető egy Unicode megjelenítési-forma kódponton keresztül egy fordított cmap-pásztázással nagyjából 690 kódponton át az U+FB50-U+FDFF és U+FE70-U+FEFF tartományokban. Amikor egy helyettesítés egy ezen a tartományon kívüli glyph-azonosítóra esik, az bemeneti ablak változatlanul megy tovább ahelyett, hogy olyasmit bocsátana ki, amit az olvasó nem tud megcímezni; a tetszőleges glyph-azonosítókon lévő betűtípus-specifikus alternatíváknak szintetikus, magánhasználatú kódpontra van szükségük, kiosztva az U+E000-U+F8FF tartományban, hogy átvigyék őket a kibocsátási útvonalon. Így az őszinte összefoglaló az, hogy a 2.435.0-s javítás egy kemény blokkolót távolított el, nem fejezte be a történetet. Előtte egy glyph formázható volt helyesen, kibocsátható helyesen, és mégis eltűnhetett a részhalmazolás idejére, ami azt jelentette, hogy a formázómotorban nem lehetett megbízni végponttól végpontig, bármilyen jók is voltak a keresései. Ami megmarad, az a megcímezhetőség, és ez a korlátozás legalább láthatóan hibázik a kibocsátás pontján, nem csendben egy build-lépésben, amely minden után fut, amit figyeltél. Ugyanennek a pipeline-nak a kibocsátási oldaláért lásd az arab és RTL szövegformázás útmutatóját Delphiben
Az itt leírt betűtípus-részhalmazolás, GSUB motor és komplex-script formázás a szabványos HotPDF Component-ban érkezik Delphihez és C++Builderhez; a termékoldal tartalmazza a fent megnevezett Unicode betűtípus- és formázási hívások teljes API-referenciáját