Tehnični članek

Izrisovanje vgrajenih pisav PDF v Delphiju s HotPDF

HotPDF izrisuje vgrajene pisave PDF v Delphiju, ne da bi bilo treba karkoli namestiti na računalnik: cevovod za izrisovanje vgrajenih glifov v HPDFGlyphRender.pas razčleni programe pisav, shranjene znotraj samega PDF-ja — obrise TrueType glyf iz FontFile2, nize znakov CFF Type 2 (charstrings) iz FontFile3 in tokove vsebin glifov Type 3 — ter jih ponovno predvaja kot napolnjene vektorske poti GDI. Ta članek je poglobljen pregled natančnosti pisav za izrisovanjem strani PDF v TBitmap s HotPDF: tisti prispevek opisuje izrisovalnik kot celoto, ta pa se osredotoča na to, kako besedilo na teh straneh dobi svoje natančne oblike

Kvadratki, praznine ali neznatno napačni znaki v izrisanem izhodu PDF skoraj vedno pomenijo, da je izrisovalnik od operacijskega sistema zahteval pisavo, namesto da bi uporabil tisto, ki je vgrajena v datoteki. Pritožba vsakič prispe na enak način: dokument je videti popoln na računalniku, ki ga je ustvaril, ko pa ga stranka odpre na čistem strežniku ali zaklenjenem namizju, japonski račun prikaže kvadratke ("tofu"), ali pa zamenjana podobna pisava premakne vsak prelom vrstice. Pisav nikoli ni bilo na tistem računalniku — bile so le znotraj PDF-ja —, izrisovalnik, ki se ustavi pri zamenjavi s sistemsko pisavo, pa jih ne more videti. Delne pisave (subset fonts) zadevo še poslabšajo: delni nabor lahko nosi štirideset glifov pod kodami znakov, ki so zasebno dodeljene tisti specifični datoteki, česar ne deli nobena nameščena pisava

Standard ISO 32000-1 §9.9 opredeljuje tri nosilce za vgrajeni program pisave v deskriptorju pisave: FontFile vsebuje izvorni program Type 1, FontFile2 program TrueType, FontFile3 pa goli CFF (Type1C ali CIDFontType0C) ali ovojnico OpenType. Četrta različica, pisava Type 3 po standardu ISO 32000-1 §9.6.5, sploh ne vgrajuje ničesar binarnega — vsak glif je majhen tok vsebine PDF, ki se izvede na mestu. Ti trije nosilci se razlikujejo v matematiki obrisov (kvadratični B-splini proti kubičnim charstringom proti poljubnim operaterjem strani), zato zanesljiv izrisovalnik potrebuje ločen interpret za vsakega izmed njih, poleg tega pa še kodirno plast, ki pretvori kode znakov v pravilen indeks glifa, preden se dotakne katerega koli obrisa

Kako HotPDF pretvori obrise TrueType glyf v poti GDI?

Razred THPDFEmbeddedTTF v HPDFGlyphRender.pas bere tabelo loca, da locira vsak zapis glifa, se točko za točko sprehodi skozi obrise glyf in odda pot GDI. Dve konvenciji TrueType potrebujeta eksplicitno obdelavo. Prvič, zaporedne točke zunaj krivulje (off-curve points) implicirajo točko na krivulji na svoji sredini, obris, katerega točke so vse zunaj krivulje, pa se začne na sredini med svojo zadnjo in prvo točko — če preskočite katero od teh pravil, zaobljeni glifi dobijo ravne robove ali pa se sesujejo. Drugič, krivulje TrueType so kvadratične Bézierjeve krivulje, medtem ko GDI-jev PolyBezierTo sprejema kubične, zato se vsakemu kvadratičnemu segmentu natančno poviša stopnja (degree-elevation), namesto da bi ga sploščili v linijske segmente

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

