PDF, joka näyttää täydelliseltä omalla koneellasi mutta renderöityy rivinä tyhjiä laatikkoja jonkun toisen koneella, on asiakirjaohjelmistojen yleisin fonttivika, ja se ei melkein koskaan tarkoita, että teksti olisi väärin. Merkit (characters) ovat koskemattomia, koodaus on kunnossa, glyyfejä (glyphs) yksinkertaisesti ei ole siellä. Se, mikä muuttui näiden kahden koneen välillä, on se, mitkä fontit käyttöjärjestelmään on asennettu, ja kuilu siirrettävän (portable) ja hauraan (fragile) tiedoston välillä on yksi päätös, joka tehtiin sivua kirjoitettaessa: matkustiko fontti PDF:n sisällä vai oletettiinko sen olevan läsnä vastaanottavassa päässä
Tämän ymmärtäminen, ja se miksi erillinen vika tuottaa tekstin, joka näyttää haettavalta mutta kopioituu ulos siansaksana, tarkoittaa sen tarkastelemista, miten PDF tallentaa tekstiä. Se ei tallenna lauseita. Se tallentaa glyyfikoodit (glyph codes) plus fonttiohjelman plus taulukot, jotka mappauttavat ne toisiinsa, ja jokainen renderöinti- tai poimintabugi asuu näiden kolmen välisessä kuilussa. Seuraavassa on kiertomatka tuohon koneistoon, perustuen ISO 32000 -standardiin, sekä Delphi-kutsut, jotka ohjaavat sitä siellä, missä niillä on merkitystä
Merkit (characters), koodit (codes) ja glyyfit (glyphs) ovat kolme eri asiaa
Sanasto saa ihmiset kompastumaan, koska arkipuhe typistää kolme erillistä ideaa sanaan "kirjain". Merkki on kirjoittamisen abstrakti yksikkö, idea isosta A:sta, joka on yksilöity Unicodessa koodilla U+0041. Glyyfi on piirretty muoto, käyrä-ja-varsi-ääriviiva (curve-and-stem outline), jota tietty fontti käyttää kuvaamaan kyseistä merkkiä. Niiden välissä istuu koodi: tavu tai tavut sisältövirrassa (content stream), jotka kertovat katseluohjelmalle (viewer), mikä glyyfi nykyisestä fontista on tarkoitus maalata
PDF toimii koodeissa. Kun sisältövirta näyttää merkkijonon, nuo tavut ovat indeksejä (indices) aktiiviseen fonttiin, eivät Unicodea. Fontin koodaus päättää, että koodi 65 tarkoittaa "piirrä glyyfi, joka on arkistoitu kohtaan 65", eikä mikään kyseisessä operaatiossa tiedä, että tulos näyttää ihmisen silmään A:lta. Tämä on se, mikä saa PDF:n renderöitymään identtisesti kaikkialla, missä se löytää glyyfit, ja siksi myös tekstin poimiminen (extraction) on erillinen ongelma sen näyttämisestä: piirtäminen tarvitsee vain koodista-glyyfiin (code-to-glyph), kun taas lukeminen tarvitsee koodista-Unicodeen (code-to-Unicode), ja nuo ovat kaksi eri taulukkoa, jotka voivat olla ristiriidassa tai puuttua toisistaan riippumatta
Fonttityypit, joita todella kohtaat
ISO 32000 määrittelee useita fonttisanakirjatyyppejä, ja käytännössä asiakirja, jonka vastaanotat tai luot, käyttää yhtä kolmesta. Sen tietäminen, mitä niistä katsot, selittää suurimman osan siitä, mikä voi mennä pieleen
Type 1 on Adoben alkuperäinen PostScript-ääriviivaformaatti, joka on rakennettu kuutio-Bézier-käyristä. Ne neljätoista standardifonttia, jotka jokaisen standardinmukaisen lukijan on tarjottava – Helvetica, Times, Courier, Symbol ja ZapfDingbats -perheet – ovat Type 1:tä, ja niistä yhden nimeävä fonttisanakirja voi laillisesti jättää fonttiohjelman pois. Tämä on se ainoa tapaus, jossa fontin jättäminen upottamatta on turvallista spesifikaation eikä pelkän tuurin varassa. Minkä tahansa muun Type 1 -kirjasimen (face) kohdalla ohjelma on upotettava, tai katseluohjelma korvaa sen jollakin, yleensä metrisesti (metrically) samankaltaisella mutta visuaalisesti eri fontilla
TrueType käyttää toisen asteen (quadratic) käyriä ja on peräisin Apple- ja Microsoft-maailmasta. Se on sitä, mitä useimmat järjestelmäfontit ovat, ja sitä mitä upotat kaikkein useimmin. Yksinkertainen TrueType-fontti PDF:ssä on rajoitettu yksitavuisiin koodeihin, joten yksi sellainen fontti voi osoittaa (address) korkeintaan 256 glyyfiä kerrallaan. Tämä yläraja on rakenteellinen syy siihen, miksi CJK- ja muita suuria kirjoitusjärjestelmiä ei voida kuljettaa yksinkertaisella fontilla
Type 0, eli yhdistelmä- tai CID-pohjainen fontti (composite or CID-keyed font), on ratkaisu tuohon rajoitukseen. Se käyttää monitavuisia koodeja ja CMapia ohjatakseen (route) ne alisteisen (descendant) CIDFontin kautta, jonka omat ääriviivat ovat itse joko TrueTypea tai CFF/Type 1:tä. Tämä on ainoa fonttityyppi, joka voi kantaa tuhansia glyyfejä, joten mikä tahansa PDF, joka sisältää kiinaa, japania, koreaa tai laajan monikielisen kirjon (multilingual mix), käyttää Type 0 -fonttia riippumatta siitä, ajatteliko kirjoittaja sitä vai ei. Tämän kompromissi on monimutkaisuus: enemmän liikkuvia osia, joista useamman on oltava oikein sekä renderöinnin että poimimisen onnistumiseksi

