Plné zarovnanie je rozloženie, pri ktorom stĺpec textu zarovnáva na ľavom aj pravom okraji — tak vyzerá text v tlačenej knihe alebo formálnej správe. Je ľahké ho opísať, ale prekvapivo ľahké urobiť chybu, pretože odpoveď na otázku „kde skončí nadbytočné miesto" nie je rovnaká pre angličtinu ako pre japončinu, a pretože naivný spôsob merania každého riadka zmení rýchlu stránku na pomalú. HotPDF poskytuje zarovnanie s ohľadom na písmo prostredníctvom jediného volania rozloženia boxu, a pod týmto volaním sa skrýva výkonnostná oprava, ktorú stojí za to pochopiť samostatne
Tento článok prechádza oboma aspektmi. Najprv typografické pravidlo, ktoré rozhoduje, ako sa rozdeľuje medzera pre písma s medzerami medzi slovami oproti písmam bez nich. Potom zmena merania, ktorá znížila náklady na zarovnanie na stránku zhruba osemdesiaťnásobne bez viditeľného rozdielu vo výstupe. Oboje je dôležité, ak generujete dokumenty vo veľkom a chcete, aby vyzerali ako skutočná sadzba, a nie ako výstup so šírkou znakov roztiahnutý na šírku stránky
Čo plné zarovnanie skutočne vyžaduje
Riadok textu nakreslený v prirodzenej šírke takmer nikdy nedosiahne pravý okraj stĺpca. Vždy existuje zvyšok — medzera — medzi miestom, kde končí posledný glyfus, a hranicou stĺpca. Zarovnanie vľavo nechá túto medzeru vpravo. Zarovnanie vpravo ju presunie doľava. Centrovanie ju rozdelí. Plné zarovnanie ju odstráni rozšírením samotného riadka, kým oba okraje nedosiahnu box, a jediný poctivý spôsob, ako to urobiť, je od vnútra od seba odtlačiť glyfy
Pravidlo, ktoré oddeľuje dobré zarovnanie od zlého, je to, kam dáte medzeru. Písmo, ktoré píše slová s medzerami medzi nimi — napríklad angličtina a zvyšok latinskej rodiny — má prirodzené švy pri každej medzislovnej medzere. Rozširovanie týchto medzier je pre oko neviditeľné, pretože čitatelia už prijímajú, že medzery medzi slovami sa menia. Písmo, ktoré píše bez medzier medzi slovami — napríklad čínske hanské znaky, japonská kana alebo kórejský hangul — také švy nemá. Tam musí byť medzera rovnomerne rozložená medzi susednými glyfmi, čo je princíp, ktorý japonskí sadzači nazývajú kintou-waritsuke, teda rovnomerné rozdelenie. Použitie latinského roztiahnutia medzier medzi slovami na riadok CJK, alebo napchanie celej medzery na jediné miesto, kde CJK riadok náhodou obsahuje medzeru, vytvára prázdniny a medzery, ktoré signalizujú amatérsky výstup
Ako HotPDF rozhoduje, kam ide medzera
HotPDF robí toto rozhodnutie pre každú medzeru zvlášť, nie pre celý riadok. Keď zarovnáva riadok, prechádza každý susedný pár glyfov a pýta sa, či medzi nimi leží rozťahovateľná hranica. Hranica je rozťahovateľná, keď je na niektorej strane medzera alebo tabulátor — latinský prípad — alebo keď obe strany sú znaky s možnosťou zalomenia CJK — prípad rovnomerného rozdelenia. Spočíta tieto hranice, rozdelí medzeru riadka rovnomerne medzi ne a pridá tento podiel ku každej vyhovujúcej medzere
Dôsledok plynie prirodzene. Anglický riadok má rozťahovateľné hranice iba pri medzerách medzi slovami, takže všetka medzera pristane tam a slová sa od seba vzdialia, kým písmená vnútri každého slova si zachovajú prirodzené rozostupy. Riadok hanských znakov alebo kany má rozťahovateľnú hranicu medzi takmer každým párom glyfov, takže medzera sa rovnomerne rozloží po celom riadku — presne také rovnomerné medziglyфové rozostupy, aké tieto písma vyžadujú. Riadok, ktorý je jedným dlhým latinským slovom bez vnútornej medzery, nemá žiadnu rozťahovateľnú hranicu, takže HotPDF ho nechá v prirodzenej šírke namiesto toho, aby slovo trhalo písmeno po písmene. Rovnaká logika zvláda zmiešané latinské a CJK úseky v jednom riadku bez špeciálnych prípadov, pretože rozhodnutie je lokálne pre každú hranicu
Jedna hranica je zámerene vylúčená všade. Pozícia za posledným glyfom riadka sa nikdy nepovažuje za medzeru, pretože roztiahnutie tam by len znovu zaviedlo pravostranný zvyšok — čo je opak zarovnania
Prečo sa posledný riadok nechá tak
Posledný riadok odseku je špeciálny, a jeho nesprávne spracovanie je najčastejšou chybou zarovnania. Posledný riadok odseku je zvyčajne krátky — často len niekoľko slov — a jeho roztiahnutie na plnú šírku stĺpca roztáhne tieto slová naprieč stránkou do riedkeho, rozbitého radu. Správna typografia nechá posledný riadok v jeho prirodzenej šírke, zarovnaný vľavo
HotPDF deteguje posledný riadok podľa polohy. Pri zalamovaní textu do riadkov vie, keď riadok, ktorý práve oddelil, dosiahne koniec dodaného reťazca. Tento posledný riadok sa vyšle s jednoduchým zarovnaním vľavo a zachováva si prirodzenú šírku. Každý riadok pred ním je zarovnaný na oba okraje. Tvrdé zalomenia riadkov, ktoré zapíšete do textu, sú dodržané tak, ako sú napísané, takže úmyselne krátky riadok sa tiež nikdy neroztiahnne. Čitateľ vidí čistý obdĺžnikový blok textu, ktorého posledný riadok končí prirodzene — čo je to, čo oko očakáva
Náklady na meranie, ktoré spomaľovalo zarovnanie
Aby ste zarovnali riadok, musíte poznať jeho presnú šírku a musíte poznať posun každého glyfu, aby ste mohli presne umiestniť ďalší priestor. Prvá implementácia získavala tieto čísla zjavným spôsobom. Merala celý riadok pomocou úplného dopytu na šírku Unicode, potom merala predponu za predponou, aby obnovila posun každého glyfu pomocou odčítania. Pre riadok s N glyfmi to je N+1 volaní do meracieho enginu, pričom každé volanie je úplný GDI round-trip — žiadosť operačného systému o tvarovanie a meranie textu a vrátenie odpovede
Na riadok to znie lacno. Na stránku to tak nie je. Zoberme si hustú stránku A4 s hlavným textom — zhruba štyridsaťpäť riadkov s asi osemdesiatimi znakmi každý. Pri N+1 round-tripoch na riadok to je okolo 81 round-tripov na každý riadok a zhruba 3 645 na stránku, pričom takmer všetky sú strávené opätovným meraním textu, ktorý engine práve nedávno pozeral. Pri dávkovej úlohe produkujúcej tisíce stránok táto réžia dominuje čas rozloženia a každý round-trip prechádza hranicou medzi vaším procesom a grafickým subsystémom
Jedno volanie namiesto N plus jedna
Oprava je typ zmeny, ktorá vyzerá malá, ale veľa prináša. GDI už dokáže v jedinom dopyte vrátiť celkovú šírku reťazca a polohu každého glyfu. HotPDF to sprístupňuje cez GetWideCharAdvances, ktoré vyplní pole prirodzeným posunom každého glyfu vrátane kerningu a vráti celkovú šírku — v jednom volaní namiesto N+1. Rutina zarovnávania, interne _HPDFEmitJustifiedWideLine, raz požiada o všetky posuny, vypočíta medzeru, rozloží ju medzi rozťahovateľné hranice a vyšle riadok
Pre tú istú stránku A4 klesá meranie na riadok zo zhruba 81 round-tripov na jeden, takže stránka klesá z približne 3 645 round-tripov na asi 45 — blízko k osemdesiaťnásobnému zníženiu. Výstup je byte-za-byte identický, pretože na meraní sa nezmenilo nič okrem toho, koľkokrát je požadované. Rovnaký GDI engine, rovnaké metriky písma, rovnaký kerning dodávajú rovnaké čísla. Klesol iba počet round-tripov. Keď je meranie správne, správna optimalizácia je prestať ho opakovane požadovať, nie ho aproximovať
Ako sa riadok dostane na stránku
Keď je medzera rozdelená, HotPDF vyšle riadok pomocou ExtTextOut a poľa posunov na glyf — poľa Dx. Každá položka je vzdialenosť od počiatku jedného glyfu k ďalšiemu, čo je prirodzený posun tohto glyfu plus jeho podiel z medzery, ak po ňom nasleduje rozťahovateľná hranica. Toto sa priamo mapuje na model zobrazovania PDF. Umiestnený text sa zapisuje operátorom TJ — poľom, ktoré striedavo kombinuje behy glyfov s explicitnými horizontálnymi úpravami, a hodnoty Dx sa stanú presne týmito úpravami. Preto ďalší priestor pristáva medzi glyfmi na presných sub-bodových pozíciách namiesto toho, aby bol simulovaný pomocou výplňových znakov, a preto zarovnaný riadok HotPDF správne zmeria, ak ho prečíta následný nástroj
ExtTextOut sami pre zarovnané odseky nevoláte. Vstupným bodom je WideTextOutBox, ktoré zabalí reťazec Unicode do boxu a aplikuje zarovnanie, ktoré požadujete. Rozdeľuje text na riadky zodpovedajúce šírke boxu, umiestňuje každý riadok pozdĺž výšky boxu a vracia počet znakov, ktoré sa mu podarilo zmestiť pred vyčerpaním vertikálneho priestoru. Zarovnanie sa volí pomocou enumerácie zarovnania
type
THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);
Prvé tri sú samovysvetľujúce — zarovnanie vľavo, na stred a vpravo. Štvrtá, jtJustify, je plné zarovnanie na oba okraje popísané tu, a to je hodnota, ktorú WideTextOutBox číta na prepnutie na rozostupovanie s ohľadom na písmo
Zarovnanie odseku v praxi
Kompletný príklad vytvorí dokument, nastaví písmo a naleje odsek do boxu s plným zarovnaním. Rovnaký kód zarovná latinský aj CJK text bez zmeny príznaku, pretože povedomosť o písme sa nachádza pod API
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;
Ak chcete nakresliť rovnaký blok zarovnaný vľavo, na stred alebo vpravo, zmeňte iba posledný argument na jtLeft, jtCenter alebo jtRight. Zalamovanie, umiestnenie riadkov a návratová hodnota zostanú rovnaké. Nameraná šírka, ktorá riadi všetky štyri cesty, pochádza z GetWideTextWidth — dopyt na šírku s povedomosťou o Unicode, ktorý správne meria WideString, kde staršie bajtové meranie by nesprávne ohodnotilo čokoľvek za Latin-1, čo je to, čo umožňuje boxu zalomiť CJK a text s náhradnými pármi na správnom mieste
Zarovnanie je jednou vrstvou väčšieho zásobníka tvarovania textu. Keď riadok obsahuje písma, ktoré preusporiadavajú alebo spájajú svoje glyfy, rozhodnutia o rozostupovaní tu stoja na vrchole práce popísanej v našom článku o tvarovaní textu komplexných písiem, a keď písmo obsahuje typografické varianty, ktoré chcete vybrať, pozrite si ako ovládať štylistické alternatívy OpenType GSUB. Všetko je súčasťou HotPDF Component pre Delphi a C++Builder, spolu so širšími textovými, rozložovacími a dokumentovými API pokrytými v tomto blogu