Tekninen artikkeli

PDF-tekstin täysi tasaus Delphissä HotPDF:n avulla

Täysi tasaus (full justification) on asettelu, joka saa tekstisarakkeen asettumaan suoraan sekä vasempaan että oikeaan reunaan, ulkonäkö, jota odotat painetulta kirjalta tai muodolliselta raportilta. Sitä on helppo kuvailla ja yllättävän helppo tehdä väärin, koska vastaus kysymykseen "minne ylimääräinen tila menee" ei ole sama englannille kuin japanille, ja koska naiivi tapa mitata jokainen rivi muuttaa nopean sivun hitaaksi. HotPDF antaa sinulle kirjoitusjärjestelmätietoisen (script-aware) tasauksen yhden laatikkoasettelu-kutsun kautta, ja tuon kutsun alla on oppikirjamainen suorituskykykorjaus, joka on ymmärtämisen arvoinen itsessään

Tämä artikkeli käy läpi molemmat. Ensinnäkin typografisen säännön, joka päättää, kuinka löysyys (slack) jaetaan kirjoitusjärjestelmille, joissa on sanavälit, verrattuna niihin, joissa ei ole. Toiseksi mittausmuutoksen, joka leikkasi tasauksen sivukohtaiset kustannukset noin kahdeksankymmenesosaan ilman näkyvää eroa tulosteessa. Molemmilla on merkitystä, jos luot asiakirjoja suurina määrinä ja haluat niiden lukevan kuin aitoa ladontaa sen sijaan, että ne olisivat venytettyä tasalevyistä tulostetta

Mitä täysi tasaus todella vaatii

Luonnollisella leveydellään piirretty tekstirivi ei melkein koskaan saavuta sarakkeensa oikeaa reunaa. Viimeisen glyyfin päättymisen ja sarakkeen rajan välissä on aina jäännös, löysyys. Vasemmalle tasaus jättää tuon löysyyden oikealle. Oikealle tasaus siirtää sen vasemmalle. Keskitys jakaa sen. Täysi tasaus poistaa sen leventämällä itse riviä, kunnes molemmat reunat kohtaavat laatikon, ja ainoa rehellinen tapa tehdä se on työntää glyyfejä erilleen sisältäpäin

Sääntö, joka erottaa hyvän tasauksen huonosta, on se, minne laitat löysyyden. Kirjoitusjärjestelmällä, joka kirjoittaa sanoja, joiden välissä on välilyöntejä, kuten englanti ja muu latinalainen perhe, on luonnolliset saumat jokaisessa sanavälissä. Näiden välien leventäminen on silmälle näkymätöntä, koska lukijat hyväksyvät jo sen, että sanavälit vaihtelevat. Kirjoitusjärjestelmässä, joka kirjoittaa ilman sanavälejä, kuten kiinalaisissa Han-merkeissä, japanilaisessa kanassa tai korealaisessa hangulissa, ei ole tällaisia saumoja. Siellä löysyys on levitettävä tasaisesti vierekkäisten glyyfien väliin, periaate, jota japanilaiset latojat kutsuvat nimellä kintou-waritsuke, tasainen välistys. Latinalaistyylisen sanavälien venyttämisen laittaminen CJK-riville tai kaiken löysyyden tunkeminen siihen yhteen paikkaan, jossa CJK-rivi sattuu sisältämään välilyönnin, tuottaa puroja ja aukkoja, jotka merkitsevät amatöörimäistä tulostetta

Kuinka HotPDF päättää, minne tila menee

HotPDF tekee tuon päätöksen väli kerrallaan, ei rivi kerrallaan. Kun se tasaa rivin, se käy läpi jokaisen vierekkäisen glyyfiparin ja kysyy, onko niiden välissä venytettävä raja. Raja on venytettävä, kun jompikumpi puoli on välilyönti tai sarkain, latinalainen tapaus, tai kun molemmat puolet ovat CJK-katkaistavia merkkejä, tasaisen välistyksen tapaus. Se laskee nuo rajat, jakaa rivin löysyyden tasan niiden kesken ja lisää tuon osuuden jokaiseen kelvolliseen väliin

Seuraus syntyy luonnollisesti. Englanninkielisellä rivillä on venytettäviä rajoja vain sanaväleissä, joten kaikki löysyys päätyy sinne ja sanat leviävät erilleen, kun taas kirjaimet kunkin sanan sisällä säilyttävät luonnollisen välistyksensä. Han- tai kana-rivillä on venytettävä raja melkein jokaisen glyyfiparin välissä, joten löysyys jakautuu tasaisesti koko riville, juuri sille tasaiselle glyyfien väliselle välistykselle, jota nuo kirjoitusjärjestelmät vaativat. Rivillä, joka on yksi pitkä latinalainen sana ilman sisäistä välilyöntiä, ei ole lainkaan venytettävää rajaa, joten HotPDF jättää sen luonnolliseen leveyteensä sen sijaan, että repisi sanan erilleen kirjain kirjaimelta. Sama logiikka käsittelee sekoitettuja latinalaisia ja CJK-jaksoja yhdellä rivillä ilman erikoistapauksia, koska päätös on paikallinen kullekin rajalle

