Первая строка, рассчитанная с точки зрения спецификации, приземляется на y = 770. На бумаге логика безупречна: ISO 32000-1 помещает начало пользовательского пространства в левый нижний угол, страница A4 имеет высоту 842 пункта, так что 770 — это дюйм ниже верхнего края. Запустите ту же программу против компонента, который меряет от верха, и приветствие приземлится примерно в дюйме над нижним краем, а над ним останется пустая страница. Добавьте вторую страницу — и тщательно выбранный шрифт снова станет 12-пунктовым Arial. Обе неожиданности растут из тех же двух решений, которые каждая PDF-программа принимает на своей первой странице: какой шрифт является текущим и от какого угла отсчитываются числа
Самая маленькая программа HotPDF на Delphi
HotPDF Hello World — консольная программа из четырёх вызовов в фиксированном порядке: задать свойства документа, BeginDoc, рисовать через SetFont и TextOut, затем EndDoc. Всё остальное — обычный Delphi. Вот она: пишет одну страницу, приветствуя читателя на одиннадцати языках:
program HelloWorld;
{$APPTYPE CONSOLE}
uses
Winapi.Windows, // DEFAULT_CHARSET; в Delphi до XE2 используйте обычный Windows
HPDFDoc; // основной модуль HotPDF
procedure CreateHelloWorld(const FileName: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
// Общедокументные настройки: всё это ставится до BeginDoc
Pdf.FileName := FileName;
Pdf.AutoLaunch := False; // по умолчанию True: открывает вьюер после EndDoc
Pdf.Compression := cmFlateDecode; // по умолчанию cmNone
Pdf.FontEmbedding := True; // уже дефолт, написано для читателя
Pdf.EnableFontSubsetting := True; // встраивать только реально нарисованные глифы
Pdf.BeginDoc; // создаёт страницу 1 (A4)
// 4-й аргумент по умолчанию ANSI_CHARSET; передайте DEFAULT_CHARSET явно
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
// X, Y в пунктах от верхнего левого угла; Y — верхний край строки
Pdf.CurrentPage.TextOut(72, 72, 0, 'Hello, Delphi PDF world!'); // English
Pdf.CurrentPage.TextOut(72, 102, 0, 'Hola, mundo Delphi PDF!'); // Spanish
Pdf.CurrentPage.TextOut(72, 132, 0, 'Hallo, Delphi PDF Welt!'); // German
Pdf.CurrentPage.TextOut(72, 162, 0, 'Bonjour, monde PDF Delphi!'); // French
Pdf.CurrentPage.TextOut(72, 192, 0, 'Ciao, mondo Delphi PDF!'); // Italian
Pdf.CurrentPage.TextOut(72, 222, 0, 'Olá, mundo Delphi PDF!'); // Portuguese
Pdf.CurrentPage.TextOut(72, 252, 0, 'Здравствуйте, Delphi PDF мир!'); // Russian
Pdf.CurrentPage.TextOut(72, 282, 0, 'こんにちは、Delphi PDFの世界!'); // Japanese
Pdf.CurrentPage.TextOut(72, 312, 0, 'Merhaba, Delphi PDF dünyası!'); // Turkish
Pdf.CurrentPage.TextOut(72, 342, 0, '你好,Delphi PDF世界'); // Chinese
Pdf.CurrentPage.TextOut(72, 372, 0, '여보세요, Delphi PDF 세계!'); // Korean
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
begin
CreateHelloWorld('HelloWorld.pdf');
Writeln('Wrote HelloWorld.pdf');
end.

THotPDF.Create(nil) в паре с try/finally ... Free — обычный шаблон владения Delphi, и производство PDF его не меняет. Строки маршируют вниз от Y = 72 с шагом 30 пунктов, что даёт комфортный интерлиньяж для 14-пунктового текста. Запустите — и HelloWorld.pdf появится в рабочем каталоге: одна страница, сжатое содержимое, шрифт встроен как подмножество
Свойства документа живут до BeginDoc
BeginDoc запускает документ и создаёт его первую страницу, так что всё, что формирует файл в целом, назначается до него. HotPDF принуждает это для части свойств, а не оставляет конвенции. Присвойте Compression после BeginDoc — и сеттер поднимет исключение с сообщением Cannot set Compression value - document in progress.; AutoLaunch охраняется так же. Громкость здесь — благо. Соберите все настройки документа в один блок над BeginDoc, и вы никогда этого сообщения не увидите
Два дефолта заслуживают второго взгляда. Compression стартует как cmNone, так что программа, которая его не трогает, пишет несжатые потоки содержимого; для страницы текста это просто расточительность, а для отчёта в сотни страниц — первое, что все замечают в размере файла. AutoLaunch стартует как True, то есть консольный инструмент открывает готовый PDF в ассоциированном вьюере в момент возврата EndDoc. За рабочим столом это приятно, а внутри сервиса, планировщика или билд-агента — неправильно. Для таких окружений ставьте False в коде; для CI-прогонов, где код менять нельзя, переменная окружения HOTPDF_SUPPRESS_AUTO_LAUNCH в значении 1 тоже выключает запуск
Обработка шрифтов — место, где мы занимаем позицию. FontEmbedding по умолчанию True: начертание едет с файлом и рендерится на машине, где Arial Unicode MS никогда не ставился. А вот EnableFontSubsetting по умолчанию выключен, чтобы полные шрифты оставались доступны рабочим процессам редактирования. Для генерируемого приветствия, счёта или отчёта включайте подмножество. Arial Unicode MS покрывает огромный диапазон письменностей, и встраивание всего начертания ради одиннадцати коротких строк раздует файл на мегабайты без видимой пользы. Оставляйте выключенным только когда кто-то дальше по цепочке будет править текст и ему нужны глифы, которые вы не рисовали
Где на странице HotPDF находится (0, 0)?
HotPDF меряет координаты страницы от верхнего левого угла, Y растёт вниз — та же система, что у VCL TCanvas. При дефолтном Resolution 72 единица — один пункт, 1/72 дюйма. Сам PDF-файл по-прежнему хранит координаты снизу вверх, как требует ISO 32000-1 §8.3.2.3; HotPDF преобразует каждый переданный Y, вычитая его из высоты страницы при записи потока содержимого
Арифметика за открывающим примером короткая. Страница по умолчанию, которую создают BeginDoc и AddPage, — это A4, 595 на 842 пункта. Строка, которая должна сидеть в дюйме от верха, — это Y = 72 в терминах HotPDF и y = 770 в терминах сырого PDF, потому что 842 − 72 = 770. Передайте 770 в TextOut, и текст уйдёт на 770 пунктов вниз от верха — это 72 пункта над нижним краем
В том же вызове есть вторая, меньшая ловушка. По умолчанию переданный Y — это верхний край текста, и HotPDF смещается вниз на высоту текста, прежде чем положить базовую линию. Код, перенесённый с GDI, обычно ожидает именно этого. Код, перенесённый с другой PDF-библиотеки или из вашего собственного писателя потоков содержимого, обычно ожидает, что Y — это базовая линия, и каждая строка выходит на одну высоту текста ниже плана. THPDFPage.TextFromBaseLine(True) переключает текущую страницу на позиционирование по базовой линии:
// Мост для координат из источника снизу вверх (раскладка PDF,
// схема из спецификации, другая библиотека). Хелпер написан для этой статьи,
// не часть HotPDF. Корректен при дефолтном Resolution 72
function FromBottomUp(Page: THPDFPage; PdfY: Single): Single;
begin
Result := Page.Height - PdfY;
end;
procedure DrawFromPdfCoordinates(Pdf: THotPDF);
begin
Pdf.CurrentPage.SetFont('Arial', [], 12);
// Y теперь именует базовую линию, как расставляет глифы собственный текстовый оператор PDF
Pdf.CurrentPage.TextFromBaseLine(True);
// Базовая линия в 770 пунктов над нижним краем, то есть Y = 72 от верха
Pdf.CurrentPage.TextOut(72, FromBottomUp(Pdf.CurrentPage, 770), 0,
'Baseline one inch below the top edge');
end;
Resolution — другая настройка, меняющая смысл чисел. Поставьте THotPDF.Resolution в 96, и координаты X/Y, поля и результаты TextWidth выражаются в 1/96 дюйма — удобно, когда вёрстка спроектирована в экранных пикселях. Ширина и высота страницы, Width и Height, продолжают отчитывать пункты, поэтому коду вёрстки, смешивающему границы страницы с координатами рисования, стоит читать CurrentPage.UserWidth и CurrentPage.UserHeight — они возвращаются в тех же единицах, что X и Y. При 72 обе пары совпадают. Если вам милее миллиметры, свойство MeasurementUnit (по умолчанию muPoints, доступны muMM, muInches, muCentimeters, muTwips и muPixels) — более чистый переключатель. Выберите одну конвенцию на программу. Никогда не складывайте их
Почему вторая страница печатается 12-пунктовым Arial?
Потому что каждая новая страница стартует из известного состояния. THotPDF.AddPage делает новую страницу текущей, задаёт ей A4 и вызывает на ней SetFont('Arial', [], 12), держа размер 12 пунктов независимо от Resolution. BeginDoc создаёт страницу один тем же путём — вот почему Hello World ставит шрифт сразу после BeginDoc, а не до. Следствие для многополосного вывода легко пропустить: SetFont, вызванный на странице один, не переносится. Латинские строки на странице два всё ещё выглядят прилично в Arial, просто мельче. CJK-строки — то, что выдаёт: у Arial нет для них глифов, и ридер показывает пустые квадраты или вообще ничего
Предсказуемый стартовый шрифт — правильный дизайн для объекта страницы: код рисования страницы пять не зависит молча от того, что случилось оставить после себя страница четыре. Цена — повторяемость настройки страницы, и чистейший способ повторять — одна процедура, через которую проходит каждая страница, включая первую:
uses
Winapi.Windows, Vcl.Graphics, HPDFDoc;
// Ваш собственный хелпер: одно место, которое знает, что нужно свежей странице
procedure PreparePage(Pdf: THotPDF; IsFirst: Boolean);
begin
if not IsFirst then
Pdf.AddPage; // сбрасывает шрифт в Arial 12
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
end;
procedure WriteGreetings(Pdf: THotPDF; Lines: array of string);
const
TopMargin = 72;
Leading = 30;
LinesPerPage = 20;
var
I, Row: Integer;
begin
PreparePage(Pdf, True); // страница 1 уже существует после BeginDoc
Row := 0;
for I := 0 to High(Lines) do
begin
if Row = LinesPerPage then
begin
PreparePage(Pdf, False);
Row := 0;
end;
Pdf.CurrentPage.TextOut(72, TopMargin + Row * Leading, 0, Lines[I]);
Inc(Row);
end;
// Жирному заголовку нужен собственный SetFont, затем шрифт тела возвращается
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial Unicode MS', [fsBold], 18, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 72, 0, 'Appendix');
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 110, 0, 'Body text after the heading');
end;
Если страницы также переключаются на позиционирование по базовой линии через TextFromBaseLine, поместите тот вызов в PreparePage тоже, рядом с шрифтом. Держать все постраничные настройки в одной процедуре означает, что новое требование — цвет или другой размер — добавляется один раз и добирается до каждой страницы
Один шрифт для многих письменностей и где он заканчивается
Один SetFont может нести латиницу, кириллицу, японский и китайский на одной странице, потому что Arial Unicode MS упаковывает глифы очень широкого диапазона письменностей в одно начертание. Четвёртый аргумент SetFont — набор символов Windows, и его дефолт — ANSI_CHARSET, привязывающий выбор шрифта к западной кодовой странице. Передача DEFAULT_CHARSET (значение 1) позволяет Windows выбирать начертание по имени — именно этого хочет многоязычная страница
Шрифт должен существовать на машине, собирающей PDF. HotPDF встраивает то, во что Windows разрешит имя, а когда Arial Unicode MS отсутствует, Windows безропотно подставляет другое начертание. Выход тогда встраивает подмену, и строки на письменностях, которых в ней нет, выходят пустыми квадратами. Обычное место проявления — билд-сервер, потому что Office-шрифты там никто не ставил. Проверьте список встроенных шрифтов первого файла, произведённого новой машиной, прежде чем доверять ей пакетный прогон
Когда ни одно начертание не покрывает все строки, TextOutWithFallback принимает те же аргументы, что TextOut, подбирает шрифт для каждого символа, рисует каждый прогон тем начертанием, которое может его отрисовать, и возвращает ваш исходный шрифт после. Право-левые письменности вроде арабского и иврита требуют большего, чем покрытие глифами: им нужны shaping и обратный порядок прогонов — территория функции RtLTextOut, обычно в паре с TrueType-файлом, загруженным через RegisterUnicodeTTF
Дальше естественные следующие шаги — размер, стиль и поворот текста, а также намеренное размещение нескольких блоков на одной странице, что разбирает пример TextOut. Линейки, рамки и изображения используют тот же объект CurrentPage и те же координаты от верхнего левого угла, так что всё сказанное выше о началах координат и постраничной настройке применяется к ним без изменений
Страница HotPDF Delphi Component покрывает остальной API рисования для Delphi и C++Builder, от фигур и изображений до форм, как только этот Hello World чисто прогонится на ваших шрифтах