Технічна стаття

Перенос слів і збалансовані колонки тексту в PDFlibPas

PDFlibPas розподіляє збережений фрагмент тексту по одній–64 колонках однакової ширини за допомогою DrawTextFlowColumns і розриває рядки з обмеженим мовно-залежним переносом слів, щойно ви викликаєте SetTextFlowLanguage та SetTextFlowHyphenation. Підтримується дев'ять мов, і мову можна успадкувати зі значення /Lang каталогу документа, а не задавати окремо для кожного потоку

Обидві функції існують з однієї причини: саме у вузькій колонці наївне розбиття рядків перестає виглядати як верстка і починає виглядати як звіт про баг

Чому вирівняний по ширині текст розсипається у вузьких колонках?

Тому що вирівнювання по ширині розподіляє залишок простору по проміжках між словами в рядку, а сама величина залишку залежить від того, що вміщується. У широкій колонці залишок малий, і око його не помічає. Зменшіть ширину вдвічі — і одне довге слово, яке не влазить, переноситься на наступний рядок, залишаючи попередні слова поглинати весь цей простір. Три такі рядки поспіль утворюють вертикальні білі канали, які типографи називають «річками», а читачі сприймають просто як текст, який важко читати, не розуміючи чому

Перенос слів усуває причину, а не симптом, дозволяючи розрив усередині слова. Німецькі та нідерландські складні слова роблять це категорично необхідним: іменник із 24 символів у колонці шириною 60 міліметрів не має гарного результату без точки розриву. Англійська мова легше переживає відсутність переносу, тому продукти, орієнтовані насамперед на англійську, часто постачають код верстки, що ламається одразу, щойно з ним працює німецький клієнт

Які мови підтримуються і звідки береться мова?

Перенос слів охоплює англійську, німецьку, нідерландську, французьку, іспанську, італійську, португальську, російську та турецьку мови. Задайте її явно для потоку через SetTextFlowLanguage або дозвольте успадкувати зі запису /Lang каталогу документа — значення, яке вже несе в собі позначений і доступний документ

Це успадкування варто використовувати, а не перевизначати. Документ, який заявляє свою мову в каталозі, повідомляє один і той самий факт з одного місця — і зчитувачам з екрана, і пошуковим індексаторам, і механізму переносу слів, а факт і має жити саме в одному місці. Якщо ви вже створюєте позначений вивід, як описано в статті про автоматичне тегування доступних PDF, запис мови вже встановлено, і потоку залишається лише слідувати йому

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');
      // Увімкнути, щонайменше 3 символи перед розривом, 3 після
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // ніколи не залишати єдиний рядок сиротою

      repeat
        // Три колонки в межах області 480 pt, поля 18 pt, збалансовано
        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 і MinSuffix — це типографіка, а не перевірка коректності

Два цілих числа після прапорця увімкнення задають мінімальну кількість символів, які мають залишатися перед розривом і після нього. Значення три й три — це консервативне значення за замовчуванням, яке приймає більшість корпоративних стилів. Значення два й два дає більше можливостей для розриву та помітно потворніший результат, бо двобуквений фрагмент, що звисає в кінці рядка, читається як друкарська помилка

Підвищуйте мінімуми, коли кегль великий і кожен фрагмент візуально помітний, і знижуйте їх лише тоді, коли колонка справді вузька і ви вирішили, що щільна верстка важливіша за охайну. Це рішення корпоративного стилю, а не технічне, і саме тому це параметр, а не константа

Що насправді означає «збалансовано» в цьому контексті?

Параметр Balance змінює поведінку лише в кінці фрагмента. Коли балансування ввімкнено, колонки скорочуються до точно однакової кількості рядків, якщо все, що залишилося, вміщується в область, — саме це не дає останній сторінці показувати дві повні колонки й третю з одним самотнім рядком. Коли фрагмент не вміщується, кожна колонка зберігає повну висоту, щоб сторінка несла якомога більше тексту, а залишок продовжувався на наступній сторінці

Ця асиметрія — правильне значення за замовчуванням для суцільних документів. Балансування посеред статті, що ллється, витрачало б вертикальний простір на кожній сторінці заради косметичного ефекту, якого ніхто не бачить, адже колонки й так повні. Балансування в кінці — це те місце, де око справді помічає різницю, і саме там воно застосовується

Розбиття рядків вимірює цілі слова

Алгоритм розбиття вимірює цілі слова, а не накопичує ширини окремих символів, і резервує обмежений пошук для завеликих токенів, які взагалі не вміщуються в рядок, наприклад URL або обліковий номер. Це тримає звичайний випадок швидким, а патологічний — обмеженим, а не навпаки

Довільні м'які переноси й автоматичні дефіси відображаються лише тоді, коли розрив, який вони позначають, — це саме той розрив, що зрештою обрано. Звучить очевидно, але це класичний дефект: наївна реалізація записує символ дефіса ще під час вимірювання, і якщо точка розриву зміщується, дефіс залишається посеред рядка. Ніщо так не видає зламаний текстовий рушій, як випадковий дефіс усередині слова

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // Визначаємо макет ще до початку малювання
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // Кількість рядків, потрібних решті фрагмента при ширині однієї колонки
    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;

Зберігайте однакові налаштування шрифту в усіх блоках

Одне правило керує всією версткою на основі потоків, і варто сказати про нього прямо: DrawTextFlow, DrawTextFlowColumns і MeasureTextFlow усі розбивають рядки, використовуючи шрифт, обраний саме в момент виклику. Змініть шрифт або кегль між двома блоками одного потоку або почніть нову сторінку, не переобравши шрифт, — і другий блок розіб'ється інакше, ніж виміряв перший

Симптом дратує саме тому, що виглядає непослідовним: текст, який вміщується на першій сторінці, переповнює другу, або виміряна кількість рядків не збігається з тим, що намальовано. Обирайте шрифт один раз перед циклом, переобирайте його після кожного NewPage — і потік поводитиметься передбачувано. Коли в одному фрагменті трапляються змішані писемності, рішення, описане в статті про автоматичний резервний шрифт для CJK і емодзі, застосовується і до вимірювання, і до малювання, тож ширини лишаються узгодженими навіть на резервних ділянках

Для звітної верстки, де потік — лише один елемент серед колонтитулів і блоків даних, патерни компонування зі статті про рушій звітів на основі набору даних чисто поєднуються з колонковими потоками: спершу виміряйте, розмістіть фіксовані елементи, а потім віддайте потоку весь простір, що залишився

PDFlibPas — це PDF-бібліотека для Delphi, C++Builder і Lazarus, і повний життєвий цикл TextFlow — створення, малювання, вимірювання, інспекція, перемотування й звільнення — також доступний через інтерфейси DLL та ActiveX. Повна документація — на сторінці PDFlibPas Delphi PDF library