Yksi raja jätetään tarkoituksella pois kaikkialla. Viimeisen glyyfin jälkeistä paikkaa rivillä ei koskaan käsitellä välinä, koska venyttäminen siellä toisi vain takaisin oikeanpuoleisen jäännöksen, mikä on tasauksen vastakohta

Miksi viimeinen rivi jätetään rauhaan

Kappaleen viimeinen rivi on erityinen, ja sen tekeminen väärin on yleisin tasausbugi. Kappaleen viimeinen rivi on yleensä lyhyt, usein vain muutaman sanan mittainen, ja sen venyttäminen koko sarakkeen leveyteen raahaa nuo sanat sivun poikki harvaksi, rikkonaiseksi riviksi. Oikea typografia jättää viimeisen rivin luonnolliseen leveyteensä vasemmalle tasattuna

HotPDF havaitsee perässä tulevan rivin sijainnin perusteella. Kun se rivittää tekstiä, se tietää, milloin juuri irrotettu rivi saavuttaa syötetyn merkkijonon lopun. Tuo viimeinen rivi lähetetään tavallisella vasemmalle tasauksella ja se pitää luonnollisen leveytensä. Jokainen sitä edeltävä rivi on tasattu molempiin reunoihin. Tekstiin kirjoittamiasi kovia rivinvaihtoja kunnioitetaan sellaisenaan, joten myöskään tahallista lyhyttä riviä ei koskaan venytetä. Lukija näkee siistin suorakulmaisen tekstilohkon, jonka viimeinen rivi päättyy luonnollisesti, mitä silmä odottaakin

Mittauskustannus, joka teki tasauksesta hitaan

Rivin tasaamiseksi sinun on tiedettävä sen tarkka leveys, ja sinun on tiedettävä jokaisen glyyfin eteneminen (advance), jotta voit sijoittaa ylimääräisen tilan tarkasti. Ensimmäinen toteutus sai nuo luvut ilmeisellä tavalla. Se mittasi koko rivin täydellä Unicode-leveyden kyselyllä ja mittasi sitten etuliitteen toisensa perään (prefix after prefix) palauttaakseen jokaisen glyyfin etenemisen erotuksen avulla. N glyyfin riville se on N+1 kutsua mittausmoottoriin, ja jokainen kutsu on täysi GDI-kierros (round-trip), jossa pyydetään käyttöjärjestelmää muotoilemaan ja mittaamaan teksti ja ojentamaan vastaus takaisin

Riviä kohti se kuulostaa halvalta. Sivun yli se ei ole. Otetaan tiheä A4-sivu leipätekstiä, noin neljäkymmentäviisi riviä, joilla kullakin on noin kahdeksankymmentä merkkiä. N+1 kierroksella per rivi se on noin 81 kierrosta jokaiselle riville ja karkeasti 3 645 sivulle, joista melkein kaikki kuluu sellaisen tekstin uudelleenmittaamiseen, jota moottori oli jo katsonut hetkiä aiemmin. Tuhansia sivuja tuottavassa eräajossa tuo lisätyö (overhead) hallitsee asetteluaikaa, ja jokainen kierros ylittää prosessisi ja grafiikka-alijärjestelmän välisen rajan

Yksi kutsu N plus yhden sijaan

Korjaus on sellainen muutos, joka näyttää pieneltä ja maksaa itsensä takaisin isosti. GDI voi jo raportoida merkkijonon kokonaisleveyden ja jokaisen glyyfin sijainnin yhdessä kyselyssä. HotPDF paljastaa tämän GetWideCharAdvances-toiminnon kautta, joka täyttää taulukon kunkin glyyfin luonnollisella etenemisellä, kerning mukaan lukien, ja palauttaa kokonaisleveyden yhdessä kutsussa N+1:n sijaan. Tasausrutiini, sisäisesti _HPDFEmitJustifiedWideLine, pyytää kaikkia etenemisiä kerran, laskee löysyyden, jakaa sen venytettäville rajoille ja lähettää rivin

