HotPDF vykresluje vložené fonty PDF v Delphi, aniž by na počítači cokoli instaloval: pipeline vykreslování vložených glyfů v HPDFGlyphRender.pas parsuje programy fontů uložené uvnitř samotného PDF — obrysy TrueType glyf z FontFile2, charstringy CFF Type 2 z FontFile3 a obsahové streamy glyfů Type 3 — a přehrává je jako vyplněné vektorové cesty GDI. Tento článek je hloubkovým ponorem do věrnosti fontů za článkem vykreslování stránek PDF do TBitmap pomocí HotPDF: ten pokrývá renderer jako celek, tento pokrývá, jak text na těchto stránkách získává své přesné tvary
Proč se PDF vykreslí s rámečky místo textu?
Rámečky, prázdná místa nebo nenápadně chybné znaky ve vykresleném výstupu PDF téměř vždy znamenají, že renderer požádal o font operační systém, místo aby použil ten vložený v souboru. Stížnost přichází pokaždé stejně: dokument vypadá dokonale na počítači, který jej vytvořil, pak jej zákazník otevře na čistém serveru nebo uzamčeném desktopu a japonská faktura ukazuje čtverečky tofu, nebo nahrazený podobný řez posune každé zalomení řádku. Fonty na tom počítači nikdy nebyly — jen uvnitř PDF — a renderer, který se zastaví u náhrady systémovým fontem, je nevidí. Podmnožiny fontů to zhoršují: podmnožina může nést čtyřicet glyfů pod kódy znaků přidělenými soukromě jen tomu jednomu souboru, což je přiřazení, které žádný nainstalovaný font nesdílí
ISO 32000-1 §9.9 definuje v deskriptoru fontu tři nosiče vloženého programu fontu: FontFile nese původní program Type 1, FontFile2 program TrueType a FontFile3 holé CFF (Type1C nebo CIDFontType0C) nebo obal OpenType. Čtvrtá varianta, font Type 3 podle ISO 32000-1 §9.6.5, nevkládá vůbec nic binárního — každý glyf je malý obsahový stream PDF vykonávaný na místě. Tři nosiče se liší matematikou obrysů (kvadratické B-spliny versus kubické charstringy versus libovolné operátory stránky), takže věrný renderer potřebuje pro každý z nich samostatný interpret plus vrstvu kódování, která převede kódy znaků na správný index glyfu dřív, než se dotkne jakéhokoli obrysu
Jak HotPDF převádí obrysy TrueType glyf na cesty GDI?
THPDFEmbeddedTTF v HPDFGlyphRender.pas čte tabulku loca, aby našel záznam každého glyfu, prochází obrysy v glyf bod po bodu a emituje cestu GDI. Dvě konvence TrueType vyžadují explicitní ošetření. Zaprvé, po sobě jdoucí body mimo křivku implikují bod na křivce v jejich středu a obrys, jehož všechny body leží mimo křivku, začíná ve středu mezi posledním a prvním bodem — vynechte kterékoli z těchto pravidel a zaoblené glyfy dostanou ploché fasety nebo se zhroutí. Zadruhé, křivky TrueType jsou kvadratické Bézierovy křivky, zatímco PolyBezierTo v GDI přijímá kubické, takže každý kvadratický segment se přesně povýší na vyšší stupeň, místo aby se zploštil na úsečky
// Přesné povýšení stupně: kvadratická (P0, Q, P2) -> kubická (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;
// poté PolyBezierTo s C1, C2, P2 — geometricky totožná křivka
Povýšení stupně je bezeztrátové: kubická křivka opisuje totožnou křivku, takže vykreslený obrys odpovídá tomu, co konformní prohlížeč nakreslí ze stejné tabulky, při jakémkoli přiblížení. Zbývající prací je umístění. Každý glyf je vytvořen v jednotkách fontu (obvykle mřížka 1000 nebo 2048 jednotek na em) a renderer před vyplněním cesty skládá matici měřítka, textovou matici a aktuální transformační matici do jedné transformace glyf-zařízení. Na pořadí zde záleží víc, než se zdá: složte tytéž tři matice pozpátku a každý glyf se zhroutí k počátku — stránka vypadající chybně, jejíž skutečnou chybou je jeden řádek maticové algebry
Jak interpret charstringů Type 2 zpracovává fonty CFF
THPDFEmbeddedCFF dává programům FontFile3 skutečný interpret charstringů Type 2: parsuje struktury CFF INDEX, Top DICT a Private DICT, poté vykoná každý charstring a emituje segmenty cesty přímo do GDI. Obal OpenType (kontejner OTTO) se nejprve odstraní, aby se dosáhlo holé tabulky CFF; nahé streamy CIDFontType0C a Type1C se konzumují přímo. Charstringy jsou kompaktní zásobníkový jazyk a tři z jeho konvencí rozhodují o tom, zda interpret zůstane v synchronizaci s bajtovým proudem. Volitelná předpona šířky znamená, že první operátor mazající zásobník může nést jeden nadbytečný úvodní operand. Operátor hintmask implikuje vstemhm, pokud jsou na zásobníku stále operandy, a počet bajtů masky k přeskočení závisí na nasčítaném počtu stemů — spleťte počet jednou a každý následující opcode se přečte špatně. A volání subrutin přidávají ke svému indexu před vyhledáním bias (107, 1131 nebo 32768 podle počtu subrutin), takže volání bez biasu skončí na úplně jiné subrutině
CFF s klíčováním CID přidává jednu nepřímost, na které naivní implementace zakopávají: kód znaku vybírá CID, ale index charstringu je GID a charset fontu mapuje GID na CID — renderer proto před kreslením sestaví inverzní mapování CID na GID a u fontů, které jich nesou několik, vybírá Private DICT pro každý glyf přes FDSelect. Programy Type1C s klíčováním podle názvů, obvyklý nosič jednoduchých fontů Type 1, místo toho vyhodnocují jednobajtové kódy přes vestavěné kódování programu CFF nebo přes mechanismus kódování na úrovni PDF popsaný dále. Jedna poctivá výhrada: interpret čte operátory hintů, aby udržel stream synchronizovaný, ale hinting nevykonává, což je hranice probíraná na konci
Co je font Type 3 a jak se kreslí?
Glyf Type 3 vůbec není obrys — ISO 32000-1 §9.6.5 jej definuje jako obsahový stream, takže HotPDF jej vykreslí tak, že uloží grafický stav, složí matici fontu, velikost fontu a textovou matici do CTM a vykoná proceduru glyfu přes stejný interpret operátorů, který kreslí stránky, s vlastními /Resources fontu v rozsahu. Pro správnost záleží na dvou detailech specifikace. /Widths u Type 3 se vyjadřují v prostoru glyfu, nikoli v textovém prostoru 1/1000, který používá každý jiný typ fontu, takže posuny musí projít přes /FontMatrix — fonty čárových kódů s maticí 0,01 jinak krokují chybně o řád. A procedura glyfu, která začíná operátorem d1, dává dva sliby, které renderer vynucuje: malování je oříznuto na deklarovaný ohraničující rámeček a podle ISO 32000-1 §9.6.5.2 glyf ignoruje své vlastní operátory barvy a maluje aktuální barvou výplně volajícího, takže rg, g, k a jejich tahové protějšky uvnitř procedury jsou po dobu daného glyfu potlačeny. Vynechte pravidlo o barvě a font čárového kódu d1 orazítkovaný stránkou modře vyjde černě; vynechte ořez a poškozený glyf maluje mimo svou buňku
Jak se kódy znaků stávají ID glyfů
Interprety obrysů jsou jen polovinou práce, protože bajty v textovém řetězci PDF jsou kódy znaků, ne indexy glyfů, a ISO 32000-1 věnuje mapování dvě podklauzule. Pro jednoduché fonty §9.6.6 předepisuje přísnou prioritu: pole /Differences přepisuje základní kódování (WinAnsiEncoding, MacRomanEncoding nebo StandardEncoding), které přepisuje vlastní mapu programu fontu. HotPDF tento řetězec vyhodnotí do tabulky kód-GID s 256 položkami a názvy glyfů překládá na indexy glyfů třemi cestami: přesnou shodou charsetu uvnitř programu CFF, číselnými názvy gNN/glyphNN branými jako doslovné indexy a překladem názvu na Unicode podle Adobe Glyph List následovaným vyhledáním v cmap u programů TrueType. Pro složené fonty §9.7 svěřuje velení CIDToGIDMap: běžným případem je /Identity, ale položka může být stream dvojic big-endian indexovaných podle CID — a vlastní výstup Unicode v HotPDF používá pro kompaktní podmnožiny přesně tuto streamovou formu, takže streamová cesta není žádný exotický kout
// /CIDToGIDMap jako stream: dvojice Word big-endian indexované podle CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // mimo rozsah se mapuje na .notdef
Když je potřeba vyhledání v cmap TrueType, HotPDF prochází záložní řetězec, místo aby důvěřoval jedné podtabulce: nejprve přijdou podtabulky Windows Unicode (formát 4, poté formát 12 pro doplňkové roviny), pak symbolová podtabulka (3,0) s její konvencí oblasti soukromého použití F000 zrcadlenou dolů na nízký bajt — důvod, proč symbolový font jako Wingdings odpovídá na prosté kódy ASCII — a poté zastaralé formáty 6 a 0. Podtabulky formátu 2 se záměrně neinterpretují: mapují zastaralé vícebajtové kódové stránky jako Shift-JIS a Big5, nikoli Unicode, a moderní fonty CJK stejně vždy nesou podtabulku formátu 4 nebo 12. Jakýkoli kód, který nepřežije žádnou z těchto cest, se pro tento jediný glyf vrátí ke kreslení přes GDI, takže jeden nemapovatelný znak degraduje jeden glyf, ne celý textový běh
Co vložená cesta nedělá
Hranice stojí za to vyslovit jasně. Hinting se nevykonává — obrysy se vyplňují tak, jak byly vytvořeny, což je při 150 DPI a výše nerozeznatelné od hintovaného výstupu, ale při velmi malých velikostech se může od hintovaného rasterizéru lišit o pixel. Původní programy Type 1 ve FontFile (charstringy šifrované eexec) se neinterpretují a osy variabilních fontů OpenType se neaplikují; oba případy, stejně jako poškozený program fontu nebo tabulka glyf bez jakékoli použitelné cmap, se vrátí ke kreslení systémovým fontem, místo aby stránka selhala. Stejný přístup s důrazem na věrnost se rozšiřuje i jinam v rendereru — vzory axiálního a radiálního stínování dostávají stejné zacházení, jaké si přechody zaslouží — a strana generování má svůj vlastní příběh o jemnostech fontů v článku jak EndDoc řadí podmnožiny fontů
Použití pipeline nevyžaduje vůbec žádný kód specifický pro fonty — každý mechanismus výše se zapojí automaticky uvnitř volání vykreslení stránky
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Vložené fonty TrueType, CFF a Type 3 se vykreslují ze
// samotného souboru — na tomto počítači není třeba nic instalovat
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Praktickým důsledkem je ten, na kterém záleží vaší schránce podpory: PDF, které nese své fonty, se s těmito fonty vykreslí na sestavovacím serveru, v kontejneru Windows nebo na desktopu zákazníka, který dané písmo nikdy neviděl. Pipeline vykreslování vložených glyfů je součástí komponenty HotPDF pro Delphi pro Delphi a C++Builder — nativní knihovny VCL pokrývající tvorbu, úpravy, extrakci textu a vykreslování stránek PDF bez závislostí na externích DLL