Technický článek

Vykreslování vložených písem PDF v Delphi pomocí HotPDF

Knihovna HotPDF vykresluje vložená písma PDF v Delphi bez nutnosti instalovat cokoli do systému: vestavěný vykreslovací kanál pro glyfy v HPDFGlyphRender.pas analyzuje programy písem uložené přímo uvnitř samotného PDF — TrueType obrysy glyf z FontFile2, CFF Type 2 charstringy z FontFile3 a proudy obsahu glyfů Type 3 — a přehrává je jako vyplněné vektorové cesty GDI. Tento článek představuje hluboký ponor do věrnosti písem na pozadí vykreslování stránek PDF do TBitmap pomocí HotPDF: zmíněný článek se zabývá vykreslovačem jako celkem, tento pak tím, jak text na těchto stránkách získává své přesné tvary

Proč se PDF vykresluje s obdélníky namísto textu?

Obdélníky, prázdná místa nebo jemně nesprávné znaky ve vykresleném výstupu PDF téměř vždy znamenají, že vykreslovač požádal operační systém o písmo namísto použití písma vloženého v souboru. Stížnost přichází pokaždé stejným způsobem: dokument vypadá perfektně na stroji, kde byl vytvořen, ale když jej zákazník otevře na čistém serveru nebo uzamčeném počítači, japonská faktura zobrazí čtverečky „tofu“, případně nahrazené podobné písmo posune konce řádků. Písma na daném stroji nikdy nebyla — jsou pouze uvnitř PDF — a vykreslovač, který skončí u nahrazení systémovým písmem, je nevidí. Podmnožiny písem (subset fonts) to ještě zhoršují: podmnožina může nést čtyřicet glyfů pod kódy znaků přiřazenými soukromě tomuto jedinému souboru, což je přiřazení, které žádné instalované písmo nesdílí

Norma ISO 32000-1 §9.9 definuje tři nosiče pro vložený program písma v deskriptoru písma: FontFile obsahuje původní program Type 1, FontFile2 program TrueType a FontFile3 čisté CFF (Type1C nebo CIDFontType0C) nebo obal OpenType. Čtvrtá varianta, písmo Type 3 podle normy ISO 32000-1 §9.6.5, neobsahuje vůbec žádný binární soubor — každý glyf je malý proud obsahu PDF prováděný na místě. Tyto tři nosiče se liší v matematice obrysů (kvadratické B-splajny versus kubické charstringy versus libovolné operátory stránek), takže věrný vykreslovač potřebuje pro každý z nich samostatný interpret a navíc kódovací vrstvu, která před jakýmkoli zásahem do obrysů převede kódy znaků na správný index glyfu

Jak HotPDF převádí obrysy TrueType glyf na cesty GDI?

Třída THPDFEmbeddedTTF v HPDFGlyphRender.pas čte tabulku loca pro vyhledání každého záznamu glyfu, prochází obrysy glyf bod po bodu a generuje cestu GDI. Dvě konvence TrueType vyžadují explicitní zpracování. Za prvé, po sobě jdoucí body mimo křivku (off-curve points) implikují bod na křivce (on-curve point) v jejich středu, a obrys, jehož všechny body jsou mimo křivku, začíná ve středu jeho posledního a prvního bodu — pokud vynecháte kterékoli z těchto pravidel, zaoblené glyfy získají ploché hrany nebo se zhroutí. Za druhé, křivky TrueType jsou kvadratické Bézierovy křivky, zatímco metoda GDI PolyBezierTo přijímá kubické křivky, takže každý kvadratický segment se přesně povýší na vyšší stupeň, namísto aby se zploštil do úseček