Povišanje stopnje je brezizgubno: kubična krivulja sledi enaki poti, zato se izrisani obris ujema s tem, kar skladni pregledovalnik izriše iz iste tabele pri kateri koli povečavi. Preostalo delo je pozicioniranje. Vsak glif je zasnovan v enotah pisave (običajno mreža 1000 ali 2048 enot na em), izrisovalnik pa pred polnjenjem poti združi matriko merila, matriko besedila in trenutno transformacijsko matriko v eno transformacijo glifa v napravo. Vrstni red je tukaj pomembnejši, kot se zdi: če iste tri matrike združite v obratnem vrstnem redu, se vsak glif sesuje proti koordinatnemu izhodišču — stran bo videti napačna, dejanski hrošč pa je le v eni vrstici matrične algebre

Kako interpret charstringov Type 2 obravnava pisave CFF

Razred THPDFEmbeddedCFF daje programom FontFile3 pristen interpret charstringov Type 2: razčleni strukture CFF INDEX, Top DICT in Private DICT, nato pa izvede vsak charstring in odda segmente poti neposredno v GDI. Ovojnica OpenType (vsebnik OTTO) se najprej odstrani, da se doseže gola tabela CFF; goli tokovi CIDFontType0C in Type1C se porabijo neposredno. Charstringi so kompakten skladovni jezik in tri njegove konvencije določajo, ali interpret ostane usklajen s tokom bajtov. Neobvezna predpona širine pomeni, da lahko prvi operater za čiščenje sklada nosi en dodaten vodilni operand. Operater hintmask implicira vstemhm, ko so operandi še na skladu, število bajtov maske, ki jih je treba preskočiti, pa je odvisno od skupnega števila stebel (stems) — če se enkrat zmotite pri tem številu, bo vsak naslednji opkoda napačno prebran. Klici podprogramov (subroutines) pa pred iskanjem dodajo odmik (bias) svojemu indeksu (107, 1131 ali 32768, odvisno od števila podprogramov), zato klic brez odmika pristane v povsem napačnem podprogramu

Z indeksi CID določeni CFF (CID-keyed CFF) dodaja eno raven posrednosti, ki lahko preseneti preproste implementacije: koda znaka izbere CID, indeks charstringa pa je GID, pri čemer nabor znakov pisave preslika GID v CID — zato izrisovalnik pred risanjem zgradi obratno preslikavo CID v GID in za pisave, ki jih nosijo več, izbere Private DICT za posamezen glif prek FDSelect. Z imeni določeni programi Type1C, ki so običajni nosilci za preproste pisave Type 1, namesto tega razrešujejo enobajtne kode prek vgrajenega kodiranja programa CFF ali prek kodirnega mehanizma na ravni PDF, ki je opisan v nadaljevanju. Iskreno opozorilo: interpret bere operaterje namigov (hints), da ohrani tok usklajen, vendar namigovanja ne izvaja, kar je omejitev, obravnavana na koncu

Kaj je pisava Type 3 in kako se izriše?

Glif Type 3 sploh ni obris — standard ISO 32000-1 §9.6.5 ga opredeljuje kot tok vsebine, zato ga HotPDF izriše tako, da potisne grafično stanje, združi matriko pisave, velikost pisave in matriko besedila v CTM ter izvede proceduro glifa prek istega interpretatorja operaterjev, ki riše strani, pri čemer so v obsegu lastni viri pisave /Resources. Dve podrobnosti specifikacije sta pomembni za pravilnost. Širine Type 3 /Widths so izražene v prostoru glifa in ne v besedilnem prostoru 1/1000, ki ga uporablja vsaka druga vrsta pisave, zato morajo premiki potekati skozi /FontMatrix — pisave s črtno kodo z matriko 0.01 se sicer premikajo napačno za red velikosti. In procedura glifa, ki se odpre z operaterjem d1, daje dve obljubi, ki ju izrisovalnik uveljavlja: risanje je omejeno na deklarirano odmejno polje (bounding box), po standardu ISO 32000-1 §9.6.5.2 pa glif prezre lastne barvne operaterje in se izriše s trenutno barvo polnjenja klicatelja, zato so operaterji rg, g, k in njihovi dvojčki za risanje črt znotraj procedure zatrti med izvajanjem tega glifa. Če preskočite barvno pravilo, bo pisava s črtno kodo d1, ki jo je stran označila modro, izrisana črno; če preskočite omejitev odmejnega polja, pa se bo nepravilno oblikovan glif izrisal izven svoje celice

Kako kode znakov postanejo ID-ji glifov

