Artykuł techniczny

Justowanie tekstu w PDF w Delphi z HotPDF

Justowanie pełne to układ, w którym kolumna tekstu wyrównana jest jednocześnie do lewej i do prawej krawędzi - efekt znany z drukowanych książek i formalnych raportów. Łatwo go opisać, a zaskakująco łatwo się pomylić, bo odpowiedź na pytanie "gdzie trafi dodatkowa spacja" jest inna dla języka angielskiego, a inna dla japońskiego. Ponadto naiwny sposób pomiaru każdego wiersza może drastycznie spowolnić generowanie strony. HotPDF oferuje justowanie uwzględniające typ pisma przez jedno wywołanie układu pudełkowego, a pod spodem kryje się godna uwagi optymalizacja wydajności

Ten artykuł omawia obydwa aspekty. Po pierwsze, zasadę typograficzną decydującą o rozkładzie pustej przestrzeni w pismach z odstępami między słowami i w pismach bez nich. Po drugie, zmianę sposobu pomiaru, która zredukowała koszt justowania na stronie blisko osiemdziesięciokrotnie bez żadnej widocznej różnicy w wynikach. Oba zagadnienia są ważne, gdy generujesz dokumenty na dużą skalę i chcesz, by wyglądały jak prawdziwy skład typograficzny, a nie rozciągnięty na całą szerokość tekst równomierny

Czego faktycznie wymaga justowanie pełne

Wiersz tekstu narysowany naturalną szerokością niemal nigdy nie sięga prawej krawędzi kolumny. Zawsze pozostaje reszta - luz - między miejscem, gdzie kończy się ostatni glif, a granicą kolumny. Wyrównanie do lewej zostawia ten luz po prawej. Wyrównanie do prawej przesuwa go na lewą. Wyśrodkowanie dzieli go na pół. Justowanie pełne usuwa ten luz, rozciągając sam wiersz, aż obie krawędzie zetkną się z pudełkiem - a jedynym rzetelnym sposobem jest rozsuwanie glifów od wewnątrz

Reguła odróżniająca dobre justowanie od złego to kwestia miejsca, gdzie umieszczamy luz. Pismo zapisujące słowa ze spacjami między nimi - jak angielski i cała rodzina łacińska - ma naturalne spoiny przy każdej przestrzeni między słowami. Poszerzanie tych przestrzeni jest niewidoczne dla oka, bo czytelnicy akceptują zmienną szerokość odstępów między słowami. Pismo zapisujące bez odstępów między słowami - chińskie znaki Han, japońska kana czy koreański Hangul - takich spoin nie ma. Tu luz trzeba rozkładać równomiernie między sąsiadującymi glifami, co jest zasadą zwaną przez japońskich zecerów kintou-waritsuke, czyli równomiernym rozmieszczeniem. Stosowanie łacińskiego rozciągania odstępów między słowami w wierszu CJK, albo wrzucanie całego luzu w jedno miejsce, gdzie wiersz CJK akurat zawiera spację, daje rzeki i dziury charakterystyczne dla amatorskiego składu

Jak HotPDF decyduje, gdzie trafia przestrzeń

HotPDF podejmuje tę decyzję dla każdej szczeliny osobno, nie dla całego wiersza. Justując wiersz, przechodzi przez każdą sąsiadującą parę glifów i sprawdza, czy między nimi istnieje rozciągalna granica. Granica jest rozciągalna, gdy po jednej stronie jest spacja lub tabulator - przypadek łaciński - albo gdy po obu stronach są znaki z możliwością podziału CJK - przypadek równomiernego rozmieszczenia. Zlicza te granice, dzieli luz wiersza równo między nie i dodaje przypadającą część do każdej kwalifikującej się szczeliny

