Odborný článok

Vykresľovanie vložených písiem PDF v Delphi pomocou HotPDF

HotPDF vykresľuje vložené písma PDF v Delphi bez inštalácie čohokoľvek do systému: interné spracovanie vykresľovania glyfov (embedded glyph rendering pipeline) v HPDFGlyphRender.pas analyzuje programy písiem uložené priamo v samotnom PDF — obrysy TrueType glyf z FontFile2, charstringy CFF Type 2 z FontFile3 a prúdy obsahu glyfov Type 3 — a prehráva ich ako vyplnené vektorové cesty GDI. Tento článok je detailným pohľadom na vernosť písiem na pozadí vykresľovania stránok PDF do TBitmap pomocou HotPDF: spomínaný článok sa zaoberá vykresľovaním vo všeobecnosti, zatiaľ čo tento sa zameriava na to, ako text na týchto stránkach získava svoje presné tvary

Prečo sa PDF vykresľuje so štvorčekmi namiesto textu?

Štvorčeky (tofu), prázdne miesta alebo mierne odlišné znaky vo vykreslenom výstupe PDF takmer vždy znamenajú, že vykresľovač požiadal operačný systém o písmo namiesto toho, aby použil písmo vložené v súbore. Sťažnosť prichádza zakaždým rovnako: dokument vyzerá dokonale na počítači, ktorý ho vygeneroval, potom ho však zákazník otvorí na čistom serveri alebo zabezpečenom počítači a japonská faktúra zobrazí štvorčeky tofu, prípadne nahradené podobné písmo posunie každý zlom riadku. Písma na danom počítači nikdy neboli – nachádzajú sa len vo vnútri PDF – a vykresľovač, ktorý skončí pri nahradení systémovým písmom, ich nedokáže zobraziť. Subsety písiem (podmnožiny) situáciu ešte zhoršujú: subset môže niesť štyridsať glyfov pod kódmi znakov priradenými súkromne len pre tento jeden súbor, čo je priradenie, ktoré žiadne nainštalované písmo nezdieľa

Norma ISO 32000-1 §9.9 definuje tri nosiče pre vložený program písma v deskriptore písma: FontFile obsahuje pôvodný program Type 1, FontFile2 program TrueType a FontFile3 čisté CFF (Type1C alebo CIDFontType0C) alebo obal OpenType. Štvrtým variantom je písmo Type 3 podľa ISO 32000-1 §9.6.5, ktoré nevkladá vôbec nič binárne — každý glyf je malý prúd obsahu PDF vykonávaný priamo na mieste. Tieto tri nosiče sa líšia v matematike obrysov (kvadratické B-splajny verzus kubické charstringy verzus ľubovoľné operátory stránky), takže verný vykresľovač potrebuje samostatný interpret pre každý z nich a navyše kódovaciu vrstvu, ktorá premení kódy znakov na správny index glyfu ešte pred akýmkoľvek spracovaním obrysu

Ako HotPDF konvertuje obrysy TrueType glyf na GDI cesty?

