A HotPDF a beágyazott PDF-betűtípusokat telepítés nélkül rendereli le Delphi-ben: az HPDFGlyphRender.pas egységben található beágyazott glyph-renderelő folyamat értelmezi a magában a PDF-ben tárolt betűkészlet-programokat – a FontFile2-ből származó TrueType glyf körvonalakat, a FontFile3-ból származó CFF Type 2 charstringeket és a Type 3 glyph tartalomfolyamokat –, és kitöltött GDI vektorútvonalként játssza le őket. Ez a cikk a PDF-oldalak TBitmap-be történő renderelése a HotPDF segítségével című cikk mögötti betűtípus-hűséget bemutató mélymerülés: az a rész a renderelőt egészében tárgyalja, ez a rész pedig azt mutatja be, hogyan kapja meg az oldalakon lévő szöveg a pontos alakját.
A renderelt PDF kimenetben megjelenő dobozok, üres helyek vagy kissé hibás karakterek szinte mindig azt jelentik, hogy a renderelő az operációs rendszertől kért egy betűtípust, ahelyett, hogy a fájlba ágyazottat használta volna. A panasz minden alkalommal ugyanúgy jelentkezik: a dokumentum tökéletesen néz ki azon a gépen, amelyen készült, de amikor egy ügyfél megnyitja egy tiszta szerveren vagy egy zárolt asztali számítógépen, a japán számlán tofu négyzetek jelennek meg, vagy a helyettesítő, hasonló kinézetű betűtípus eltolja az összes sortörést. A betűtípusok soha nem voltak azon a gépen – csak a PDF-ben –, és a rendszerszintű betűtípus-helyettesítésnél megálló renderelő nem látja őket. A részhalmazos (subset) betűtípusok csak rontanak a helyzeten: a részhalmaz negyven glyph-et tartalmazhat az adott fájlhoz privát módon hozzárendelt karakterkódok alatt, amelyekkel egyetlen telepített betűtípus sem rendelkezik.
Az ISO 32000-1 §9.9 szabvány három hordozót határoz meg a betűleíróban (font descriptor) lévő beágyazott betűtípus-programhoz: a FontFile az eredeti Type 1 programot, a FontFile2 a TrueType programot, a FontFile3 pedig egy tiszta CFF (Type1C vagy CIDFontType0C) fájlt vagy egy OpenType burkolót tartalmaz. A negyedik változat, a Type 3 betűtípus az ISO 32000-1 §9.6.5 szerint egyáltalán nem ágyaz be semmilyen bináris állományt – minden egyes glyph egy kis helyben végrehajtott PDF tartalomfolyam. A három hordozó eltér a körvonal-matematikában (másodfokú B-spline-ok versus harmadfokú charstringek versus tetszőleges oldaloperátorok), így a pontos rendereléshez mindegyikhez külön értelmezőre van szükség, valamint egy kódolási rétegre, amely a karakterkódokat a megfelelő glyph-indexre fordítja, még mielőtt a körvonalhoz hozzányúlnánk.
Hogyan konvertálja a HotPDF a TrueType glyf GDI útvonalakká?
A THPDFEmbeddedTTF a HPDFGlyphRender.pas fájlban beolvassa a loca táblát az egyes glyph rekordok megkereséséhez, pontról pontra végigjárja a glyf kontúrokat, és GDI útvonalat bocsát ki. Két TrueType konvenció igényel kifejezett kezelést. Először is, az egymást követő görbén kívüli (off-curve) pontok egy görbén lévő (on-curve) pontot feltételeznek a felezőpontjukban, és a csupa görbén kívüli pontból álló kontúr az utolsó és az első pontjának felezőpontjában kezdődik – ha bármelyik szabályt kihagyjuk, a kerekített glyph-ek lapos felületeket kapnak vagy összeomlanak. Másodszor, a TrueType görbék másodfokú Bézierek, míg a GDI PolyBezierTo harmadfokúakat fogad, így minden másodfokú szegmensnek pontosan megemelkedik a fokszáma (degree-elevated), ahelyett, hogy egyenes vonalú szegmensekre laposodna.
// Pontos fokszám-emelés: másodfokú (P0, Q, P2) -> harmadfokú (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// majd PolyBezierTo a C1, C2, P2 pontokkal — geometrikusan azonos görbe
A fokszám-emelés veszteségmentes: a harmadfokú görbe teljesen azonos ívet ír le, így a renderelt körvonal megegyezik azzal, amit egy szabványos megjelenítő rajzol ugyanabból a táblából, bármilyen nagyításban. A fennmaradó munka a pozicionálás. Minden glyph font egységekben (jellemzően 1000 vagy 2048 units-per-em rácsban) van definiálva, és a renderelő a skálázási mátrixot, a szöveg-mátrixot és az aktuális transzformációs mátrixot (CTM) egyetlen glyph-eszköz transzformációvá komponálja az útvonal kitöltése előtt. A sorrend itt sokkal fontosabb, mint amilyennek látszik: ha ugyanazt a három mátrixot fordítva komponáljuk meg, minden egyes glyph összeomlik a kezdőpont felé – ez egy rosszul kinéző oldal, amelynek tényleges hibája mindössze egyetlen sornyi mátrixalgebra.
Hogyan kezeli a Type 2 charstring-értelmező a CFF betűtípusokat?
A THPDFEmbeddedCFF valódi Type 2 charstring-értelmezőt biztosít a FontFile3 programoknak: elemzi a CFF INDEX struktúrákat, a Top DICT-et és a Private DICT-et, majd végrehajtja az egyes charstringeket és közvetlenül a GDI-nek bocsátja ki az útvonal-szegmenseket. Az OpenType burkolót (az OTTO konténert) először lefejti a tiszta CFF tábla eléréséhez; a csupasz CIDFontType0C és Type1C folyamokat közvetlenül használja fel. A charstringek egy tömör verem-alapú nyelv (stack language), és három konvenciója dönti el, hogy az értelmező szinkronban marad-e a bájtfolyammal. Az opcionális szélességi előtag azt jelenti, hogy az első veremürítő operátor tartalmazhat egy extra vezető operandust. A hintmask operátor vstemhm-et feltételez, amikor az operandusok még a vermen vannak, és a kihagyandó maszkbájtok száma a felhalmozott stem számlálójától függ – ha a számlálót egyszer elrontja a rendszer, minden ezt követő opkód félreolvasásra kerül. A szubrutin-hívások pedig a keresés előtt egy eltolást (bias) adnak hozzá az indexükhöz (szubrutinszámtól függően 107-et, 1131-et vagy 32768-at), így az eltolás nélküli hívás teljesen rossz szubrutinhoz vezet.
A CID-kulcsos CFF egy olyan áttételt ad hozzá, amely megzavarhatja a naiv megvalósításokat: a karakterkód kiválaszt egy CID-t, de a charstring index egy GID, és a betűtípus karakterkészlete (charset) a GID-et a CID-re képezi le – így a renderelő a rajzolás előtt felépíti a fordított CID-GID leképezést, és az FDSelect segítségével választja ki a glyph-enkénti Private DICT-et a többet tartalmazó betűtípusokhoz. A név-kulcsos Type1C programok, amelyek a klasszikus Type 1 betűtípusok szokásos hordozói, ehelyett egybájtos kódokat oldanak fel a CFF program beépített kódolásán vagy a következő részben leírt PDF-szintű kódolási mechanizmuson keresztül. Egy őszinte figyelmeztetés: az értelmező beolvassa a hint operátorokat a folyam szinkronban tartása érdekében, de a hintinget nem hajtja végre, amely korlátozást a cikk végén részletezzük.
Mi az a Type 3 betűtípus, és hogyan rajzolódik ki?
A Type 3 glyph egyáltalán nem körvonal – az ISO 32000-1 §9.6.5 tartalomfolyamként (content stream) határozza meg, így a HotPDF a grafikai állapot elmentésével, a betűmátrixnak, a betűméretnek és a szövegmátrixnak a CTM-re való rávetítésével, valamint a glyph-eljárásnak a lapokat rajzoló operátor-értelmezőn keresztüli végrehajtásával rendereli le, a betűtípus saját /Resources hatókörében. A helyesség szempontjából két részlet fontos a specifikációban. A Type 3 /Widths szélességek a glyph-térben fejeződnek ki a minden más betűtípus által használt 1/1000-es szövegtér helyett, így a léptetéseknek át kell menniük a /FontMatrix-en – a 0,01-es mátrixú vonalkód-betűtípusok különben egy nagyságrenddel rosszabbul lépnének. Továbbá a d1 operátorral megnyíló glyph-eljárás két olyan ígéretet tesz, amelyet a renderelő betartat: a rajzolás a deklarált befoglaló keretre (bounding box) van korlátozva, és az ISO 32000-1 §9.6.5.2 szerint a glyph figyelmen kívül hagyja a saját színoperátorait, és a hívó aktuális kitöltési színével fest, így a rg, g, k és azok körvonalazó párjai elnyomásra kerülnek az adott glyph időtartamára. Ha kihagyja a színszabályt, az oldalon kék színnel pecsételt d1 vonalkód-betűtípus feketén jelenik meg; ha kihagyja a korlátozást, a hibás formátumú glyph a celláján kívülre rajzol.
Hogyan válnak a karakterkódok glyph ID-kká?
A körvonal-értelmezők csak a munka felét jelentik, mivel a PDF szövegláncban lévő bájtok karakterkódok, nem pedig glyph-indexek, és az ISO 32000-1 két alfejezetet szentel a leképezésnek. Egyszerű betűtípusok esetén a §9.6.6 szigorú prioritást ír elő: a /Differences tömb felülírja az alapértelmezett kódolást (WinAnsiEncoding, MacRomanEncoding vagy StandardEncoding), ami pedig felülírja magának a betűtípus-programnak a térképét. A HotPDF ezt a láncot egy 256 bejegyzésből álló kód-GID táblázattá oldja fel, a glyph-neveket három úton fordítva le glyph-indexekké: pontos karakterkészlet-egyezés a CFF programon belül, numerikus gNN/glyphNN nevek mint szó szerinti indexek, valamint az Adobe Glyph List név-Unicode fordítás, amelyet egy cmap keresés követ a TrueType programoknál. Összetett betűtípusok esetén a §9.7 a CIDToGIDMap-et helyezi irányításba: a gyakori eset az /Identity, de a bejegyzés lehet egy CID szerint indexelt big-endian párokból álló folyam is – és a HotPDF saját Unicode kimenete pontosan ezt a folyam-formátumot használja a tömör részhalmazokhoz, így a folyam-útvonal nem egy ritka határeset.
// /CIDToGIDMap folyamként: CID szerint indexelt big-endian Word párok
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // a tartományon kívüli érték a .notdef-re képződik le
Amikor TrueType cmap keresésre van szükség, a HotPDF egy tartalék láncot jár végig, ahelyett, hogy egyetlen rész-táblázatban bízna: a Windows Unicode rész-táblázatok (format 4, majd format 12 a kiegészítő síkokhoz) állnak az első helyen, majd a (3,0) szimbólum rész-táblázat következik az F000 magáncélú terület (private-use-area) konvenciójával az alsó bájtig tükrözve – ez az oka annak, hogy egy olyan szimbólum-betűtípus, mint a Wingdings, válaszol a sima ASCII kódokra –, végül pedig a régebbi format 6 és format 0. A format 2 rész-táblázatok szándékosan nincsenek értelmezve: ezek régebbi több-bájtos kódlapokat képeznek le, mint a Shift-JIS és a Big5, nem pedig Unicode-ot, és a modern CJK betűtípusok amúgy is kivétel nélkül tartalmaznak format 4 vagy format 12 rész-táblát. Bármely kód, amely a fenti utak egyikén sem felel meg, visszatér a GDI rajzoláshoz ennél az egyetlen glyph-nél, így egy leképezhetetlen karakter csak egyetlen glyph-et ront el, nem pedig a teljes szövegfutamot.
Amit a beágyazott útvonal nem tesz meg
A határokat érdemes nyíltan kimondani. A hinting nem kerül végrehajtásra – a körvonalak úgy kerülnek kitöltésre, ahogy meg vannak írva, ami 150 DPI és a felett megkülönböztethetetlen a hintingelt kimenettől, de nagyon kis méretben egy képponttal eltérhet a hintingelt raszterezőtől. A FontFile-ban lévő eredeti Type 1 programok (eexec-titkosított charstringek) nem kerülnek értelmezésre, és az OpenType változó betűtípusok (variable-font) tengelyei sem érvényesülnek; mindkét eset, csakúgy, mint a sérült betűtípus-program vagy a használható cmap nélküli glyf tábla, a rendszerszintű rajzolásra esik vissza ahelyett, hogy meghiúsítaná az oldal renderelését. Ugyanez a hűség-központú megközelítés érvényes a renderelő többi részén is – az axiális és radiális árnyékolási minták ugyanazt a kezelést kapják, mint amit a színátmenetek érdemelnek –, és a generáló oldalon is megvan a maga betűtípus-finomsága abban, ahogyan az EndDoc rendezi a betű-részhalmazokat.
A folyamat használata egyáltalán nem igényel betűtípus-specifikus kódot – a fenti mechanizmusok mindegyike automatikusan bekapcsol az oldal-renderelési hívás során.
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// A beágyazott TrueType, CFF és Type 3 betűtípusok magából
// a fájlból renderelődnek — semmit sem kell telepíteni erre a gépre
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
A gyakorlati következmény az, ami a támogatási postaládáját érdekli: a betűtípusait hordozó PDF ezekkel a betűtípusokkal renderelődik egy build szerveren, egy Windows konténerben vagy az ügyfél asztalán, amely még soha nem látta az adott betűképet. A beágyazott glyph-renderelő folyamat a Delphi és C++Builder rendszerekhez készült HotPDF Component részeként érkezik – egy natív VCL könyvtár, amely lefedi a PDF létrehozását, szerkesztését, szöveg kinyerését és oldal-renderelést külső DLL függőségek nélkül.