Konsekwencje wynikają z tego naturalnie. Wiersz angielski ma rozciągalne granice tylko przy odstępach między słowami, więc cały luz trafia tam i słowa rozchodzą się, podczas gdy litery wewnątrz każdego słowa zachowują naturalny odstęp. Wiersz złożony ze znaków Han lub kany ma rozciągalną granicę między niemal każdą parą glifów, więc luz rozkłada się równomiernie na cały wiersz - dokładnie taki równomierny odstęp między glifami, jakiego te pisma wymagają. Wiersz będący jednym długim słowem łacińskim bez wewnętrznych spacji nie ma żadnej rozciągalnej granicy, więc HotPDF zostawia go o naturalnej szerokości, zamiast rozrywać słowo litera po literze. Ta sama logika obsługuje mieszane ciągi łacińskie i CJK w jednym wierszu bez specjalnych przypadków, bo decyzja jest lokalna dla każdej granicy

Jedna granica jest celowo wszędzie wykluczona. Pozycja za ostatnim glifem wiersza nigdy nie jest traktowana jako szczelina, bo rozciąganie tam jedynie przywróciłoby resztkę po prawej stronie - co jest przeciwieństwem justowania

Dlaczego ostatni wiersz jest pozostawiany w spokoju

Ostatni wiersz akapitu jest wyjątkowy, a błędna obsługa go to najczęstszy błąd justowania. Ostatni wiersz akapitu jest zwykle krótki, często zawiera zaledwie kilka słów, a rozciągnięcie go do pełnej szerokości kolumny wlecze te słowa przez całą stronę, tworząc rzadki, rozbity rząd. Prawidłowa typografia zostawia ostatni wiersz o naturalnej szerokości, wyrównany do lewej

HotPDF wykrywa końcowy wiersz na podstawie pozycji. Zawijając tekst w wiersze, wie, kiedy właśnie oddzielony wiersz sięga końca dostarczonego ciągu. Ten ostatni wiersz jest emitowany z prostym wyrównaniem do lewej i zachowuje naturalną szerokość. Wszystkie wcześniejsze wiersze są justowane do obu krawędzi. Twarde podziały wiersza zapisane w tekście są honorowane tak, jak są, więc celowo krótki wiersz również nie jest rozciągany. Czytelnik widzi czysty prostokątny blok tekstu, którego ostatni wiersz kończy się naturalnie - a tego właśnie oczekuje oko

Koszt pomiaru, który sprawiał, że justowanie było wolne

Aby justować wiersz, trzeba znać jego dokładną szerokość i advance każdego glifu, by móc precyzyjnie rozmieścić dodatkową przestrzeń. Pierwsza implementacja pobierała te liczby w oczywisty sposób. Mierzyła cały wiersz pełnym zapytaniem o szerokość Unicode, a następnie mierzyła kolejne prefiksy, by odtworzyć advance każdego glifu przez różnicowanie. Dla wiersza N glifów to N+1 wywołań do silnika pomiarowego, a każde z nich jest pełną komunikacją z GDI - pytaniem do systemu operacyjnego o układ i pomiar tekstu oraz odebraniem odpowiedzi

Na wiersz brzmi to tanio. Na stronie już nie. Weź gęstą stronę A4 z tekstem bloku, mniej więcej czterdzieści pięć wierszy po około osiemdziesiąt znaków. Przy N+1 wywołaniach na wiersz to około 81 wywołań dla każdego wiersza i blisko 3 645 dla całej strony - niemal wszystkie spędzone na ponownym mierzeniu tekstu, który silnik oglądał już chwilę wcześniej. Przy zadaniu wsadowym generującym tysiące stron ten narzut dominuje czas składu, a każda komunikacja przekracza granicę między procesem a podsystemem graficznym

Jedno wywołanie zamiast N plus jeden

Poprawka to zmiana, która wygląda drobnie, a przynosi duże zyski. GDI potrafi już raportować łączną szerokość ciągu i pozycję każdego glifu w jednym zapytaniu. HotPDF udostępnia to przez GetWideCharAdvances, które wypełnia tablicę naturalnym advanceem każdego glifu - wraz z kerningiem - i zwraca łączną szerokość, w jednym wywołaniu zamiast N+1. Procedura justowania, wewnętrznie _HPDFEmitJustifiedWideLine, pobiera wszystkie advance'y jednorazowo, oblicza luz, rozkłada go po rozciągalnych granicach i emituje wiersz