Trieda THPDFEmbeddedTTF v HPDFGlyphRender.pas číta tabuľku loca, aby lokalizovala každý záznam glyfu, prechádza obrysy glyf bod po bode a generuje GDI cestu. Dve konvencie TrueType si vyžadujú explicitné spracovanie. Po prvé, po sebe nasledujúce body mimo krivky (off-curve) implikujú bod na krivke (on-curve) v ich strede a obrys, ktorého všetky body sú mimo krivky, začína v strede jeho posledného a prvého bodu — ak vynecháte jedno z týchto pravidiel, zaoblené glyfy získajú ploché hrany alebo sa zrútia. Po druhé, krivky TrueType sú kvadratické Bézierove krivky, zatiaľ čo metóda PolyBezierTo rozhrania GDI prijíma kubické krivky, takže každý kvadratický segment je presne povýšený na vyšší stupeň namiesto toho, aby bol sploštený do úsečiek

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (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;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

Povýšenie stupňa (degree elevation) je bezstratové: kubická krivka sleduje identickú trasu, takže vykreslený obrys zodpovedá tomu, čo kompatibilný prehliadač nakreslí z rovnakej tabuľky pri akomkoľvek priblížení. Zostávajúcou prácou je umiestnenie. Každý glyf je navrhnutý v jednotkách písma (zvyčajne mriežka 1000 alebo 2048 units-per-em) a vykresľovač pred vyplnením cesty skombinuje maticu mierky, maticu textu a aktuálnu transformačnú maticu do jednej transformácie glyf-zariadenie. Na poradí tu záleží viac, ako sa zdá: skombinujte rovnaké tri matice v opačnom poradí a každý glyf sa zrúti smerom k počiatku súradníc — stránka bude vyzerať nesprávne a skutočná chyba bude v jedinom riadku maticovej algebry

Ako interpret charstringov Type 2 spracováva písma CFF

Trieda THPDFEmbeddedCFF v HPDFGlyphRender.pas poskytuje programom FontFile3 skutočný interpret charstringov Type 2: analyzuje štruktúry CFF INDEX, Top DICT a Private DICT, potom vykoná každý charstring a posiela segmenty cesty priamo do GDI. Obal OpenType (kontajner OTTO) sa najprv odstráni, aby sa dosiahla samotná tabuľka CFF; čisté prúdy CIDFontType0C a Type1C sa spracúvajú priamo. Charstringy sú kompaktný zásobníkový jazyk (stack language) a tri z jeho konvencií určujú, či interpret zostane zosynchronizovaný s prúdom bajtov. Volateľný prefix šírky znamená, že prvý operátor vyprázdňujúci zásobník môže niesť jeden vedúci operand navyše. Operátor hintmask implikuje vstemhm, keď sú operandy stále na zásobníku, a počet bajtov masky, ktoré sa majú preskočiť, závisí od akumulovaného počtu kmeňov (stem count) — stačí raz spočítať kmeň nesprávne a každý ďalší operačný kód (opcode) bude prečítaný chybne. A volania subrutín pridávajú pred vyhľadaním bias (odchýlku) k svojmu indexu (107, 1131 alebo 32768 v závislosti od počtu subrutín), takže volanie bez biasu skončí na úplne nesprávnej subrutíne

CFF s kľúčom CID pridáva jedno nepriame smerovanie (indirection), na ktorom zlyhávajú naivné implementácie: kód znaku vyberá CID, ale index charstringu je GID a znaková sada (charset) písma mapuje GID na CID — so vykresľovač pred kreslením zostaví inverzné mapovanie CID na GID a vyberie slovník Private DICT pre jednotlivé glyfy cez FDSelect pre písma, ktoré ich nesú niekoľko. Programy Type1C s kľúčom názvu (name-keyed), ktoré sú bežným nosičom pre jednoduché písma Type 1, namiesto toho riešia jednobajtové kódy prostredníctvom vstavaného kódovania programu CFF alebo prostredníctvom kódovacieho aparátu na úrovni PDF popísaného nižšie. Jedno úprimné upozornenie: interpret číta operátory hintov, aby udržal prúd zosynchronizovaný, ale nevykonáva samotný hinting (vyhladzovanie), čo je obmedzenie popísané na konci

Čo je písmo Type 3 a ako sa kreslí?

Glyf Type 3 nie je vôbec obrys — norma ISO 32000-1 §9.6.5 ho definuje ako prúd obsahu (content stream), takže HotPDF ho vykresľuje tak, že uloží grafický stav, skombinuje maticu písma, veľkosť písma a maticu textu do CTM a vykoná procedúru glyfu cez rovnaký interpret operátorov, ktorý kreslí stránky, pričom v rozsahu platnosti sú vlastné prostriedky /Resources daného písma. Pre správnosť sú dôležité dva detaily špecifikácie. Šírky /Widths písiem Type 3 sú vyjadrené v priestore glyfov a nie v textovom priestore 1/1000, ktorý používajú všetky ostatné typy písiem, takže posuny musia prechádzať cez /FontMatrix — písma čiarových kódov s maticou 0.01 by inak krokovali nesprávne o celú desiatkovú úroveň. A procedúra glyfu, ktorá začína operátorom d1, dáva dva sľuby, ktoré vykresľovač vynucuje: kreslenie je orezané na deklarovaný ohraničujúci rámec (bounding box) a podľa normy ISO 32000-1 §9.6.5.2 glyf ignoruje svoje vlastné operátory farieb a kreslí sa aktuálnou farbou výplne volajúceho, takže operátory rg, g, k a ich náprotivky pre ťahy sú počas trvania daného glyfu potlačené. Ak preskočíte pravidlo o farbe, písmo čiarového kódu d1 opečiatkované stránkou na modro vyjde čierne; ak preskočíte orezanie, nesprávne formovaný glyf vykreslí čiary mimo svojej bunky

Ako sa kódy znakov menia na ID glyfov

Interprety obrysov predstavujú len polovicu práce, pretože bajty v textovom reťazci PDF sú kódy znakov, nie indexy glyfov, a norma ISO 32000-1 venuje tomuto mapovaniu dva pododseky. Pre jednoduché písma predpisuje §9.6.6 prísnu prioritu: pole /Differences prekrýva základné kódovanie (WinAnsiEncoding, MacRomanEncoding alebo StandardEncoding), ktoré zasa prekrýva vlastnú mapu programu písma. HotPDF vyhodnocuje tento reťazec do tabuľky priradenia kódov na GID s 256 položkami, pričom názvy glyfov prekladá na indexy glyfov tromi cestami: presnou zhodou znakovej sady vo vnútri programu CFF, číselnými názvami gNN/glyphNN branými ako doslovné indexy a prekladom názvu na Unicode podľa tabuľky Adobe Glyph List, po ktorom nasleduje vyhľadanie v tabuľke cmap pre programy TrueType. Pri zložených písmach riadi proces §9.7 prostredníctvom mapy CIDToGIDMap: bežným prípadom je /Identity, avšak položka môže byť prúdom párov v big-endian formáte indexovaných podľa CID — a vlastný Unicode výstup HotPDF používa presne túto formu prúdu pre kompaktné subsety, takže cesta prúdu nie je žiadnym exotickým rohom

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

Keď je potrebné vyhľadanie v tabuľke cmap pre TrueType, HotPDF prechádza záložný reťazec namiesto toho, aby dôveroval jednej podtabuľke: prvé sú na rade Unicode podtabuľky Windows (formát 4, potom formát 12 pre doplnkové roviny), nasledované podtabuľkou symbolov (3,0) s konvenciou privátnej zóny F000 zrkadlenou do dolného bajtu — čo je dôvod, prečo symbolické písmo ako Wingdings odpovedá na bežné kódy ASCII — potom historické formáty 6 a 0. Podtabuľky formátu 2 sa zámerne neinterpretujú: mapujú historické viacbajtové kódové stránky ako Shift-JIS a Big5, not Unicode, and modern CJK fonts invariably carry a format 4 or format 12 subtable anyway. Any code that survives none of these routes falls back to GDI drawing for that single glyph, so one unmappable character degrades one glyph, not the whole text run

Čo vstavaná cesta nevykonáva

Hinting (vyhladzovanie) sa nevykonáva — obrysy sa vyplnia tak, ako boli navrhnuté, čo je pri rozlíšení 150 DPI a viac na nerozoznanie od výstupu s hintingom, no pri veľmi malých veľkostiach sa môže o pixel líšiť od rastra s hintingom. Pôvodné programy Type 1 vo FontFile (eexec-šifrované charstringy) sa neinterpretujú a neaplikujú sa ani osi premenlivých písiem OpenType (variable-font axes); oba prípady, podobne ako poškodený program písma alebo tabuľka glyf bez akejkoľvek použiteľnej cmap, prechádzajú na kreslenie systémovým písmom namiesto zlyhania vykreslenia stránky. Rovnaký prístup s prioritou pre vernosť sa uplatňuje aj na iných miestach vykresľovača — axiálne a radiálne vzory tieňovania dostávajú rovnakú starostlivosť, akú si zaslúžia prechody (gradients) — a strana generovania má svoj vlastný príbeh o detailoch písiem v tom, ako metóda EndDoc určuje poradie subsetov písiem

Použitie tohto spracovania si nevyžaduje vôbec žiadny kód špecifický pre písma — každý z vyššie uvedených mechanizmov sa zapája automaticky vo vnútri volania vykreslenia stránky

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      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ý dôsledok je presne ten, ktorý zaujíma vašu technickú podporu: PDF, ktoré nesie svoje písma, sa s týmito písmami aj vykreslí, či už na zostavovacom serveri (build server), v kontajneri Windows, alebo na počítači zákazníka, ktorý dané písmo nikdy nevidel. Spracovanie vykresľovania vložených glyfov sa dodáva ako súčasť komponentu HotPDF Component pre Delphi a C++Builder — natívnej knižnice VCL pokrývajúcej tvorbu, úpravu, extrakciu textu a vykresľovanie stránok PDF bez externých závislostí na DLL