Samalle A4-sivulle rivikohtainen mittaus putoaa noin 81 kierroksesta yhteen, joten sivu putoaa karkeasti 3 645 kierroksesta noin 45:een, mikä on lähellä kahdeksankymmenkertaista vähennystä. Tuloste on tavu tavulta identtinen, koska mikään mittauksessa ei muuttunut paitsi se, kuinka monta kertaa sitä pyydetään. Sama GDI-moottori, samat fonttimetriikat, sama kerning syöttävät samoja lukuja. Vain kierrosten määrä putosi. Kun mittaus on jo oikein, oikea optimointi on lopettaa sen pyytäminen toistuvasti, ei sen arvioiminen

Kuinka rivi saavuttaa sivun

Kun löysyys on jaettu, HotPDF lähettää rivin käyttämällä ExtTextOut-kutsua ja glyyfikohtaista etenemistaulukkoa, Dx-taulukkoa. Jokainen merkintä on etäisyys yhden glyyfin origosta seuraavaan, mikä on kyseisen glyyfin luonnollinen eteneminen plus sen osuus löysyydestä, kun venytettävä raja seuraa sitä. Tämä yhdistyy suoraan PDF-kuvantamismalliin. Sijoitettu teksti kirjoitetaan TJ-operaattorilla, taulukolla, joka lomittaa glyyfijaksoja nimenomaisilla vaakasuorilla säädöillä, ja Dx-arvoista tulee juuri noita säätöjä. Siksi ylimääräinen tila laskeutuu glyyfien väliin tarkoissa alipistepaikoissa (sub-point positions) sen sijaan, että sitä väärennettäisiin täytemerkeillä, ja siksi tasattu HotPDF-rivi mitataan oikein, jos jatkovaiheen työkalu lukee sen takaisin

Et kutsu ExtTextOut-toimintoa itse tasatuille kappaleille. Aloituskohta on WideTextOutBox, joka rivittää Unicode-merkkijonon laatikkoon ja soveltaa pyytämääsi tasausta. Se jakaa tekstin laatikon leveyteen sopiville riveille, sijoittaa jokaisen rivin alaspäin laatikon korkeudella ja palauttaa niiden merkkien määrän, jotka se onnistui sovittamaan, ennen kuin pystysuora tila loppui. Tasaus valitaan tasausenumeraattorilla

type
  THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);

Kolme ensimmäistä ovat itsestäänselvästi vasemmalle, keskelle ja oikealle tasaus. Neljäs, jtJustify, on tässä kuvattu täysi molempien reunojen tasaus, ja se on arvo, jonka WideTextOutBox lukee kytkeäkseen päälle kirjoitusjärjestelmätietoisen välistyksen

Kappaleen tasaaminen käytännössä

Täydellinen esimerkki luo asiakirjan, asettaa fontin ja kaataa kappaleen laatikkoon täydellä tasauksella. Sama koodi tasaa latinalaista ja CJK-tekstiä ilman lipun vaihtoa, koska kirjoitusjärjestelmätietoisuus asuu API:n alapuolella

uses
  HPDFDoc;

procedure JustifyParagraph;
var
  Pdf: THotPDF;
  Body: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Justified.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', 11);

    Body :=
      'Full justification spreads the slack on each filled line so both ' +
      'edges meet the column, while the last line keeps its natural width. ' +
      'For scripts with word gaps the space lands between words; for ' +
      'scripts without them it spreads evenly between glyphs.';

    // X, Y, LineSpacing, BoxWidth, BoxHeight, Text, Align
    Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Jos haluat piirtää saman lohkon vasemmalle tasattuna, keskitettynä tai oikealle tasattuna, muuta vain viimeinen argumentti muotoon jtLeft, jtCenter tai jtRight. Rivitys, rivien sijoittelu ja paluuarvo pysyvät samoina. Mitattu leveys, joka ohjaa kaikkia neljää polkua, tulee GetWideTextWidth-kutsusta, Unicode-tietoisesta leveyskyselystä, joka mittaa WideString:in oikein silloin, kun vanhempi tavukohtainen mittaus mitoittaisi väärin kaiken Latin-1:n ohi menevän, mikä saa laatikon rivittämään CJK- ja korvikepari-tekstin (surrogate-pair) oikeaan paikkaan heti alkuunsa

Tasaus on yksi kerros suurempaa tekstinmuotoilupinoa (text-shaping stack). Kun rivi sisältää kirjoitusjärjestelmiä, jotka järjestävät uudelleen tai yhdistävät glyyfejään, tässä tehdyt välistyspäätökset istuvat sen työn päällä, jota kuvataan artikkelissamme monimutkaisten kirjoitusjärjestelmien tekstin muotoilusta, ja kun fontti sisältää typografisia vaihtoehtoja, jotka haluat valita, katso kuinka ohjata OpenType GSUB -tyylivaihtoehtoja. Se kaikki toimitetaan HotPDF Componentissa Delphille ja C++Builderille tämän blogin kattamien laajempien teksti-, asettelu- ja asiakirja-API:en rinnalla