Interpretatorji obrisov so le polovica dela, saj so bajti v besedilnem nizu PDF kode znakov in ne indeksi glifov, standard ISO 32000-1 pa temu preslikovanju namenja dve podpoglavji. Za preproste pisave določa §9.6.6 strogo prednostno vrstni red: matrika /Differences povozi osnovno kodiranje (WinAnsiEncoding, MacRomanEncoding ali StandardEncoding), ki pa povozi lastno tabelo programa pisave. HotPDF razreši to verigo v tabelo pretvorbe kode v GID z 256 vnosi, pri čemer prevaja imena glifov v indekse glifov na tri načine: natančno ujemanje nabora znakov znotraj programa CFF, številčna imena gNN/glyphNN, obravnavana kot dobesedni indeksi, in prevod ime-v-Unicode po Adobe Glyph List, ki mu sledi iskanje v tabeli cmap za programe TrueType. Za sestavljene pisave §9.7 postavi v ospredje CIDToGIDMap: običajni primer je /Identity, lahko pa je vnos tok parov v obliki big-endian, indeksiranih s CID — in lasten Unicode izhod HotPDF uporablja natanko to obliko toka za kompaktne delne nabore, zato pot toka ni eksotičen kot

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

Ko je potrebno iskanje v tabeli cmap TrueType, se HotPDF sprehodi skozi verigo nadomestnih možnosti (fallbacks) in ne zaupa zgolj eni podtabeli: najprej pridejo na vrsto podtabele Windows Unicode (format 4, nato format 12 za dodatne ravnine), nato podtabela simbolov (3,0) s svojo konvencijo zasebnega območja uporabe F000, preslikano na spodnji bajt — kar je razlog, da se pisava s simboli, kot je Wingdings, odziva na navadne kode ASCII —, nato pa še zastarela formata 6 in 0. Podtabel formata 2 se namerno ne interpretira: te preslikujejo stare večbajtne kodne strani, kot sta Shift-JIS in Big5, in ne Unicode, sodobne pisave CJK pa v vsakem primeru nosijo podtabelo formata 4 ali formata 12. Vsaka koda, ki ne preživi nobene od teh poti, se za tisti posamezni glif vrne na risanje GDI, tako da en nepreslikljiv znak pokvari le en glif in ne celotnega besedilnega niza

Česa vgrajena pot ne naredi

Omejitve je vredno navesti jasno. Namigovanje (hinting) se ne izvaja — obrisi se zapolnijo tako, kot so zapisani, kar se pri ločljivostih 150 DPI in več ne razlikuje od namignjenega izhoda, pri zelo majhnih velikostih pa se lahko razlikuje za slikovno piko od namignjenega rastrirnika. Izvirni programi Type 1 v FontFile (z eexec šifrirani charstringi) se ne interpretirajo, osi spremenljivih pisav OpenType (variable fonts) pa se ne uporabljajo; v obeh primerih, kot tudi pri poškodovanem programu pisave ali tabeli glyf brez uporabne tabele cmap, se izrisovalnik vrne na risanje s sistemsko pisavo, namesto da bi javil napako na strani. Enak pristop, usmerjen v zanesljivost, se razširja tudi drugje v izrisovalniku — osni in radialni vzorci senčenja dobijo enak tretma, kot si ga zaslužijo prelivi —, stran za ustvarjanje pa ima svojo zgodbo o podrobnostih pisav v članku o tem, kako EndDoc ureja delne nabora pisav

Uporaba tega cevovoda sploh ne zahteva kode, specifične za pisave — vsak od zgornjih mehanizmov se samodejno aktivira znotraj klica za izris strani

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;

Praktična posledica je tista, ki zanima vaš predal za podporo: PDF, ki prenaša svoje pisave, se izriše s temi pisavami, bodisi na strežniku za gradnjo, v vsebniku Windows ali na namizju stranke, ki te pisave nikoli ni videla. Cevovod za izrisovanje vgrajenih glifov se prinaša kot del komponente HotPDF Component za Delphi in C++Builder — izvorne knjižnice VCL, ki pokriva ustvarjanje, urejanje, ekstrakcijo besedila in izrisovanje strani PDF brez zunanjih odvisnosti od datotek DLL