Полное выравнивание по ширине - это вёрстка, при которой столбец текста выстраивается по обоим краям: левому и правому, - именно так выглядит печатная книга или официальный отчёт. Задача просто описывается и удивительно легко решается неправильно, поскольку ответ на вопрос «куда девается лишнее место» не одинаков для английского и японского, а наивный способ измерения каждой строки превращает быстрый документ в медленный. HotPDF обеспечивает выравнивание с учётом типа письменности через единственный вызов блочной вёрстки, и под этим вызовом скрывается оптимизация производительности, достойная самостоятельного изучения
Настоящая статья охватывает обе темы. Во-первых, типографическое правило, определяющее распределение свободного пространства в письменностях с межсловными пробелами и без них. Во-вторых, изменение в подходе к измерению, которое сократило стоимость выравнивания на страницу примерно в восемьдесят раз без какого-либо видимого изменения результата. Оба аспекта важны, если вы генерируете документы в большом объёме и хотите, чтобы они выглядели как настоящая профессиональная вёрстка, а не как текст с фиксированной шириной символов, растянутый по ширине
Что в действительности требует полное выравнивание по ширине
Строка текста, набранная в естественной ширине, почти никогда не достигает правого края колонки. Всегда остаётся свободное место - зазор между концом последнего глифа и границей колонки. Выравнивание по левому краю оставляет этот зазор справа. Выравнивание по правому краю переносит его влево. Центрирование делит его пополам. Полное выравнивание по ширине устраняет его, расширяя саму строку до тех пор, пока оба края не совпадут с границами блока, и единственный честный способ сделать это - раздвинуть глифы изнутри
Правило, отличающее хорошее выравнивание от плохого, - куда поместить свободное место. В письменности, разделяющей слова пробелами, например в английском и остальных языках латинской группы, естественные швы расположены в каждом межсловном промежутке. Расширение этих промежутков незаметно для глаза, поскольку читатели уже воспринимают переменную ширину межсловных пробелов как норму. В письменности без межсловных пробелов - таких как китайские иероглифы хань, японская кана или корейский хангыль - подобных швов нет. Там свободное место должно равномерно распределяться между соседними глифами, что соответствует принципу, который японские типографы называют «кинто-варицуке» (равномерное распределение). Применение латинского растяжения межсловных промежутков к CJK-строке, или сжатие всего свободного места в единственное место, где в CJK-строке оказывается пробел, порождает реки и дыры, характерные для любительской вёрстки
Как HotPDF решает, куда направить пространство
HotPDF принимает это решение для каждого промежутка отдельно, а не для всей строки целиком. При выравнивании строки он перебирает каждую смежную пару глифов и проверяет, находится ли между ними растяжимая граница. Граница является растяжимой, когда с одной из сторон находится пробел или табуляция (латинский случай) либо когда обе стороны - CJK-символы, допускающие разрыв строки (случай равномерного распределения). Алгоритм подсчитывает такие границы, равномерно делит между ними свободное место строки и добавляет соответствующую долю к каждому подходящему промежутку
Следствие вытекает естественным образом. Английская строка имеет растяжимые границы только в местах межсловных пробелов, поэтому всё свободное место располагается именно там, а слова расходятся в стороны, тогда как буквы внутри каждого слова сохраняют естественный интервал. В строке хань или каны растяжимая граница находится почти между каждой парой глифов, поэтому свободное место равномерно распределяется по всей строке - именно тот равномерный межглифный интервал, который требуется этим письменностям. Строка, состоящая из единственного длинного латинского слова без внутренних пробелов, не имеет ни одной растяжимой границы, поэтому HotPDF оставляет её в естественной ширине, не разрывая слово по буквам. Та же логика обрабатывает смешанные латинские и CJK-участки в одной строке без специальных оговорок, поскольку решение принимается локально для каждой границы
Одна граница намеренно исключена везде. Позиция после последнего глифа строки никогда не рассматривается как промежуток, поскольку растяжение там лишь вернуло бы правый остаток, что является противоположностью выравнивания по ширине
Почему последняя строка остаётся нетронутой
Последняя строка абзаца занимает особое положение, и неправильная её обработка - самая распространённая ошибка выравнивания. Последняя строка абзаца, как правило, короткая - зачастую всего несколько слов - и её растяжение до полной ширины колонки разносит эти слова по странице в редкий, разрозненный ряд. Правильная типографика оставляет последнюю строку в естественной ширине, выровненной по левому краю
HotPDF определяет завершающую строку по её положению. В процессе разбивки текста на строки он знает, когда только что отделённая строка достигает конца переданной строки. Эта финальная строка выводится с обычным выравниванием по левому краю и сохраняет свою естественную ширину. Все строки перед ней выравниваются по обоим краям. Явные переносы строк, вписанные в текст, обрабатываются так, как записаны, поэтому намеренно короткая строка также никогда не растягивается. Читатель видит чистый прямоугольный блок текста, последняя строка которого заканчивается естественно - именно то, чего ожидает глаз
Стоимость измерения, из-за которой выравнивание становилось медленным
Чтобы выровнять строку, необходимо знать её точную ширину и продвижение каждого глифа, чтобы точно разместить дополнительное пространство. Первоначальная реализация получала эти числа очевидным способом. Она измеряла всю строку полным запросом ширины Unicode, а затем последовательно измеряла каждый префикс, чтобы восстановить продвижение каждого глифа через разности. Для строки из N глифов это N+1 обращений к подсистеме измерений, и каждое обращение - полный цикл взаимодействия с GDI: операционная система выполняет шейпинг, измерение и возвращает результат
На одну строку это звучит дёшево. На страницу - нет. Возьмём плотно заполненную страницу A4 с основным текстом: примерно сорок пять строк по восемьдесят символов каждая. При N+1 обращениях на строку это около 81 обращения для каждой строки и примерно 3645 для всей страницы, причём почти все они тратятся на повторное измерение текста, который подсистема рассматривала несколько мгновений назад. При пакетной генерации тысяч страниц эти накладные расходы занимают большую часть времени вёрстки, и каждое обращение пересекает границу между вашим процессом и графической подсистемой
Один вызов вместо N плюс один
Исправление - это изменение, которое выглядит небольшим, но даёт большую отдачу. GDI уже способен сообщить общую ширину строки и позицию каждого глифа в одном запросе. HotPDF открывает это через GetWideCharAdvances, который заполняет массив естественным продвижением каждого глифа (включая кернинг) и возвращает общую ширину - в одном вызове вместо N+1. Процедура выравнивания (_HPDFEmitJustifiedWideLine внутри) запрашивает все продвижения один раз, вычисляет свободное место, распределяет его по растяжимым границам и выводит строку
Для той же страницы A4 измерение на строку падает с примерно 81 обращения до одного, и страница падает примерно с 3645 обращений до около 45 - снижение примерно в восемьдесят раз. Вывод побайтно идентичен, потому что в измерении ничего не изменилось, кроме количества запросов. Тот же GDI, те же метрики шрифта, тот же кернинг дают те же числа. Снизилось только количество обращений. Когда измерение уже корректно, правильная оптимизация - перестать запрашивать его многократно, а не приближать его
Как строка попадает на страницу
После распределения свободного места HotPDF выводит строку с помощью ExtTextOut и массива продвижений для каждого глифа - массива Dx. Каждый элемент - расстояние от начала одного глифа до начала следующего: естественное продвижение данного глифа плюс его доля свободного места, если за ним следует растяжимая граница. Это непосредственно отображается на модель изображения PDF. Позиционированный текст записывается оператором TJ, массивом, чередующим глифовые последовательности с явными горизонтальными корректировками, и значения Dx становятся именно этими корректировками. Вот почему дополнительное пространство располагается между глифами с точностью до долей пункта, а не имитируется символами-разделителями, и почему выровненная строка HotPDF корректно измеряется при повторном чтении сторонним инструментом
Для выровненных абзацев ExtTextOut вызывать самостоятельно не нужно. Точкой входа является WideTextOutBox, которая упаковывает строку Unicode в блок и применяет заданное выравнивание. Она разбивает текст на строки, помещающиеся в заданную ширину блока, располагает каждую строку по высоте блока и возвращает количество символов, которые удалось разместить до исчерпания вертикального пространства. Выравнивание задаётся перечислением
type
THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);
Первые три значения - очевидные выравнивания по левому краю, по центру и по правому краю. Четвёртое, jtJustify, - это полное выравнивание по обоим краям, описанное здесь; именно это значение WideTextOutBox считывает для включения чувствительного к письменностям распределения пространства
Практический пример выравнивания абзаца
Полный пример создаёт документ, устанавливает шрифт и помещает абзац в блок с полным выравниванием по ширине. Тот же код выравнивает латинский и CJK-текст без смены флага, поскольку учёт типа письменности реализован ниже уровня 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;
Чтобы вывести тот же блок с выравниванием по левому краю, по центру или по правому краю, достаточно изменить только последний аргумент на jtLeft, jtCenter или jtRight. Перенос строк, их расположение и возвращаемое значение остаются теми же. Ширина, от которой зависят все четыре варианта, определяется через GetWideTextWidth - запрос ширины с поддержкой Unicode, корректно измеряющий WideString там, где устаревшее побайтовое измерение дало бы неверные результаты для всего, что выходит за рамки Latin-1, - именно это позволяет блоку корректно переносить CJK-текст и текст с суррогатными парами
Выравнивание - один уровень более широкого стека обработки текста. Когда строка содержит письменности, переупорядочивающие или соединяющие глифы, решения о распределении пространства опираются на работу, описанную в нашей статье о шейпинге текста сложных письменностей, а когда шрифт содержит типографические варианты, которые вы хотите выбрать, см. руководство по управлению стилистическими альтернативами OpenType GSUB. Всё это входит в состав HotPDF Component для Delphi и C++Builder, наряду с API для работы с текстом, вёрсткой и документами, рассмотренными в других статьях этого блога