Artykuł techniczny

PDFlibPas: dzielenie wyrazów i wyważone kolumny w Delphi

PDFlibPas rozmieszcza zachowany fragment tekstu w od jednej do 64 kolumn o równej szerokości za pomocą DrawTextFlowColumns i łamie jego wiersze przy użyciu ograniczonego dzielenia wyrazów zależnego od języka, gdy tylko wywołasz SetTextFlowLanguage i SetTextFlowHyphenation. Obsługiwanych jest dziewięć języków, a język można odziedziczyć z wartości /Lang katalogu dokumentu zamiast ustawiać go osobno dla każdego przepływu

Obie funkcje istnieją z tego samego powodu: wąska kolumna to miejsce, w którym naiwne łamanie wierszy przestaje wyglądać jak skład i zaczyna wyglądać jak zgłoszenie błędu

Dlaczego tekst wyjustowany rozpada się w wąskich kolumnach?

Ponieważ justowanie rozprowadza pozostałą przestrzeń w odstępy między wyrazami w wierszu, a ilość tej przestrzeni zależy od tego, co się zmieści. Przy szerokiej kolumnie nadmiar jest niewielki i oko go nie zauważa. Zmniejsz szerokość o połowę, a jedno długie słowo, które się nie zmieści, przechodzi do następnego wiersza, zostawiając poprzednie słowa do wchłonięcia całej tej przestrzeni. Trzy takie wiersze pod rząd tworzą pionowe białe kanały, które typografowie nazywają rzekami, a czytelnicy odbierają jako tekst trudny do śledzenia, nie wiedząc dlaczego

Dzielenie wyrazów usuwa przyczynę zamiast objawu, pozwalając na złamanie wewnątrz słowa. Niemieckie i niderlandzkie złożenia sprawiają, że jest to nieodzowne: 24-znakowy rzeczownik w 60-milimetrowej kolumnie nie ma dobrego rozwiązania bez punktu podziału. Angielski toleruje brak dzielenia lepiej, dlatego produkty projektowane najpierw pod język angielski często wysyłają kod układu, który wywraca się przy pierwszym niemieckim kliencie

Które języki i skąd bierze się język?

Dzielenie wyrazów obejmuje angielski, niemiecki, niderlandzki, francuski, hiszpański, włoski, portugalski, rosyjski i turecki. Ustaw go jawnie dla przepływu przez SetTextFlowLanguage albo pozwól, aby został odziedziczony z wpisu /Lang katalogu dokumentu, czyli wartości, którą oznaczony i dostępny dokument już posiada

To dziedziczenie warto wykorzystać zamiast je nadpisywać. Dokument, który deklaruje swój język w katalogu, przekazuje ten sam fakt czytnikom ekranu, indeksującym wyszukiwarkom i mechanizmowi dzielenia wyrazów z jednego miejsca, a jedno miejsce to właśnie tam, gdzie fakt powinien mieszkać. Jeśli już generujesz oznaczone wyjście opisane w automatycznym tagowaniu dla dostępnych plików PDF, wpis języka jest już ustawiony, a przepływ może po prostu z niego skorzystać

var
  Lib: TPDFlib;
  Flow, Drawn: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('Georgia', 1);
    Lib.SetTextSize(10.5);

    Flow := Lib.NewTextFlow(ArticleBody);
    try
      Lib.SetTextFlowLanguage(Flow, 'de');
      // Włączone, co najmniej 3 znaki przed podziałem, 3 po
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // nigdy nie zostawiaj osamotnionego wiersza

      repeat
        // Trzy kolumny w obszarze 480 pt, 18 pt marginesu wewnętrznego, wyważone
        Drawn := Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);
        if (Drawn = 0) or (Lib.TextFlowFinished(Flow) = 1) then
          Break;
        Lib.NewPage;
      until False;
    finally
      Lib.ReleaseTextFlow(Flow);
    end;

    Lib.SaveToFile('newsletter.pdf');
  finally
    Lib.Free;
  end;
end;

MinPrefix i MinSuffix to typografia, nie walidacja

Dwie liczby całkowite po fladze włączającej ustawiają minimalną liczbę znaków, które muszą pozostać przed i po podziale. Trzy i trzy to konserwatywna wartość domyślna akceptowana przez większość reguł firmowych. Dwa i dwa daje więcej możliwości podziału i zauważalnie brzydsze rezultaty, ponieważ dwuliterowy fragment zwisający na końcu wiersza czyta się jak literówka

Podnoś minimalne wartości, gdy rozmiar czcionki jest duży, bo wtedy każdy fragment jest wizualnie wyeksponowany, a obniżaj je tylko wtedy, gdy kolumna jest naprawdę wąska i zdecydowałeś, że ciasny skład liczy się bardziej niż czysty. To decyzja stylu firmowego, a nie techniczna, i właśnie dlatego jest to parametr, a nie stała