Třída THPDFEmbeddedTTF v HPDFGlyphRender.pas čte tabulku loca pro vyhledání každého záznamu glyfu, prochází obrysy glyf bod po bodu a generuje cestu GDI. Dvě konvence TrueType vyžadují explicitní zpracování. Za prvé, po sobě jdoucí body mimo křivku (off-curve points) implikují bod na křivce (on-curve point) v jejich středu, a obrys, jehož všechny body jsou mimo křivku, začíná ve středu jeho posledního a prvního bodu — pokud vynecháte kterékoli z těchto pravidel, zaoblené glyfy získají ploché hrany nebo se zhroutí. Za druhé, křivky TrueType jsou kvadratické Bézierovy křivky, zatímco metoda GDI PolyBezierTo přijímá kubické křivky, takže každý kvadratický segment se přesně povýší na vyšší stupeň, namísto aby se zploštil do úseček

// 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ýšení stupně je bezztrátové: kubická křivka vykresluje identickou křivku, takže vykreslený obrys odpovídá tomu, co vyhovující prohlížeč vykreslí 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 písma (obvykle mřížka 1000 nebo 2048 jednotek na em) a vykreslovač složí matici měřítka, matici textu a aktuální transformační matici (CTM) do jedné transformace glyfu na zařízení před vyplněním cesty. Na pořadí záleží více, než se zdá: složte stejné tři matice v opačném pořadí a každý glyf se zhroutí směrem k počátku souřadnic — nesprávně vypadající stránka, jejíž skutečná chyba spočívá v jediném řádku maticové algebry

Jak interpret charstringů Type 2 zpracovává písma CFF

Třída THPDFEmbeddedCFF poskytuje programům FontFile3 skutečný interpret charstringů Type 2: analyzuje struktury CFF INDEX, Top DICT a Private DICT, poté provede každý charstring a předá segmenty cesty přímo do GDI. Obal OpenType (kontejner OTTO) se nejprve odstraní, aby se dosáhlo čisté tabulky CFF; čisté proudy CIDFontType0C a Type1C se zpracovávají přímo. Charstringy jsou kompaktní zásobníkový jazyk a tři z jeho konvencí určují, zda interpret zůstane synchronizovaný s bajtovým proudem. Volitelný prefix šířky znamená, že první operátor vyprazdňující zásobník může nést jeden operand navíc. Operátor hintmask implikuje vstemhm, když jsou operandy stále na zásobníku, a počet bajtů masky, které se mají přeskočit, závisí na nashromážděném počtu dříků (stem count) — stačí jednou spočítat dříky špatně a každý další operační kód je přečten chybně. A volání podprogramů před vyhledáním přičítají k indexu bias (107, 1131 nebo 32768 v závislosti na počtu podprogramů), takže volání bez biasu skončí ve zcela nesprávném podprogramu

Písma CFF indexovaná pomocí CID přidávají jednu úroveň nepřímého adresování, která zaskočí naivní implementace: kód znaku vybírá CID, avšak index charstringu je GID a znaková sada písma (charset) mapuje GID na CID — vykreslovač proto před kreslením sestaví inverzní mapování CID na GID a vybere Private DICT pro jednotlivé glyfy přes FDSelect u písem, která jich nesou několik. Programy Type1C indexované názvy, což je obvyklý nosič pro jednoduchá písma Type 1, naopak překládají jednobajtové kódy prostřednictvím vestavěného kódování programu CFF nebo přes kódovací mechanismus na úrovni PDF, který je popsán dále. Jedno upřímné upozornění: interpret čte operátory hintů pro zachování synchronizace proudu, ale hinting neprovádí, což je omezení rozebrané na konci

Co je to písmo Type 3 a jak se kreslí?

Glyf Type 3 není obrysem v pravém slova smyslu — norma ISO 32000-1 §9.6.5 jej definuje jako proud obsahu, takže jej HotPDF vykresluje tak, že uloží grafický stav, složí matici písma, velikost písma a matici textu do CTM a provede proceduru glyfu pomocí stejného interpretu operátorů, který kreslí stránky, přičemž v rozsahu platnosti jsou vlastní /Resources písma. Dva detaily specifikace jsou klíčové pro správnost. Šířky /Widths u Type 3 jsou vyjádřeny v prostoru glyfu namísto textového prostoru 1/1000, který používá každý jiný typ písma, takže posuny must pass through /FontMatrix — barcode fonts with a 0.01 matrix otherwise step wrong by an order of magnitude. And a glyph procedure that opens with the d1 operator makes two promises the renderer enforces: painting is clipped to the declared bounding box, and per ISO 32000-1 §9.6.5.2 the glyph ignores its own colour operators and paints with the caller's current fill colour, so rg, g, k and their stroking twins inside the procedure are suppressed for the duration of that glyph. Skip the colour rule and a d1 barcode font stamped in blue by the page comes out black; skip the clip and a malformed glyph paints outside its cell

