HotPDF piirtää upotetut PDF-fontit Delphissä asentamatta mitään koneelle: HPDFGlyphRender.pas-tiedostossa sijaitseva upotettujen merkkien piirtoputki jäsentää itse PDF:n sisään tallennetut fonttiohjelmat — TrueType glyf -ääriviivat FontFile2-tiedostosta, CFF Type 2 -merkkijonot FontFile3-tiedostosta ja Type 3 -merkkien sisältövirrat — ja toistaa ne täytettyinä GDI-vektoripolkuina. Tämä artikkeli on fonttien tarkkuuteen pureutuva syväsukellus PDF-sivujen TBitmap-bittikartaksi piirtämistä käsittelevän oppaan taustalla. Kyseinen opas kattaa piirtomoottorin kokonaisuudessaan, kun taas tämä artikkeli keskittyy siihen, miten sivujen teksti saa tarkan muotonsa
Miksi PDF piirtyy laatikoina tekstin sijaan?
Laatikot, tyhjät kohdat tai hieman väärät merkit piirretyssä PDF-tulosteessa tarkoittavat lähes aina sitä, että piirtomoottori pyysi käyttöjärjestelmältä fonttia tiedostoon upotetun fontin käyttämisen sijaan. Valitus saapuu joka kerta samalla tavalla: asiakirja näyttää täydelliseltä sen tuottaneella koneella, mutta kun asiakas avaa sen puhtaalla palvelimella tai lukitulla työpöydällä, japanilainen lasku näyttää "tofu-neliöitä" tai korvaava samankaltainen fontti siirtää jokaisen rivinvaihdon. Fontteja ei koskaan ollut kyseisellä koneella — vain PDF-tiedoston sisällä — eikä piirtomoottori, joka tyytyy käyttöjärjestelmän fonttien korvaamiseen, kykene näkemään niitä. Alijoukkofontit (subset fonts) pahentavat tilannetta: alijoukko voi sisältää neljäkymmentä merkkiä merkkikoodeilla, jotka on osoitettu yksityisesti juuri kyseiselle tiedostolle, eikä mikään asennettu fontti jaa tätä osoitusta
ISO 32000-1 §9.9 määrittelee kolme tapaa upotetun fonttiohjelman kuljettamiseen fonttikuvauksessa (font descriptor): FontFile sisältää alkuperäisen Type 1 -ohjelman, FontFile2 TrueType-ohjelman ja FontFile3 pelkän CFF-virran (Type1C tai CIDFontType0C) tai OpenType-kääreen. Neljäs vaihtoehto, ISO 32000-1 §9.6.5 mukainen Type 3 -fontti, ei upota mitään binääristä — kukin merkki on pieni PDF-sisältövirta, joka suoritetaan paikallaan. Nämä kolme tapaa eroavat ääriviivamatematiikaltaan (neliölliset B-splinit vs. kuutiolliset merkkijonot vs. vapaat sivuoperaattorit), joten tarkka piirtomoottori vaatii erillisen tulkin kullekin sekä koodauskerroksen, joka muuntaa merkkikoodit oikeaksi merkin indeksiksi (glyph index) ennen kuin ääriviivoihin kosketaan
Miten HotPDF muuntaa TrueType glyf -ääriviivat GDI-poluiksi?
THPDFEmbeddedTTF tiedostossa HPDFGlyphRender.pas lukee loca-taulukon kunkin merkin tietueen paikantamiseksi, käy läpi glyf-ääriviivat piste pisteeltä ja tuottaa GDI-polun. Kaksi TrueType-käytäntöä vaatii selkeää käsittelyä. Ensinnäkin peräkkäiset käyrän ulkopuoliset pisteet (off-curve points) määrittävät käyrällä olevan pisteen niiden puoliväliin, ja ääriviiva, jonka pisteet ovat kaikki käyrän ulkopuolella, alkaa sen viimeisen ja ensimmäisen pisteen puolivälistä. Jos jompikumpi sääntö ohitetaan, pyöristetyt merkit saavat suoria reunoja tai luhistuvat. Toiseksi TrueType-käyrät ovat neliöllisiä Bézier-käyriä, kun taas GDI:n PolyBezierTo ottaa vastaan kuutiollisia käyriä, joten jokaisen neliöllisen segmentin astelukua korotetaan (degree-elevated) tarkasti sen sijaan, että se tasoitettaisiin suoriksi viivoiksi
// Tarkka asteluvun korotus: neliöllinen (P0, Q, P2) -> kuutiöllinen (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;
// sen jälkeen PolyBezierTo arvoilla C1, C2, P2 — geometrisesti identtinen käyrä
Asteluvun korotus on häviötön: kuutiollinen käyrä seuraa täsmälleen samaa reittiä, joten piirretty ääriviiva vastaa sitä, mitä yhteensopiva katseluohjelma piirtää samasta taulukosta millä tahansa zoomilla. Jäljellä oleva työ on sijoittelu. Kukin merkki on luotu fonttiyksiköissä (yleensä 1000 tai 2048 units-per-em -ruudukko), ja piirtomoottori koostaa skaalausmatriisin, tekstikohdistusmatriisin ja nykyisen muunnosmatriisin (CTM) yhdeksi merkistä-laitteelle-muunnokseksi ennen polun täyttämistä. Järjestys on tässä tärkeämpi kuin miltä se näyttää: jos samat kolme matriisia koostetaan käänteisesti, jokainen merkki luhistuu kohti alkupistettä — tuloksena on väärältä näyttävä sivu, jonka todellinen virhe on vain yhdellä matriisialgebran rivillä
Miten Type 2 -merkkijonotulkki käsittelee CFF-fontteja
THPDFEmbeddedCFF tarjoaa FontFile3-ohjelmille todellisen Type 2 -merkkijonotulkin (charstring interpreter): se jäsentää CFF INDEX -rakenteet, Top DICT- ja Private DICT -sanakirjat, ja suorittaa sen jälkeen kunkin merkkijonon tuottaen polkusegmentit suoraan GDI:lle. OpenType-kääre (OTTO-säiliö) riisutaan ensin paljaan CFF-taulukon saavuttamiseksi; paljaat CIDFontType0C- ja Type1C-virrat kulutetaan suoraan. Merkkijonot ovat kompakti pinokieli (stack language), ja sen kolme käytäntöä ratkaisevat, pysyykö tulkki synkronoituna tavuvirran kanssa. Valinnainen leveyden etuliite tarkoittaa, että ensimmäinen pinon tyhjentävä operaattori voi kantaa yhtä ylimääräistä johtavaa operandia. Operaattori hintmask edellyttää vstemhm-operaattoria silloin, kun operandit ovat vielä pinossa, ja ohitettavien maskitavujen määrä riippuu kertyneestä runkojen (stem) määrästä. Jos laskenta menee kerrankin väärin, jokainen sitä seuraava käskykoodi luetaan virheellisesti. Ja aliohjelmakutsut lisäävät poikkeaman (bias) indeksiinsä (107, 1131 tai 32768 aliohjelmien määrästä riippuen) ennen hakua, joten ilman poikkeamaa tehty kutsu päätyy kokonaan väärään aliohjelmaan
CID-avainnetut CFF-fontit lisäävät yhden epäsuoruuden, joka hämmentää yksinkertaisia toteutuksia: merkkikoodi valitsee CID-arvon, mutta merkkijonoindeksi on GID, ja fontin merkistö kartoittaa GID-arvot CID-arvoiksi. Piirtomoottori rakentaa siten käänteisen CID-GID-kartoituksen ennen piirtämistä ja valitsee merkkikohtaisen Private DICT -sanakirjan FDSelect-valitsimella useita sanakirjoja kantaville fonteille. Nimi-avainnetut Type1C-ohjelmat, jotka ovat tyypillisiä yksinkertaisten Type 1 -fonttien kuljettajia, ratkaisevat sen sijaan yksitavuiset koodit CFF-ohjelman sisäänrakennetun koodauksen tai seuraavaksi kuvattavan PDF-tason koodausjärjestelmän kautta. Rehellinen varoitus: tulkki lukee vihjeoperaattoreita (hint operators) virran synkronoinnin säilyttämiseksi, mutta ei suorita varsinaista vihjeistystä (hinting), mikä on lopussa esitelty rajaus
Mikä on Type 3 -fontti ja miten se piirretään?
Type 3 -merkki ei ole lainkaan ääriviiva — ISO 32000-1 §9.6.5 määrittelee sen sisältövirraksi, joten HotPDF piirtää sen tallentamalla grafiikkatilan, koostamalla fonttimatriisin, fonttikoon ja tekstikohdistusmatriisin CTM:n päälle ja suorittamalla merkkiproseduurin saman operaattoritulkin kautta, joka piirtää sivut, fontin omien /Resources-resurssien ollessa voimassa. Kaksi spesifikaation yksityiskohtaa on tärkeää oikeellisuuden kannalta. Type 3 -leveydet /Widths ilmoitetaan merkkiavaruudessa (glyph space) sen sijaan, että käytettäisiin 1/1000-tekstiavaruutta kuten muut fonttityypit. Siksi etenemien on kuljettava /FontMatrix-matriisin kautta — viivakoodifontit 0.01-matriisilla etenevät muutoin väärin kertaluokalla. Ja merkkiproseduuri, joka alkaa d1-operaattorilla, antaa kaksi lupausta, jotka piirtomoottori valvoo: piirtäminen rajataan ilmoitettuun rajauslaatikkoon (bounding box), ja ISO 32000-1 §9.6.5.2 -standardin mukaisesti merkki jättää huomiotta omat värioperaattorinsa ja piirtyy kutsujan nykyisellä täyttövärillä, jolloin proseduurin sisäiset rg, g, k ja niiden viivausvastineet vaimennetaan merkin käsittelyn ajaksi. Jos värisääntö ohitetaan, sivun sinisellä leimaama d1-viivakoodifontti piirtyy mustana. Jos rajaus ohitetaan, väärin muotoiltu merkki piirtyy solunsa ulkopuolelle
Miten merkkikoodit muuttuvat merkkien tunnuksiksi (glyph IDs)
Ääriviivatulkit ovat vain puolet työstä, sillä PDF-tekstirivin tavut ovat merkkikoodeja, eivät merkkien indeksejä, ja ISO 32000-1 omistaa kaksi alalukua tälle kartoitukselle. Yksinkertaisille fonteille §9.6.6 määrää tiukan tärkeysjärjestyksen: /Differences-taulukko ohittaa peruskoodauksen (WinAnsiEncoding, MacRomanEncoding tai StandardEncoding), joka puolestaan ohittaa fonttiohjelman oman kartan. HotPDF ratkaisee tämän ketjun 256-paikkaiseksi koodi-GID-taulukoksi kääntäen merkkien nimet merkkien indekseiksi kolmea reittiä: tarkalla merkistötäsmäyksellä CFF-ohjelman sisällä, numeerisilla gNN/glyphNN-nimillä suorina indekseinä sekä Adobe Glyph List -nimen-Unicode-käännöksellä, jota seuraa cmap-haku TrueType-ohjelmille. Yhdistelmäfonteille (composite fonts) §9.7 asettaa vastuuseen CIDToGIDMap-kartoituksen: yleisin tapaus on /Identity, mutta merkintä voi olla myös CID-arvojen mukaan indeksoitu isojen tavujen (big-endian) parien virta — ja HotPDF:n oma Unicode-tuloste käyttää juuri tätä virtamuotoa kompakteille alijoukoille, joten virtapolku ei ole mikään eksoottinen poikkeus
// /CIDToGIDMap virtana: big-endian Word-parit indeksoituna CID-arvoilla
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // alueen ulkopuolella oleva kartoittuu tunnukseksi .notdef
Kun TrueType cmap -haku on tarpeen, HotPDF käy läpi varaketjun yhden alitaulukon luottamisen sijaan: Windows Unicode -alitaulukot (muoto 4, sitten muoto 12 lisätasoille) ovat ensisijaisia, sitten symbolialitaulukko (3,0) sen F000-yksityisalueen (private-use-area) käytännöllä heijastettuna alas alimpaan tavuun — mikä on syy siihen, miksi symbolifontti kuten Wingdings vastaa tavallisiin ASCII-koodeihin — ja sen jälkeen perinteiset muodot 6 ja 0. Muodon 2 alitaulukoita ei tietoisesti tulkita: ne kartoittavat perinteisiä monitavuisia koodisivuja kuten Shift-JIS ja Big5, eivät Unicodea, ja nykyaikaiset CJK-fontit sisältävät poikkeuksetta muodon 4 tai muodon 12 alitaulukon joka tapauksessa. Mikä tahansa koodi, joka ei sovi mihinkään näistä reiteistä, palautuu GDI-piirtoon kyseiselle yksittäiselle merkille, joten yksi kartoittamaton merkki heikentää vain yhtä merkkiä, ei koko tekstiajoa
Mitä upotettu polku ei tee
Rajat on syytä ilmoittaa selvästi. Vihjeistystä (hinting) ei suoriteta — ääriviivat täytetään luodussa muodossaan, mikä ei eroa vihjeistetystä tulosteesta vähintään 150 DPI:n tarkkuudella, mutta voi poiketa pikselin verran vihjeistetystä rasterointilaitteesta erittäin pienillä koilla. Alkuperäisiä Type 1 -ohjelmia FontFile-tiedostossa (eexec-salatut merkkijonot) ei tulkita, eikä OpenType-muuttujafonttien (variable-font) akseleita sovelleta. Molemmissa tapauksissa, aivan kuten vaurioituneen fonttiohjelman tai ilman käyttökelpoista cmap-taulukkoa olevan glyf-taulukon kohdalla, palataan käyttöjärjestelmän fonttien piirtämiseen sivun epäonnistumisen sijaan. Sama tarkkuus edellä -lähestymistapa ulottuu muuallekin piirtomoottorissa — akselinsuuntaiset ja säteittäiset varjostuskuviot saavat saman käsittelyn kuin liukuvärit ansaitsevat — ja luontipuolella on oma fonttien yksityiskohtaisuustarinansa siinä, miten EndDoc järjestää fonttien alijoukot
Piirtoputken käyttäminen ei vaadi lainkaan fonttikohtaista koodia — jokainen yllä oleva mekanismi kytkeytyy automaattisesti päälle sivunpiirtokutsun sisällä
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Upotetut TrueType-, CFF- ja Type 3 -fontit piirtyvät itse tiedostosta —
// tälle koneelle ei tarvitse asentaa mitään
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Käytännön seuraus on se, josta tukipalvelusi välittää: PDF, joka kantaa fonttinsa mukanaan, piirtyy kyseisillä fonteilla käännöspalvelimella, Windows-säiliössä tai asiakkaan työpöydällä, joka ei ole koskaan nähnyt kyseistä kirjasintyyppiä. Upotettu merkkien piirtoputki toimitetaan osana HotPDF Component -tuotetta Delphille ja C++Builderille — GDI-vektorigrafiikkaan perustuva VCL-kirjasto, joka kattaa PDF-luonnin, muokkauksen, tekstin purkamisen ja sivujen piirron ilman ulkoisia DLL-riippuvuuksia