Technický článek

PDFlibPas: dělení slov a vyvážené sloupce textu v Delphi

PDFlibPas rozlévá uchovaný text do jednoho až 64 stejně širokých sloupců pomocí DrawTextFlowColumns a láme řádky s ohraničeným, jazykově citlivým dělením slov, jakmile zavoláte SetTextFlowLanguage a SetTextFlowHyphenation. Podporováno je devět jazyků a jazyk lze místo nastavování pro každý flow zdědit z hodnoty /Lang v Catalogu dokumentu

Obě funkce existují ze stejného důvodu: úzký sloupec je místo, kde naivní lámání řádků přestává vypadat jako sazba a začíná vypadat jako hlášení chyby

Proč se zarovnaný text v úzkých sloupcích rozpadá?

Protože zarovnání do bloku rozprostírá zbylý prostor do mezer mezi slovy na řádku, a kolik prostoru zbude, závisí na tom, co se na řádek vejde. Při širokém sloupci je přebytek malý a oko si ho nevšimne. Zúžíte-li šířku na polovinu, jediné dlouhé slovo, které se nevejde, se přesune na další řádek a slova před ním musí pohltit veškerý ten prostor. Tři takové řádky za sebou vytvoří svislé bílé kanály, kterým typografové říkají řeky, a čtenáři je vnímají jako text, který se špatně čte, aniž by věděli proč

Dělení slov řeší příčinu, ne příznak, tím, že povolí zlom uvnitř slova. Německé a nizozemské složeniny to dělají nevyhnutelným: 24znakové podstatné jméno v 60milimetrovém sloupci nemá bez místa pro zlom žádné dobré řešení. Angličtina snáší absenci lépe, a proto produkty vyvíjené primárně pro angličtinu často dodávají kód rozvržení, který se zhroutí hned při prvním německém zákazníkovi

Které jazyky a odkud se jazyk bere?

Dělení slov pokrývá angličtinu, němčinu, nizozemštinu, francouzštinu, španělštinu, italštinu, portugalštinu, ruštinu a turečtinu. Nastavte ho explicitně pro každý flow metodou SetTextFlowLanguage, nebo ho nechte zdědit z položky /Lang v Catalogu dokumentu, kterou už stejně nese značkovaný a přístupný dokument

Toto dědění se vyplatí využít, ne ho přepisovat vlastní hodnotou. Dokument, který deklaruje svůj jazyk v Catalogu, sděluje stejnou skutečnost čtečkám obrazovky, vyhledávacím indexům i dělení slov z jediného místa, a přesně na jednom místě má daná skutečnost žít. Pokud už vytváříte značkovaný výstup popsaný v článku o automatickém značkování přístupných PDF, položka jazyka je už nastavená a flow se jí může prostě řídit

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');
      // zapnuto, alespoň 3 znaky před zlomem, 3 za ním
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // nikdy neponechat osamocený řádek

      repeat
        // tři sloupce v oblasti o šířce 480 bodů, mezery 18 bodů, vyváženo
        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 a MinSuffix jsou otázka typografie, ne validace

Dvě celá čísla za příznakem zapnutí určují minimální počet znaků, které musí zůstat před zlomem a za ním. Tři a tři je konzervativní výchozí hodnota, kterou akceptuje většina firemních stylů. Dvě a dvě přinese víc míst pro zlom a viditelně ošklivější výsledek, protože dvouznakový útržek visící na konci řádku vypadá jako překlep

Minima zvyšte tam, kde je písmo velké a každý útržek je vizuálně nápadný, a snižte je jen tehdy, když je sloupec opravdu úzký a rozhodli jste se, že těsná sazba je důležitější než čistá. Jde o rozhodnutí firemního stylu, ne o technickou otázku, a přesně proto je to parametr, ne konstanta

Co tady vlastně znamená „vyvážené"?

Parametr Balance mění chování jen na konci pasáže. Když je vyvažování zapnuté, sloupce se zkrátí na přesně stejný počet řádků ve chvíli, kdy se vše zbývající vejde do dané oblasti, což zabrání tomu, aby poslední stránka ukázala dva plné sloupce a třetí s jedním osamoceným řádkem. Když se pasáž nevejde, každý sloupec si podrží plnou výšku, takže stránka pojme co nejvíc textu a zbytek pokračuje na další stránce

Tato asymetrie je pro souvislé dokumenty správné výchozí chování. Vyvažování uprostřed plynoucího článku by na každé stránce plýtvalo svislým prostorem kvůli kosmetickému efektu, který nikdo nevidí, protože sloupce jsou tak jako tak plné. Vyvažování na konci je místo, kde si toho oko skutečně všimne, a přesně tam se také uplatňuje

Lámání řádků měří celá slova

Algoritmus lámání měří celá slova místo sčítání šířek jednotlivých znaků a vyhrazuje si ohraničené hledání pro nadměrně velké tokeny, které se na řádek nevejdou vůbec, jako je URL nebo evidenční číslo. Díky tomu je běžný případ rychlý a patologický případ ohraničený, ne naopak

Volitelné měkké rozdělovníky i automatické rozdělovníky se vykreslí, jen když se zlom, který označují, skutečně zvolí. Zní to samozřejmě, a přesto jde o klasickou chybu: naivní implementace zapíše znak rozdělovníku už při měření, a pokud se zlom později posune, rozdělovník zůstane trčet uprostřed řádku. Nic nevypadá jako rozbitý textový engine víc než osamocený rozdělovník uprostřed slova

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // o rozvržení rozhodněte dřív, než začnete cokoli kreslit
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // počet řádků, které zbytek pasáže potřebuje při šířce jednoho sloupce
    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;

Udržujte nastavení písma stejné napříč boxy

Jedno pravidlo řídí každé rozvržení založené na flow a stojí za to ho říct otevřeně: DrawTextFlow, DrawTextFlowColumns i MeasureTextFlow lámou řádky podle písma zvoleného v okamžiku, kdy jsou volány. Změníte-li písmo nebo velikost mezi dvěma boxy téhož flow, nebo začnete novou stránku bez opětovné volby písma, druhý box se zalomí jinak, než co změřil ten první

Tento příznak je k zbláznění právě proto, že vypadá nahodile: text, který se vejde na stránku jedna, přeteče na stránce dva, nebo se změřený počet řádků neshoduje s tím, co se vykreslilo. Zvolte písmo jednou před cyklem, znovu ho zvolte po každém NewPage, a flow se bude chovat předvídatelně. Když se ve stejné pasáži objeví smíšené skripty, řešení popsané v článku o automatickém fallbacku písem pro CJK a emoji se uplatní jak při měření, tak při kreslení, takže šířky zůstávají konzistentní i napříč úseky řešenými fallbackem

U rozvržení reportů, kde je flow jen jedním prvkem mezi záhlavími, zápatími a bloky řízenými daty, se kompoziční vzory z článku o reportovacím enginu nad datovou sadou čistě kombinují se sloupcovým flow: nejdřív změřte, umístěte pevné prvky rozvržení a pak flow přidělte to, co z plochy zbylo

PDFlibPas je knihovna pro práci s PDF pro Delphi, C++Builder a Lazarus a celý životní cyklus TextFlow – vytvoření, kreslení, měření, inspekce, převinutí i uvolnění – je dostupný i přes rozhraní DLL a ActiveX. Úplná dokumentace je na stránce PDFlibPas Delphi PDF library