Műszaki cikk

Teljes sorkizárás PDF szöveghez Delphiben a HotPDF segítségével

A teljes sorkizárás az az elrendezés, amely egy oszlopnyi szöveget a bal és a jobb szélén is egyvonalba hoz – ez az a megjelenés, amelyet egy nyomtatott könyvtől vagy egy hivatalos jelentéstől elvár. Könnyű leírni, de meglepően könnyű elrontani, mert a "hová kerül a plusz hely" kérdésre a válasz nem ugyanaz az angolnál, mint a japánnál, és mert a sorok mérésének naiv módja a gyors oldalt lassúvá teszi. A HotPDF írásmód-tudatos sorkizárást biztosít egyetlen dobozelrendezés (box-layout) híváson keresztül, és e hívás mögött egy tankönyvbe illő teljesítményjavítás áll, amelyet önmagában is érdemes megérteni

Ez a cikk mindkettőt bemutatja. Először a tipográfiai szabályt, amely eldönti, hogyan oszlik el a lazaság (slack) a szóközökkel rendelkező írásmódok és a szóközök nélküliek esetében. Másodszor, azt a mérésbeli változást, amely a sorkizárás oldalankénti költségét nagyjából nyolcvanszorosára csökkentette, a kimenetben pedig nincs látható különbség. Mindkettő számít, ha nagy mennyiségben generál dokumentumokat, és azt szeretné, hogy úgy olvassák őket, mint a valódi szedést, nem pedig úgy, mint egy méretre nyújtott, rögzített szélességű (monospaced) kimenetet

Mit is követel meg valójában a teljes sorkizárás

A természetes szélességénél megrajzolt szövegsor szinte soha nem éri el az oszlopa jobb szélét. Mindig marad egy maradék (a lazaság) aközött, ahol az utolsó glifa végződik, és ahol az oszlophatár található. A balra igazítás ezt a lazaságot a jobb oldalon hagyja. A jobbra igazítás a bal oldalra mozgatja. A középre igazítás felosztja. A teljes sorkizárás eltávolítja úgy, hogy magát a sort szélesíti ki, amíg mindkét széle nem találkozik a dobozzal, és ennek az egyetlen őszinte módja az, hogy a glifákat belülről toljuk szét

A szabály, amely elválasztja a jó sorkizárást a rossztól, az, hogy hová teszi a lazaságot. Egy olyan írásmód, amely szavakat ír szóközökkel közöttük, mint például az angol és a latin család többi tagja, természetes varratokkal rendelkezik minden szóközben. Ezeknek a szóközöknek a szélesítése láthatatlan a szem számára, mert az olvasók már elfogadják, hogy a szóközei változnak. Egy olyan írásmód, amely szóközök nélkül ír, mint például a kínai han karakterek, a japán kana vagy a koreai hangul, nem rendelkezik ilyen varratokkal. Ott a lazaságot egyenletesen kell elosztani a szomszédos glifák között, ez az az elv, amelyet a japán szedők kintou-waritsuke-nek, egyenletes térköznek (even spacing) neveznek. A latin stílusú szóköznyújtás CJK sorra helyezése, vagy az összes lazaság bezsúfolása egyetlen helyre, ahol egy CJK sor véletlenül szóközt tartalmaz, olyan folyókat (rivers) és hézagokat hoz létre, amelyek az amatőr kimenetet jelzik

Hogyan dönti el a HotPDF, hová kerül a hely

A HotPDF ezt a döntést résenként (gap) hozza meg, nem pedig soronként. Amikor sorkizárttá tesz egy sort, végigmegy minden szomszédos glifapáron, és megkérdezi, hogy van-e közöttük nyújtható határ. Egy határ akkor nyújtható, ha mindkét oldalon szóköz vagy tabulátor van (ez a latin eset), vagy ha mindkét oldalon CJK törhető karakter van (ez az egyenletes térköz esete). Megszámolja ezeket a határokat, elosztja a sor lazaságát egyenlően közöttük, és hozzáadja ezt a részt minden minősített réshez