Yksi yksityiskohta tämän kuvan takana ohjaa tiedostokokoa. Fontti on ääriviivaston (outlines) kirjasto, ei kiinteäkokoisia (fixed-size) bittikarttoja, joten sama upotettu ohjelma palvelee kaikkia pistekokoja (point size) sivulla. Skaalaus on muunnos, jota sovelletaan piirtohetkellä (draw time), minkä vuoksi otsikko ja sen leipäteksti (body text) jakavat yhden upotetun kirjasimen, ja upottamisen kustannus lankeaa fonttia, ei kokoa kohti
Upottaminen (embedding) on ero siirrettävän (portable) ja hauraan (fragile) välillä
Upottaminen tarkoittaa sitä, että fonttiohjelma, varsinainen ääriviivadata, on kirjoitettu PDF-tiedostoon virtana (stream). Lukija koneella, joka ei ole koskaan kuullutkaan fontistasi, lukee nuo ääriviivat suoraan tiedostosta ja piirtää tarkalleen oikeat glyyfit. Jos hyppäät upottamisen yli, lyöt vetoa sen puolesta, että määränpäässä on samanniminen fontti; kun ei ole, katseluohjelma (viewer) putoaa takaisin korvikkeeseen (fallback). Standardin neljäntoista (standard fourteen) osalta tuo korvaaminen on määritelty ja harmiton. Kaikelle muulle se vaihtelee läheltä piti -osumasta (near-miss) eri kirjasimessa aina tyhjään laatikkoon saakka, kun mikään korvike ei kata kyseistä kirjoitusjärjestelmää ollenkaan
HotPDF:ssä ohjaimena toimii yksi ainoa ominaisuus (property), joka asetetaan ennen asiakirjan avaamista. FontEmbedding kertoo kirjastolle, että sen käyttämät kirjasimet (faces) tulee pakata tiedoston sisään:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Compression := cmFlateDecode;
Pdf.FontEmbedding := True; // outlines travel inside the file
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Calibri', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Järjestys ei ole vain kosmeettinen. BeginDoc on kohta, jossa HotPDF lukitsee (commits) asiakirjan rakenteen, joten FontEmbedding-ominaisuuden on oltava tosi ennen tuota kutsua. Jos asetat sen jälkikäteen, ei tule virhettä, ei varoitusta, ainoastaan tiedosto, joka lähti kaikessa hiljaisuudessa ulos ilman fonttejaan. Se on pahin mahdollinen bugi: se läpäisee jokaisen testin kehittäjän koneella, jonne fontti sattuu olemaan asennettu, ja nousee pintaan (surfaces) vasta asiakkaan koneella, jossa se ei ole
Upottaminen on myös paikka, jossa lisensointi kohtaa suunnittelun (engineering). Fonttiohjelma kantaa mukanaan lippuja (flags), jotka kuvaavat (describe), saako sen upottaa vapaasti, vain esikatselua varten vai ei ollenkaan. Näiden lippujen kunnioittaminen on sinun vastuullasi, ei renderöijän, ja "se toimi" ei ole sama asia kuin "se oli sallittua"
Osajoukotus (subsetting): upota vain käyttämäsi glyyfit
Täysi upottaminen kirjoittaa koko fonttiohjelman tiedostoon. Suuri CJK-TrueType-kirjasin voi venyä useisiin megatavuihin, ja sen upottaminen kokonaisena vain kourallisen merkkien näyttämiseksi on tuhlailevaa (wasteful) tavalla, joka kertautuu (compounds) monisivuisessa asiakirjassa. Osajoukotus (subsetting) ratkaisee tämän kirjoittamalla vain ne glyyfit, joihin asiakirja viittaa, ja sen jälkeen uudelleennimeämällä fontin kuusikirjaimisella tunnisteella (tag) ja plusmerkillä, mikä on se ABCDEF+Calibri-muoto, joka näkyy minkä tahansa osajoukotetun PDF:n fonttilistauksessa, joten lukija ei koskaan sekoita osittaista (partial) kirjasinta samannimiseen täyteen järjestelmäfonttiin
Useimmille generoiduille asiakirjoille osajoukotus on oikea oletusasetus. Se pitää tiedostokoon suhteutettuna sisältöön lähdefontin (source font) sijaan, mikä on tärkeintä suurille monikielisille (multilingual) fonteille, jotka muuten hallitsisivat tiedostoa. Ainoa varaus (caveat) on se, että osajoukko sisältää vain sen, mitä luomishetkellä käytettiin. Jos jokin prosessi myöhemmin putkessa yrittää lisätä tekstiä osajoukotettuun fonttiin, sen tarvitsemia glyyfejä ei välttämättä ole tiedostossa – tämä on todellinen rajoite jonkun toisen PDF:n inkrementaalisen muokkaamisen suhteen
Unicode-fontit ja CJK-laatikko-ongelma
Kun teksti ei ole pelkkää latinaa, yksinkertaisen fontin polku loppuu kesken, ja ratkaisu on rekisteröidä Unicodeen pystyvä (Unicode-capable) fontti eksplisiittisesti ja antaa HotPDF:n rakentaa siitä Type 0 -fontti. RegisterUnicodeTTF lataa TrueType-tiedoston sen polun perusteella; sen jälkeen rekisteröity nimi on käytettävissä SetFont-kutsussa kuten mikä tahansa muu:
Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世界 こんにちは 안녕하세요');
Pdf.EndDoc;
Kaksi asiaa tekee tämän tai rikkoo sen. Fontin on katettava merkkijonossa (string) olevat kirjoitusjärjestelmät (scripts): vain latinaa tukeva (Latin-only) TrueType ei kasvata kiinalaisia glyyfejä (Chinese glyphs) vain siksi, että pyysit sitä tekemään niin, ja tuloksena on taas tyhjiä laatikkoja, tällä kertaa siksi, että glyyfiä ei aidosti ole olemassa kyseisessä kirjasimessa (face). Lisäksi upottamisen on pysyttävä päällä (on), koska rekisteröidystä TTF:stä koottu Type 0 -fontti on merkityksetön lukijalle, joka ei löydä ääriviivoja (outlines). Sekalaiselle sisällölle kestävä valinta on laajasti kattava (broad-coverage) kirjasin, kuten Noto- ja Arial Unicode MS -perheet (jotka ovat tavallisimmat vastaukset), upotettuina ja osajoukotettuina
Oikealta vasemmalle (Right-to-left) kirjoitettavat ja monimutkaiset kirjoitusjärjestelmät lisäävät kattavuuden (coverage) päälle muotoilukerroksen (shaping layer). HotPDF tarjoaa arabialle ja heprealle RtLTextOut-komennon, joka käsittelee suunnallisen (directional) uudelleenjärjestelyn, joten välität loogisen järjestyksen (logical order) ja annat kirjaston (library) hoitaa asettelun (lay it out). Arabian kielen saaminen oikein vaatii kattavuuden, muotoilun (shaping) ja suunnan, kolme erillistä asiaa, ja siellä ilmestyvä laatikko voi tarkoittaa, että mikä tahansa näistä epäonnistui
ToUnicode-taulukko: missä kopioi-liitä (copy-paste) asuu
Kaikki yllä oleva koskee piirtämistä. Tekstin poimiminen (extraction) on peilikuva (mirror image) ja epäonnistuu omista syistään. Katseluohjelma (viewer) renderöi sivun käyttäen fontin koodista-glyyfiin-mäppäystä (code-to-glyph mapping), mutta kun käyttäjä valitsee tekstiä ja kopioi sen, katseluohjelman on muutettava nuo samat koodit takaisin Unicodeksi. Tämä käänteinen mäppäys on ToUnicode CMap, joka on fonttiin liitetty (attached) valinnainen virta (optional stream)
Kun se on läsnä ja oikein, kopioitu teksti tulee ulos oikeina merkkeinä. Kun se puuttuu tai on väärä, tai kun fontti on osajoukotettu (subsetted) kustomoiduilla glyyfikoodeilla (custom glyph codes) eikä mitään ToUnicode-taulukkoa ole kirjoitettu, sivu näyttää täydelliseltä mutta leikepöytä (clipboard) täyttyy roskalla: glyyfikoodit luetaan (read) ikään kuin ne olisivat Unicodea, mitä kustomoidusti koodatulle osajoukolle ne eivät ole. Tämä on se syy, miksi OCR-tekstikerroksella varustettu skannattu asiakirja voi olla haettavissa (searchable), kun taas huolimattoman generaattorin luoma (born-digital) digitaalinen PDF ei ole. Renderöinti ja poiminta ammentavat eri taulukoista, joten tiedosto voi tyydyttää toisen ja epäonnistua toisessa. Jos tekstin poimiminen on tärkeää tulosteellesi, suhtaudu oikeaan ToUnicode-karttaan (map) vaatimuksena, ja todenna (verify) se kopioimalla tekstiä ulos näytteestä sen sijaan, että luottaisit sen olevan siellä
Kuinka diagnosoida fonttibugi nopeasti
Vikaantumistila (failure mode) kertoo, mistä etsiä. Tyhjät laatikot toisella koneella tarkoittavat melkein aina fonttia, jota ei upotettu, joten tarkista ensin upottaminen (embedding) ja toiseksi glyyfien kattavuus (glyph coverage). Laatikot, jotka näkyvät omallakin koneellasi, viittaavat kattavuuteen: fontti ei sisällä kyseistä kirjoitusjärjestelmää, upottamisesta riippumatta. Teksti, joka renderöityy oikein mutta kopioituu ulos järjettömänä (nonsense), on ToUnicode-ongelma, ei renderöintiongelma, eikä fonttien tai upottamisen sorkkiminen (fiddling) korjaa sitä, koska piirtäminen ei koskaan ollut rikki. Lukeaksesi valmista tiedostoa, avaa se Acrobatissa ja katso Asiakirjan ominaisuudet (Document Properties) -kohdasta Fontit: terve merkintä (healthy entry) näyttää tyypin (type), sanoo Upotettu (Embedded) tai Upotettu osajoukko (Embedded Subset) ja nimeää koodauksen. Fontti, jonka pitäisi olla upotettu ja joka ei ole, ilmoittaa itsestään siellä ennen kuin asiakas tekee sen
Mikään tästä ei ole eksoottista sen jälkeen, kun kahtiajako merkin (character), koodin (code) ja glyyfin (glyph) välillä on selvä. Upota (embed) ne fontit, joilla piirrät, tee suurista osajoukkoja (subset), tartu Unicode-kirjasimeen ja käytä komentoa RegisterUnicodeTTF sillä hetkellä, kun teksti poistuu latinasta, ja pidä ToUnicode-kartta oikein, jos kukaan tulee koskaan poimimaan (extract) tekstiä. Kun saat nämä oikein, laatikot lakkaavat ilmestymästä. Ympäröivän mekaniikan osalta minimaalisen PDF:n anatomia näyttää, missä kohtaa fonttisanakirja (font dictionary) istuu objektipuussa (object tree), ja asiakirjan rakenteen läpikäynti käsittelee sen, kuinka resurssit jaetaan (shared) sivujen kesken
Tässä esitetyt SetFont-, FontEmbedding- ja RegisterUnicodeTTF-kutsut ovat osa Delphille ja C++Builderille suunnattua HotPDF-komponenttia