PDFium має репутацію рушія для перегляду документів, оскільки саме він відповідає за рендеринг вкладок PDF у Chrome. Тому найперше, що варто прояснити: PDFium Component також дозволяє створювати документи, яких раніше не існувало. Сторона створення базується на API сторінкових об'єктів PDFium: ви створюєте порожній документ, додаєте сторінки з точними розмірами та розміщуєте текст, векторні контури й зображення на кожній сторінці за вказаними координатами. Тут немає потреби вивчати мову опису сторінок чи використовувати драйвери принтера. Ви викликаєте методи, бібліотека формує об'єкти PDF, а метод SaveAs серіалізує готовий результат
Чого ви не отримаєте, так це рушія компонування (layout engine). Про це варто сказати відразу, оскільки це визначає кожен наведений нижче приклад. PDFium Component розміщує контент саме там, де ви вкажете, в абсолютних координатах, і ніде інше. Він не буде переносити абзаци, розбивати текст на кілька сторінок або автоматично обчислювати таблицю з рядків та стовпців. Це ваше завдання. Якщо ви очікували інструмент, який автоматично переформатовує текст, як текстовий процесор, змініть очікування: це точний низькорівневий 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;
Одна деталь часто спантеличує розробників, які бачили старі приклади коду: після виклику CreateDocument не потрібно присвоювати Pdf.Active := True. Властивість 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, а не 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 Component запитує в операційної системи дані 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 без проміжних конвертацій у bitmap. Як і в усіх інших випадках, координати розміщення відповідають нижньому лівому куту зображення в просторі користувача, а ширина та висота вказують розмір на сторінці в пунктах, а не розмір джерела в пікселях
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 Component - це тонкий прошарок, який точно відтворює об'єктну модель сторінки PDFium. Він забезпечує реальне створення документа, роботу із вбудованими шрифтами, векторним і растровим контентом та їхню серіалізацію у файл, що відповідає стандартам. Це не рушій для роботи з потоковими документами і він на це не претендує. Головна межа полягає у форматуванні тексту. Якщо ваш результат - це шаблон, як-от рахунки-фактури, сертифікати, етикетки чи панелі керування, прив'язані до фіксованої сітки, модель абсолютних координат буде прямою, швидкою, а код залишиться читабельним. Якщо ж ваш результат - це довгий текст, який потрібно розбивати на абзаци та сторінки, вам доведеться будувати власний рушій компонування поверх цих викликів, а для таких завдань це просто неправильний інструмент. Розуміння того, на якому боці цієї межі ви знаходитесь, і є найважливішим етапом ухвалення рішення
Описані тут методи створення є частиною компонента PDFium Component для Delphi, який об'єднує функції створення з можливостями рендерингу та вилучення тексту, якими більше відомий PDFium