A következmény természetesen adódik. Egy angol sornak csak a szóközöknél vannak nyújtható határai, így minden lazaság oda kerül, és a szavak széttolódnak, míg a szavakon belüli betűk megtartják természetes térközüket. Egy han vagy kana sornak szinte minden glifapár között van egy nyújtható határa, így a lazaság egyenletesen oszlik el az egész soron – pontosan az az egyenletes glifaközi térköz, amelyet ezek az írásmódok megkövetelnek. Egy olyan sornak, amely egyetlen hosszú latin szó, belső szóköz nélkül, egyáltalán nincs nyújtható határa, így a HotPDF a természetes szélességében hagyja ahelyett, hogy betűről betűre széttépné a szót. Ugyanez a logika kezeli a vegyes latin és CJK szakaszokat egy soron belül különleges eset (special-casing) nélkül, mert a döntés minden határra lokális

Egy határt szándékosan mindenhol kizárunk. A sor utolsó glifája utáni pozíciót soha nem kezeljük résként, mert az ottani nyújtás csak egy jobb oldali maradékot vezetne be újra, ami ellentétes a sorkizárással

Miért hagyjuk békén az utolsó sort

A bekezdés utolsó sora különleges, és ennek elrontása a leggyakoribb sorkizárási hiba. A bekezdés utolsó sora általában rövid, gyakran csak néhány szó, és a teljes oszlopszélességre történő kinyújtása egy ritka, törött sorként húzza át ezeket a szavakat az oldalon. A helyes tipográfia az utolsó sort természetes szélességében hagyja, balra igazítva

A HotPDF a zárósort a pozíciója alapján ismeri fel. Ahogy a szöveget sorokba tördeli, tudja, mikor éri el a megadott karakterlánc végét az a sor, amelyet éppen leválasztott. Ezt az utolsó sort egyszerű balra igazítással bocsátja ki, és megtartja természetes szélességét. Minden előtte lévő sor mindkét szélére igazítva (sorkizárva) van. A szövegbe írt kemény sortöréseket (hard line breaks) a leírtak szerint tartja tiszteletben, így egy szándékosan rövid sort sem nyújt ki soha. Az olvasó egy tiszta téglalap alakú szövegblokkot lát, amelynek utolsó sora természetesen végződik, és ezt várja a szem is

A mérési költség, amely lassúvá tette a sorkizárást

Egy sor sorkizárttá tételéhez ismerni kell annak pontos szélességét, és ismerni kell minden glifa előrehaladását (advance), hogy a plusz helyet pontosan el lehessen helyezni. Az első megvalósítás kézenfekvő módon szerezte meg ezeket a számokat. Megmérte a teljes sort egy teljes Unicode szélesség lekérdezéssel, majd megmérte a prefixeket egymás után, hogy különbségképzéssel (differencing) visszanyerje az egyes glifák előrehaladását. Egy N glifából álló sornál ez N+1 hívás a mérőmotorba, és minden hívás egy teljes GDI oda-vissza út (round-trip), amely arra kéri az operációs rendszert, hogy formázza és mérje meg a szöveget, majd adja vissza a választ

Soronként ez olcsónak hangzik. Egy egész oldalon viszont nem az. Vegyünk egy sűrű A4-es oldalnyi törzsszöveget, nagyjából negyvenöt sorral, egyenként körülbelül nyolcvan karakterrel. N+1 oda-vissza útnál ez körülbelül 81 oda-vissza út minden sorra, és nagyjából 3645 az oldalra, amelyek szinte mindegyike olyan szöveg újramérésére megy el, amelyet a motor pillanatokkal korábban már megnézett. A több ezer oldalt előállító kötegelt feladatok (batch job) esetében ez a többletmunka uralja az elrendezési időt, és minden oda-vissza út átlépi a határt a folyamat (process) és a grafikus alrendszer között

Egy hívás az N plusz egy helyett

A javítás egy olyan változás, amely kicsinek tűnik, de nagymértékben megtérül. A GDI már egyetlen lekérdezésben is képes jelenteni egy karakterlánc teljes szélességét és minden egyes glifa pozícióját. A HotPDF ezt a GetWideCharAdvances-en keresztül teszi elérhetővé, amely kitölt egy tömböt minden glifa természetes előrehaladásával – a kerninggel együtt –, és visszaadja a teljes szélességet, N+1 helyett egyetlen hívással. A sorkizárási rutin (belső nevén _HPDFEmitJustifiedWideLine) egyszer elkéri az összes előrehaladást, kiszámítja a lazaságot, szétosztja a nyújtható határok között, és kibocsátja a sort

