PDFium имеет репутацию движка для просмотра, рендерера, стоящего за вкладкой PDF в Chrome, поэтому первое, что нужно прояснить, — это то, что компонент PDFium также может создать документ, которого никогда не существовало ранее. Сторона создания документов оборачивает API объектов страницы PDFium: вы создаете пустой документ, добавляете страницы с явными размерами и помещаете текст, векторные пути и изображения на каждую страницу по выбранным вами координатам. Вам не нужно изучать язык описания страниц и использовать драйвер принтера. Вы вызываете методы, библиотека собирает объекты PDF, а SaveAs сериализует результат
Чего вы не получаете, так это движка макета. Это достаточно важно, чтобы сказать об этом сразу, потому что это формирует каждый из приведенных ниже примеров. Компонент PDFium размещает контент там, где вы ему скажете, в абсолютных координатах и нигде больше. Он не будет переносить абзац, перераспределять текст через разрыв страницы или вычислять таблицу из строк и столбцов. Это ваша работа. Если вы пришли в ожидании чего-то, что переформатирует текст так, как это делает текстовый процессор, откалибруйте свои ожидания сейчас: это точный низкоуровневый API размещения, более близкий к рисованию на холсте, чем к верстке документа. Для генерируемых счетов, сертификатов, этикеток и страниц отчетов, где вы уже знаете, где находится каждый элемент, эта точность — именно то, что вам нужно
Минимум, который создает файл
Три вызова стоят между пустым TPdf и сохраненным PDF-файлом: создать документ, добавить страницу, записать его на диск. Все остальное — это контент, который вы наслаиваете между ними
uses
Vcl.Graphics, // for clBlack and TColor
PDFium; // TPdf lives here
procedure CreateBlankPdf(const FileName: string);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // empty in-memory document
Pdf.AddPage(0, 595, 842); // A4 portrait, in points
Pdf.AddText('First page', 'Arial', 18, 50, 780);
Pdf.SaveAs(FileName); // serialize to disk
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Одна деталь сбивает с толку людей, которые видели старые фрагменты кода: вы не присваиваете Pdf.Active := True после CreateDocument. Свойство Active сообщает о том, существует ли дескриптор документа, а CreateDocument его уже создал, поэтому свойство имеет значение True в тот момент, когда этот вызов возвращает управление. Повторная его установка в лучшем случае ничего не делает, а в худшем — вводит в заблуждение следующего читателя. Active отрабатывает свое при завершении: присвоение False освобождает базовый документ перед Free, что является чистым порядком демонтажа. Рассматривайте CreateDocument и открытие файла для загрузки как взаимоисключающие действия. Библиотека отказывается создавать новый документ в TPdf, у которого уже открыт документ, поэтому повторное использование означает, что сначала нужно закрыть текущий документ
Координаты начинаются слева внизу
Вторая пара аргументов в AddText и в каждом вызове размещения — это точка в пространстве пользователя PDF. Начало координат находится в левом нижнем углу страницы, X направлен вправо, а Y — вверх. Одна единица равна одному пункту, 1/72 дюйма, поэтому страница A4 имеет размер 595 на 842 единицы, а US Letter — 612 на 792. Этот направленный вверх Y является самым частым источником путаницы из серии «мой текст оказался за пределами страницы», потому что экранные и растровые координаты помещают начало координат вверху, а Y растет вниз. На странице высотой 842 пункта заголовок в верхней части находится примерно на уровне Y 780, а не Y 60. Когда текст попадает куда-то в неожиданное место, высота страницы минус ваш Y — это почти всегда то число, которое вы на самом деле имели в виду
AddPage принимает позицию вставки в качестве своего первого аргумента, выраженную начиная с единицы, при этом 0 является удобным сокращением «начало документа». Передайте 0 или 1 для первой страницы, и страница будет вставлена в начало; передайте значение, совпадающее с текущим количеством, к которому вы добавляете, чтобы добавить в конец. Только что добавленная страница также становится текущей страницей, на которую нацелены последующие вызовы рисования, поэтому после ее добавления не требуется отдельного шага «выбрать эту страницу». Если вы добавляете несколько страниц, а затем вам нужно вернуться и нарисовать что-то на более ранней, установите PageNumber, чтобы переместить курсор; пока вы заполняете страницы по порядку по мере их создания, вы можете оставить это свойство в покое
Написание текста и правило шрифтов, которое кусает незаметно
Сигнатура AddText содержит все, что нужно для одного фрагмента текста: строку, название шрифта, размер в пунктах, привязку по X и Y, затем необязательный цвет, альфа-байт для прозрачности и угол поворота в градусах
procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
// Title in black, default opacity, no rotation
Pdf.AddText(Title, 'Arial', 20, 50, 780);
// A lighter byline 24 points below it
Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
// A faint diagonal draft stamp across the page
Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;
Альфа-байт варьируется от $00 (невидимый) до $FF (непрозрачный), что и делает штамп черновика водяным знаком, а не сплошным блоком: $30 — это примерно девятнадцать процентов непрозрачности, достаточно, чтобы можно было читать сквозь него. Угол поворачивает фрагмент текста против часовой стрелки вокруг его привязки, поэтому 45 градусов дают классический штамп из угла в угол. Ни для чего из этого не нужна отдельная функция водяного знака. Водяной знак — это просто большой, полупрозрачный, повернутый вызов AddText, и то, рисуете ли вы его до или после основного текста, определяет, будет ли он находиться позади или поверх контента
Шрифты заслуживают внимательного отношения, потому что режим сбоя является тихим. Когда вы передаете название шрифта, компонент PDFium запрашивает у операционной системы данные TrueType для этого шрифта и внедряет их в документ, поэтому файл, созданный на вашем компьютере, отображается идентично на компьютере, где этот шрифт никогда не был установлен. Подвох заключается в том, что происходит, когда название не разрешается: опечатка или гарнитура, которая просто не установлена на компьютере сборки. Исключения не возникает. Библиотека возвращается к созданию текстового объекта, который несет имя только в качестве метки, без какого-либо внедрения, и оставляет средству просмотра возможность подставить то, что оно сочтет близким. Текст появляется в ваших тестах, выглядит правдоподобно, а метрики или глифы сдвигаются в тот момент, когда файл открывается где-то с другими установленными шрифтами. Используйте имена, которые, как вы знаете, присутствуют на генерирующей машине, относитесь к списку шрифтов как к зависимости развертывания и откройте образец в средстве просмотра на чистой системе, прежде чем доверять выводу
Векторные фигуры: создайте путь, затем зафиксируйте его
Линии, прямоугольники и заполненные области проходят через путь. Вы открываете путь с помощью CreatePath, который устанавливает начальную точку и сразу весь стиль: режим заливки, цвета заливки и обводки с их собственными альфа-байтами, ширину обводки, концы линий и соединения. Затем вы расширяете его с помощью LineTo, BezierTo и ClosePath, и, наконец, AddPath фиксирует готовый путь на странице. Шаг фиксации легко забыть, и если вы его пропустите, ничего не будет создано
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
// A thin horizontal rule. The rectangle overload sets a box directly:
// X, Y, Width, Height, then fill mode and colors.
Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
True, clBlack, $FF, 1.0);
Pdf.AddPath;
end;
procedure DrawTriangle(Pdf: TPdf);
begin
// Point overload: start at the first vertex, line to the rest, close.
Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
Pdf.LineTo(300, 300);
Pdf.LineTo(250, 400);
Pdf.ClosePath;
Pdf.AddPath; // nothing is drawn until this runs
end;
Две перегрузки покрывают общие случаи. Форма с четырьмя координатами принимает X, Y, ширину и высоту и дает вам выровненный по осям прямоугольник за один вызов, к чему вы и обращаетесь, чтобы нарисовать линейку, границу ячейки или заполненную фоновую панель. Форма с двумя координатами устанавливает только начальную точку, а остальную часть контура вы обводите самостоятельно с помощью LineTo и BezierTo. Режим заливки управляет тем, как закрашиваются перекрывающиеся области: fmWinding (ненулевая обмотка) подходит для большинства сплошных фигур, fmAlternate (четное-нечетное) обрабатывает вырезы и самопересекающиеся контуры, а fmNone оставляет путь только с обводкой без заливки, что и использует разделитель выше
Таблицы — это пути и текст, собранные вручную
Поскольку примитива таблицы не существует, таблица — это цикл. Вы определяете смещения X для столбцов и высоту строк, записываете каждую ячейку с помощью AddText и рисуете линейки с помощью прямоугольных путей. Арифметика остается за вами, но она проста, и будучи однажды написанной, она обобщается на любую сетку, которая вам нужна
procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
ColX: array[0..2] of Double = (0, 110, 210); // column offsets
RowH = 20;
var
Y: Double;
Row: Integer;
begin
// Header row
Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);
// Rule under the header
Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
Pdf.AddPath;
// Data rows, stepping Y downward each iteration
Y := Top;
for Row := 1 to 3 do
begin
Y := Y - RowH;
Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
end;
end;
Обратите внимание, что координата Y на каждом шаге опускается вниз на высоту строки, опять же потому, что положительное направление — вверх. Здесь также проявляется отсутствие измерения текста: ничто не мешает длинному названию элемента перейти в следующий столбец, поскольку библиотека не знает, какой ширины отрендерилась ваша строка. Для вывода с фиксированным форматом, где вы контролируете данные, вы с запасом выбираете размер столбцов и двигаетесь дальше. Для по-настоящему переменного контента вы либо ограничиваете ввод, либо самостоятельно измеряете ширину глифов перед их размещением; в этот момент специализированная библиотека компоновки начинает окупать себя
Изображения и многостраничность
Растровый контент поступает через вспомогательные функции для изображений. AddPicture принимает загруженный TPicture и помещает его в точку, с дополнительными параметрами ширины и высоты для его масштабирования; AddImage принимает путь к файлу или TBitmap напрямую, а AddJpegImage транслирует байты JPEG без цикла через растровое изображение. Как и во всем остальном, координаты размещения — это левый нижний угол изображения в пространстве пользователя, а ширина и высота — это размер на странице в пунктах, а не размеры источника в пикселях
procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
Pdf: TPdf;
P: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
for P := 1 to PageCount do
begin
Pdf.AddPage(P, 595, 842); // append; the new page becomes current
Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
'Arial', 10, 50, 30); // footer near the bottom edge
// ... draw this page's body here ...
end;
Pdf.SaveAs(FileName);
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Многостраничный документ — это шаблон одной страницы, помещенный в цикл. Каждый AddPage добавляет страницу и делает ее текущей, поэтому основная часть и нижний колонтитул, которые вы рисуете следующими, попадают на ту страницу, которую вы только что добавили. Вы не переназначаете PageNumber внутри этого цикла, потому что добавление страницы уже переместило курсор туда; вам нужен PageNumber только тогда, когда вы возвращаетесь к странице не в порядке ее создания. Вызовите SaveAs один раз в конце, после заполнения последней страницы. Если вам нужен архивный профиль, а не обычный файл, тот же объект документа предоставляет методы SaveAsPdfA и другие варианты соответствия стандартам, поэтому выбор стандарта вывода — это отдельный вызов сохранения, а не другой путь сборки
Где это применимо
Честная формулировка заключается в том, что API создания документов компонента PDFium — это точный, тонкий слой над объектной моделью страницы PDFium: реальное создание документов, реальные встроенные шрифты, реальный векторный и растровый контент, сериализуемые в соответствующий стандартам файл. Он не является и не претендует на звание механизма перекомпоновки документов. Разделительной линией является верстка текста. Если ваш вывод — это шаблоны, счета, сертификаты, этикетки, информационные панели, отрендеренные по фиксированной сетке, то модель абсолютных координат будет прямой и быстрой, а код останется удобочитаемым. Если ваш вывод — это длинная проза, которая должна самостоятельно переноситься и разбиваться на страницы, вы будете перестраивать механизм макетирования поверх этих вызовов, а это неподходящий инструмент для этой работы. Понимание того, на какой стороне этой линии вы находитесь, — это большая часть решения
Методы создания, описанные здесь, являются частью компонента PDFium для Delphi, который сочетает этот путь создания с функциями рендеринга и извлечения текста, благодаря которым PDFium более известен