Каждая видимая строка в документе HotPDF поступает через один вызов: TextOut(X, Y, angle, Text). Пример Hello World использует его в самом простом виде, с однократно заданным шрифтом и четырьмя аргументами, оставленными на разумных значениях по умолчанию. После этой первой страницы те же четыре аргумента несут весь вес макета. Третий аргумент вращает блок текста. Шрифт, установленный непосредственно перед ним, определяет размер и стиль. И пара X, Y, измеряемая от угла страницы в пунктах, — это единственное, что стоит между чистым отчетом и текстом, который перекрывается, обрезается или смещается на строку ниже на чьем-то чужом принтере. Именно здесь TextOut оправдывает свое существование, и именно здесь значений по умолчанию становится недостаточно
Сигнатуру стоит закрепить в памяти прежде всего остального: X и Y являются Single в пунктах, angle является Extended в градусах, а Text является WideString, поэтому Unicode проходит без отдельного вызова. Вторая перегрузка принимает PWORD плюс длину для тех случаев, когда у вас уже есть коды глифов, но для обычных строк вам нужна форма WideString
Размер и стиль берутся из SetFont, а не из TextOut
У TextOut нет параметра размера. Размер, толщина, наклон — все это живет в вызове SetFont, предшествующем блоку текста, и остается в силе до тех пор, пока следующий SetFont не заменит его. Это единственный факт, который объясняет большую часть путаницы в первый день работы: строка получается жирной, потому что тремя вызовами ранее что-то установило [fsBold], и ничто не очистило его
Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
Pdf.CurrentPage.TextOut(72, 740, 0, 'Quarterly Report'); // 24pt regular
Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
Pdf.CurrentPage.TextOut(72, 712, 0, 'Revenue'); // 12pt bold
Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
Pdf.CurrentPage.TextOut(72, 694, 0, 'figures in thousands'); // 11pt italic
Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsItalic], 10);
Pdf.CurrentPage.TextOut(72, 676, 0, ' +18.4% YoY'); // styles combine
Второй аргумент — это набор TFontStyles, поэтому [fsBold, fsItalic] — это полужирный курсив, а [] — обычный текст. Размер указан в пунктах, в тех же единицах измерения, что и координаты, что позволяет легко рассуждать о вертикальных интервалах: для 12-пунктовой строки требуется примерно от 14 до 16 пунктов вертикального шага, чтобы дышать, поэтому уменьшение Y на 14 пунктов на строку является разумным начальным интерлиньяжем. Автоматического перевода строки нет. Вы вычисляете каждую базовую линию самостоятельно, что утомительно для абзаца, но точно для формы, где каждое поле находится по фиксированной координате
Два практических замечания об имени шрифта. Оно сопоставляется со шрифтами, установленными на машине сборки, и то, что ОС возвращает обратно, встраивается в документ; поэтому имя, которое разрешается на вашем рабочем столе, и имя, которое разрешается на сервере сборки, не гарантированно будут одним и тем же шрифтом. Кроме того, шрифт должен охватывать алфавиты в строке. Блок текста на кириллице или CJK со шрифтом только для латиницы отображается в виде квадратов, заменяющих отсутствующие глифы, без возникновения ошибок. По этой причине страница Hello World использует широкий шрифт Unicode при смешивании языков
Аргумент угла обеспечивает поворот вокруг якоря
Третий аргумент — это тот, который большинство программ навсегда оставляют на нуле. Передайте ненулевое значение, и блок текста повернется против часовой стрелки вокруг собственного якоря (X, Y), левого нижнего угла текста, на указанное количество градусов. Сам якорь не сдвигается, поэтому та же самая координата, в которой была размещена горизонтальная метка, размещает ее повернутого близнеца; меняется только направление, в котором идут глифы
Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);
// A vertical axis label down the left margin: 90 degrees reads bottom-to-top.
Pdf.CurrentPage.TextOut(40, 300, 90, 'Units sold');
// A diagonal DRAFT watermark across the page body.
Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');
// Column headers tilted 60 degrees so long labels fit a narrow table.
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');
Девяносто градусов — это обычный случай, когда метка идет вверх по краю диаграммы, или для заголовка на корешке книги. Сорок пять градусов подходят для наклонных заголовков столбцов — хитрость, позволяющая широкой метке располагаться над узким столбцом, не перекрывая соседние. Поворот не меняет интерпретацию якоря, что сбивает людей с толку: блок текста под углом 90 градусов все равно начинается в координатах (X, Y) и растет оттуда вверх, поэтому, чтобы отцентрировать повернутую метку, вы корректируете якорь, а не угол. Если несколько повернутых блоков текста используют одну базовую линию, присвойте им одинаковый Y и делайте шаг по X, точно так же, как вы делали бы шаг по Y для сложенных горизонтальных строк
Размещение координат без угадывания
Координаты — это та часть, которая либо успешно проходит ревью, либо незаметно ломает его. HotPDF измеряет от левого нижнего угла страницы, при этом Y растет вверх, в пунктах по 72 пункта на дюйм. Страница US Letter имеет размер 612 на 792 пункта; A4 — 595 на 842. Таким образом, верхнее поле в один дюйм на Letter помещает вашу первую базовую линию около Y = 792 минус 72 минус размер шрифта, а не на каком-то маленьком числе вблизи верхнего края. Любой, кто перешел от экранных координат, где Y увеличивается вниз от нуля, пишет первую строку за нижним краем и потом десять минут гадает, куда она делась
Относитесь к макету как к арифметике с именованными якорями, а не как к колонке магических чисел. Левое поле, текущая базовая линия, которую вы уменьшаете для каждой строки, и фиксированный интерлиньяж превращают блок меток в короткий цикл, а не в стену литералов:
const
LeftMargin = 72; // 1 inch in
TopBaseline = 720; // first line, ~1 inch down on Letter
Leading = 16; // vertical step between lines
var
Y: Single;
Line: string;
begin
Pdf.CurrentPage.SetFont('Arial', [], 11);
Y := TopBaseline;
for Line in ReportLines do
begin
Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Line);
Y := Y - Leading;
if Y < 72 then // bottom margin reached
begin
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 11); // font resets on a new page
Y := TopBaseline;
end;
end;
end;
Защита от разрыва страницы — это строка, которую все забывают в первую очередь и из-за которой больше всего страдают на практике. Под TextOut нет потоковой компоновки. Если вы продолжите уменьшать значение за нижним полем, текст будет и дальше рисоваться в промежутке, за пределами страницы, в никуда, без каких-либо предупреждений. Поэтому вам приходится следить за Y самостоятельно, вызывать AddPage, когда он пересекает границу, и сбрасывать базовую линию. SetFont после AddPage не является необязательным дополнением: текущий шрифт не сохраняется после разрыва страницы, и первый блок текста на новой странице выводится шрифтом программы просмотра по умолчанию, если вы пропустите эту команду
Интервалы между символами и словами для подгонки и выравнивания
Иногда строка является правильной, но имеет неверную ширину: заголовок, который должен охватывать фиксированную линию, код, который должен читаться с более разнесенными цифрами, столбец, значения которого нужно немного сдвинуть для выравнивания. В PDF для этого предусмотрены два оператора состояния текста: интервал между символами (Tc, дополнительное пространство, добавляемое после каждого глифа) и интервал между словами (Tw, дополнительное пространство, добавляемое к каждому символу пробела), причем оба они выражаются в немасштабированных единицах текстового пространства, которые фактически представляют собой пункты при текущем размере шрифта. Это состояние, а не аргументы для TextOut, поэтому вы задаете их, рисуете, а затем сбрасываете обратно
// Letter-space a short heading so it stretches across a rule.
Pdf.CurrentPage.SetCharacterSpacing(4);
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(72, 740, 0, 'S U M M A R Y');
Pdf.CurrentPage.SetCharacterSpacing(0); // reset before normal body text
// Open up the gaps between words on a single wide line.
Pdf.CurrentPage.SetWordSpacing(6);
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 712, 0, 'Name Department Extension');
Pdf.CurrentPage.SetWordSpacing(0);
Интервал между словами действует только на символ пробела (код 32), что имеет последствие, о котором стоит знать: он ничего не делает внутри блока CJK, в котором нет пробелов ASCII, и он странно взаимодействует с текстом, закодированным как индексы глифов, а не как байты. Для табличного вывода на латинице это дешевый способ расширить промежутки без повторного набора строки. Интервал между символами — это лучший инструмент для заголовка, который должен достичь целевой ширины, так как он распределяет регулировку равномерно по каждому глифу, а не группирует ее на пробелах
Сброс — это залог порядка. Интервалы, как и шрифт, являются частью состояния рисования страницы, а состояние сохраняется до тех пор, пока вы его не измените. Задайте межбуквенный интервал для одного заголовка и забудьте его обнулить, и каждый абзац ниже унаследует это растяжение, что воспринимается как тонкая, трудноопределимая неправильность, которая переживает случайную вычитку и не проходит тщательную проверку. Надежная привычка заключается в том, чтобы установить значение интервала, нарисовать блок текста, которому он нужен, и сбросить его обратно на ноль в следующей строке, чтобы никакой последующий код не должен был знать, что делал более ранний раздел
Проверка вывода там, где он действительно ломается
Текстовая верстка дает сбой на второй машине, а не на первой, поэтому важные проверки происходят вдали от вашего рабочего стола. Откройте сгенерированный файл в системе, в которой не установлен набор шрифтов разработчика, и убедитесь, что встроенные шрифты по-прежнему отображаются, включая латиницу с диакритическими знаками, любые нелатинские алфавиты и пунктуацию, за один проход, а не выборочно проверяя простые символы. Выделите и скопируйте несколько строк, чтобы убедиться, что текст является настоящим текстом, а не контурами, что имеет значение в тот момент, когда требуется поиск или извлечение. Предоставьте макету репрезентативные данные — самую длинную немецкую метку и самое широкое число, а не аккуратный текст-заполнитель, потому что блок текста, который переполняет поле — это всегда тот, который вы не вводили вручную. И если страница должна оказаться на предварительно отпечатанном бланке, распечатайте или растрируйте один образец и приложите его к оригиналу; дрейф базовой линии на четверть миллиметра невидим на экране и очевиден на бумаге
Если вы еще не написали ни одной страницы, начните с примера HotPDF Hello World, который настраивает документ, шрифт и нижнюю левую систему координат, от которой зависит все вышеперечисленное. Вызовы TextOut, SetFont и интервалов, показанные здесь, являются частью компонента HotPDF для Delphi и C++Builder