Ugyanazon az A4-es oldalon a soronkénti mérés körülbelül 81 oda-vissza útról egyre csökken, így az oldal nagyjából 3645 oda-vissza útról körülbelül 45-re esik vissza, ami majdnem nyolcvanszoros csökkenést jelent. A kimenet bájtról bájtra azonos, mert a mérésben nem változott semmi, kivéve azt, hogy hányszor kérjük le. Ugyanaz a GDI motor, ugyanazok a betűtípus-metrikák, ugyanaz a kerning táplálja ugyanazokat a számokat. Csak az oda-vissza utak száma csökkent. Ha egy mérés már pontos, a helyes optimalizálás az, hogy abbahagyjuk a lekérdezést ismételten, nem pedig az, hogy közelítjük (approximate) azt

Hogyan jut el a sor az oldalra

Amint a lazaságot elosztották, a HotPDF az ExtTextOut és egy glifánkénti előrehaladási tömb (a Dx tömb) használatával bocsátja ki a sort. Minden bejegyzés a távolság az egyik glifa origójától a következőig, amely az adott glifa természetes előrehaladása plusz a lazaság rá eső része, amikor egy nyújtható határ követi. Ez közvetlenül leképeződik a PDF képalkotási modellre. A pozicionált szöveget a TJ operátorral írják fel, amely egy olyan tömb, amely a glifafolyamokat (glyph runs) explicit vízszintes beállításokkal (adjustments) váltogatja, és a Dx értékek pontosan ezek a beállítások lesznek. Ez az oka annak, hogy a plusz hely a glifák között pontos alpont (sub-point) pozíciókban landol, ahelyett, hogy térkitöltő (padding) karakterekkel hamisítanák meg, és ezért mérhető egy sorkizárt HotPDF sor helyesen, ha egy későbbi eszköz olvassa vissza

Önnek nem kell magának meghívnia az ExtTextOut-ot sorkizárt bekezdésekhez. A belépési pont a WideTextOutBox, amely egy Unicode karakterláncot tördel egy dobozba, és alkalmazza az Ön által kért igazítást. A szöveget olyan sorokra osztja, amelyek illeszkednek a doboz szélességéhez, minden sort lefelé elhelyez a doboz magasságában, és visszaadja azoknak a karaktereknek a számát, amelyeket be tudott illeszteni, mielőtt kifogyott volna a függőleges helyből. Az igazítást a sorkizárási (justification) felsorolás (enum) választja ki

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

Az első három magától értetődő balra, középre és jobbra igazítás. A negyedik, a jtJustify az itt leírt teljes, mindkét szélre vonatkozó sorkizárás, és ezt az értéket olvassa a WideTextOutBox, hogy bekapcsolja az írásmód-tudatos térközt

Bekezdés sorkizárt mivoltának (justifying) gyakorlati megvalósítása

Egy teljes példa létrehoz egy dokumentumot, beállít egy betűtípust, és beönt egy bekezdést egy dobozba teljes sorkizárással. Ugyanez a kód a latin és a CJK szöveget is sorkizárttá teszi jelzőváltoztatás (flag change) nélkül, mert az írásmód-tudatosság (script-awareness) az API alatt él

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;

Ugyanannak a blokknak a balra igazított, középre igazított vagy jobbra igazított rajzolásához csak az utolsó argumentumot változtassa jtLeft, jtCenter vagy jtRight értékre. A tördelés, a sor elhelyezése és a visszatérési érték ugyanaz marad. A mért szélesség, amely mind a négy utat vezérli, a GetWideTextWidth-ből származik, abból a Unicode-tudatos szélesség lekérdezésből, amely helyesen méri a WideString-et ott, ahol a régebbi bájtonkénti mérés rosszul méretezne mindent, ami a Latin-1-en túl van, és ez az, ami a dobozt arra készteti, hogy a CJK-t és a helyettesítő páros (surrogate-pair) szöveget már az elején a megfelelő helyen tördelje

A sorkizárás egy nagyobb szövegformázó (text-shaping) verem egyik rétege. Amikor egy sor olyan írásmódokat tartalmaz, amelyek újrarendezik vagy egyesítik a glifáikat, az itteni térközdöntések (spacing decisions) a komplex írásmódok szövegformázásáról szóló cikkünkben leírt munkára épülnek (sit on top of the work), és ha egy betűtípus olyan tipográfiai változatokat hordoz, amelyeket ki szeretne választani, nézze meg, hogyan vezérelheti az OpenType GSUB stilisztikai alternatíváit. Mindezt a Delphihez és C++Builderhez készült HotPDF komponensben szállítjuk, a blogon bemutatott szélesebb szöveg, elrendezés és dokumentum API-k mellett