Jak se kódy znaků stávají ID glyfů

Interprety obrysů jsou pouze polovinou práce, protože bajty v textovém řetězci PDF jsou kódy znaků, nikoli indexy glyfů, a norma ISO 32000-1 věnuje tomuto mapování dva podčlánky. Pro jednoduchá písma předepisuje §9.6.6 přísnou prioritu: pole /Differences přebíjí základní kódování (WinAnsiEncoding, MacRomanEncoding nebo StandardEncoding), které přebíjí vlastní mapu programu písma. HotPDF překládá tento řetězec do tabulky kódů na GID o 256 položkách, přičemž překládá názvy glyfů na indexy glyfů třemi způsoby: přesnou shodou znakové sady uvnitř programu CFF, číselnými názvy gNN/glyphNN branými jako doslovné indexy a překladem názvu na Unicode pomocí Adobe Glyph List následovaným vyhledáním v cmap pro programy TrueType. U složených písem řídí proces §9.7 pomocí CIDToGIDMap: běžným případem je /Identity, avšak položka může být proudem dvojic v kódování big-endian indexovaných pomocí CID — a vlastní Unicode výstup HotPDF používá právě tuto formu proudu pro kompaktní podmnožiny, takže cesta proudu není žádným exotickým okrajovým případem

// /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

Když je vyžadováno vyhledání v cmap TrueType, prochází HotPDF záložní řetězec, místo aby důvěřoval jediné podtabulce: nejprve přicházejí na řadu podtabulky Windows Unicode (formát 4, poté formát 12 pro doplňkové roviny), následně podtabulka symbolů (3,0) s konvencí soukromé oblasti použití F000 zrcadlenou do dolního bajtu — což je důvod, proč písmo symbolů jako Wingdings odpovídá na běžné kódy ASCII — a poté starší formáty 6 and 0. Format 2 subtables are deliberately not interpreted: they map legacy multi-byte code pages such as Shift-JIS and 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

Co vestavěný kanál neprovádí

Hranice podpory stojí za jasné vyjádření. Hinting se neprovádí — obrysy se vyplňují tak, jak byly vytvořeny, což je k nerozeznání od hintovaného výstupu při rozlišení 150 DPI a vyšším, ale může se lišit o pixel od hintovaného rastru při velmi malých velikostech. Původní programy Type 1 v FontFile (eexec-šifrované charstringy) se neinterpretují a osy variabilních písem OpenType se neaplikují; v obou případech, stejně jako u poškozeného programu písma nebo tabulky glyf bez jakékoli použitelné cmap, se systém vrací k vykreslování systémovým písmem, místo aby vykreslení stránky selhalo. Zde zobrazené funkce pro skládání a třídy šablon vodoznaků se dodávají v aktuální verzi axial and radial shading patterns get the same treatment gradients deserve — and the generation side has its own font-subtlety story in how EndDoc orders font subsets

Použití tohoto kanálu nevyžaduje vůbec žádný kód specifický pro písma — každý z výše uvedených mechanismů se zapojuje 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
      // 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ým důsledkem je to, na čem závisí spokojenost vašich uživatelů: PDF, které nese svá písma, se vykreslí s těmito písmy na sestavovacím serveru, v kontejneru Windows i na ploše zákazníka, který dané písmo nikdy neviděl. Vestavěný vykreslovací kanál pro glyfy se dodává jako součást produktu HotPDF Component pro Delphi a C++Builder — nativní knihovny VCL pokrývající vytváření, úpravy, extrakci textu a vykreslování stránek PDF bez externích závislostí na DLL