Dla tej samej strony A4 pomiar na wiersz spada z około 81 wywołań do jednego, więc strona schodzi z blisko 3 645 wywołań do około 45 - redukcja niemal osiemdziesięciokrotna. Wynik jest bajt w bajt identyczny, bo nic w pomiarze się nie zmieniło poza liczbą żądań. Ten sam silnik GDI, te same metryki czcionek, ten sam kerning dostarczają tych samych liczb. Spada tylko liczba wywołań. Gdy pomiar jest już prawidłowy, właściwą optymalizacją jest zaprzestanie wielokrotnego pytania o to samo - nie przybliżanie

Jak wiersz trafia na stronę

Po rozdzieleniu luzu HotPDF emituje wiersz przez ExtTextOut z tablicą advance'ów na glif - tablicą Dx. Każdy wpis to odległość od punktu początkowego jednego glifu do następnego, czyli naturalny advance tego glifu powiększony o jego udział w luzie, gdy po nim następuje rozciągalna granica. To bezpośrednio odwzorowuje się na model obrazowania PDF. Tekst pozycjonowany jest zapisywany operatorem TJ - tablicą przeplatającą ciągi glifów z jawnymi przesunięciami poziomymi - a wartości Dx stają się dokładnie tymi przesunięciami. Dlatego dodatkowa przestrzeń ląduje między glifami w precyzyjnych pozycjach podpunktowych, a nie jest symulowana znakami wypełniającymi, i dlatego uzasadniony wiersz HotPDF mierzy się poprawnie, gdy narzędzie niższego poziomu go odczytuje

ExtTextOut nie wywołujesz sam dla justowanych akapitów. Punktem wejścia jest WideTextOutBox, które zawija ciąg Unicode w pudełko i stosuje wybrane wyrównanie. Dzieli tekst na wiersze pasujące do szerokości pudełka, umieszcza każdy wiersz wzdłuż wysokości pudełka i zwraca liczbę znaków, które zdążyło pomieścić, zanim skończyło się pionowe miejsce. Wyrównanie wybiera enumeracja justowania

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

Pierwsze trzy to oczywiste wyrównanie do lewej, wyśrodkowanie i wyrównanie do prawej. Czwarte, jtJustify, to pełne justowanie do obu krawędzi opisane tutaj - wartość, którą WideTextOutBox odczytuje, by włączyć odstępy uwzględniające pismo

Justowanie akapitu w praktyce

Pełny przykład tworzy dokument, ustawia czcionkę i wlewa akapit do pudełka z pełnym justowaniem. Ten sam kod justuje tekst łaciński i CJK bez żadnej zmiany flagi, bo świadomość pisma tkwi poniżej 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;

Aby narysować ten sam blok wyrównany do lewej, wyśrodkowany lub wyrównany do prawej, wystarczy zmienić tylko ostatni argument na jtLeft, jtCenter lub jtRight. Zawijanie, rozmieszczenie wierszy i wartość zwracana pozostają takie same. Zmierzona szerokość, która napędza wszystkie cztery ścieżki, pochodzi z GetWideTextWidth - zapytania o szerokość uwzględniającego Unicode, które poprawnie mierzy WideString tam, gdzie starszy pomiar bajtowy mylnie wylicza rozmiar wszystkiego poza Latin-1. To właśnie sprawia, że pudełko zawija tekst CJK i znaki zastępcze par surogatów w odpowiednim miejscu

Justowanie jest jedną warstwą większego stosu kształtowania tekstu. Gdy wiersz zawiera pisma porządkujące lub łączące glify, decyzje dotyczące odstępów opisane tutaj siedzą na wierzchu pracy opisanej w artykule o kształtowaniu tekstu złożonego pisma, a gdy czcionka zawiera warianty typograficzne, które chcesz wybrać, zapoznaj się z obsługą wariantów stylistycznych OpenType GSUB. Wszystko to wchodzi w skład komponentu HotPDF dla Delphi i C++Builder, wraz z szerszymi API do tekstu, składu i dokumentów omówionymi w tym blogu