Technický článok

Plné zarovnanie PDF textu v Delphi s HotPDF

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