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 — и поток начнёт вести себя предсказуемо. Когда в одном фрагменте смешиваются письменности, решение, описанное в статье об автоматическом fallback шрифтов для CJK и эмодзи, применяется и к измерению, и к отрисовке, поэтому ширины остаются согласованными и на резервных прогонах
Для раскладок отчётов, где поток — лишь один из элементов наряду с колонтитулами и блоками на основе данных, шаблоны компоновки из статьи о движке отчётов на базе набора данных хорошо сочетаются с колоночными потоками: сначала измерьте, затем разместите фиксированные элементы, а потоку отдайте всё, что осталось от области
PDFlibPas — это библиотека PDF для Delphi, C++Builder и Lazarus, и полный жизненный цикл TextFlow — создание, отрисовка, измерение, инспекция, перемотка и освобождение — также доступен через интерфейсы DLL и ActiveX. Полная документация доступна на странице PDFlibPas Delphi PDF library