Co właściwie oznacza tu „wyważenie”?

Parametr Balance zmienia zachowanie tylko na końcu fragmentu. Przy włączonym wyważaniu kolumny są skracane do dokładnie równej liczby wierszy, gdy wszystko, co pozostało, mieści się w obszarze, co właśnie zapobiega sytuacji, w której ostatnia strona pokazuje dwie pełne kolumny i trzecią z jednym osamotnionym wierszem. Gdy fragment się nie mieści, każda kolumna zachowuje pełną wysokość, więc strona niesie tyle tekstu, ile może, a reszta trafia na kolejną stronę

Ta asymetria jest właściwym ustawieniem domyślnym dla dokumentów ciągłych. Wyważanie w środku płynącego artykułu marnowałoby przestrzeń pionową na każdej stronie dla kosmetycznego efektu, którego nikt nie widzi, ponieważ kolumny i tak są pełne. Wyważanie na końcu to miejsce, gdzie oko rzeczywiście to zauważa, i właśnie tam ma ono zastosowanie

Łamanie wierszy mierzy całe słowa

Algorytm łamania mierzy kompletne słowa zamiast sumować szerokości znaków i rezerwuje ograniczone przeszukiwanie dla nadmiarowo dużych tokenów, które w ogóle nie mieszczą się w wierszu, takich jak adres URL czy numer referencyjny. Dzięki temu typowy przypadek pozostaje szybki, a przypadek patologiczny ograniczony, a nie odwrotnie

Miękkie łączniki opcjonalne i automatyczne łączniki są rysowane tylko wtedy, gdy oznaczony przez nie podział jest tym, który faktycznie zostaje wybrany. Brzmi to oczywiście, a jest klasycznym błędem: naiwna implementacja zapisuje znak łącznika już podczas mierzenia, a jeśli podział się przesunie, łącznik zostaje w środku wiersza. Nic nie wygląda bardziej jak zepsuty silnik tekstowy niż zabłąkany łącznik w środku słowa

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // Zdecyduj o układzie, zanim cokolwiek narysujesz
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // Liczba wierszy potrzebnych na resztę fragmentu przy szerokości jednej kolumny
    Needed := Lib.MeasureTextFlow(Flow, 148);
    if Needed > 3 * LinesPerColumn then
      UseTwoPageSpread
    else
      UseSinglePage;

    Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);

    if Lib.TextFlowFinished(Flow) <> 1 then
      CarryOver(Lib.GetTextFlowRemaining(Flow));
  finally
    Lib.ReleaseTextFlow(Flow);
  end;
end;

Zachowaj identyczne ustawienia czcionki we wszystkich polach

Jedna zasada rządzi każdym układem opartym na przepływie i warto powiedzieć to wprost: DrawTextFlow, DrawTextFlowColumns i MeasureTextFlow łamią wiersze, używając czcionki wybranej w chwili ich wywołania. Zmień czcionkę lub rozmiar między dwoma polami tego samego przepływu albo rozpocznij nową stronę bez ponownego wyboru czcionki, a drugie pole złamie się inaczej, niż wynikało z pomiaru pierwszego

Objaw jest wyjątkowo irytujący właśnie dlatego, że wygląda na przerywany: tekst, który mieści się na pierwszej stronie, wylewa się na drugiej, albo zmierzona liczba wierszy nie zgadza się z tym, co zostało narysowane. Wybierz czcionkę raz przed pętlą, wybierz ją ponownie po każdym NewPage, a przepływ będzie zachowywał się poprawnie. Gdy w tym samym fragmencie pojawiają się mieszane pisma, rozwiązanie opisane w automatycznym fallbacku czcionek dla tekstu CJK i emoji ma zastosowanie zarówno do pomiaru, jak i do rysowania, więc szerokości pozostają spójne również w przebiegach fallbacku

Dla układów raportów, gdzie przepływ jest jednym elementem obok nagłówków, stopek i bloków sterowanych danymi, wzorce kompozycji z silnika raportów opartego na zbiorze danych łączą się bez problemu z kolumnami przepływu: najpierw zmierz, umieść stały element strony, a potem daj przepływowi cały pozostały obszar

PDFlibPas to biblioteka PDF dla Delphi, C++Builder i Lazarus, a pełny cykl życia TextFlow, czyli tworzenie, rysowanie, pomiar, inspekcję, przewijanie wstecz i zwalnianie, jest udostępniony również przez interfejsy DLL i ActiveX. Pełna dokumentacja znajduje się na stronie biblioteki PDFlibPas dla Delphi