Виклик, який розміщує текст на сторінці PDF, є досить простим. Ви передаєте AddText рядок, шрифт, розмір та позицію, і гліфи з'являються. Чого він не робить, так це не каже вам, наскільки широким буде цей рядок після його малювання, і він не розбиває довгий рядок на кілька рядків. Один виклик малює один блок тексту в одній позиції. Якщо цей блок ширший за стовпець, у який ви хотіли його вписати, він просто виходить за край, і ніщо у виклику малювання вас про це не попереджає. У той момент, коли вам потрібен абзац, а не просто один надпис, відсутнім елементом стає ширина рядка у вибраному шрифті та розмірі, виміряна перед його розміщенням на сторінці
Це класична проблема верстки (макетування). Щоб перенести абзац у стовпець, ви повинні знати, слово за словом, скільки горизонтального простору займе кожен потенційний рядок, і ви повинні знати це до того, як почнете щось малювати. Перенесення слів — це цикл вимірювання, обгорнутий навколо виклику малювання, і обв'язка, яка лише малює, дає вам тільки другу половину. Підтримка вимірювання тексту в компоненті PDFium заповнює цю прогалину двома функціями, MeasureText та MeasureTextWidth, які повідомляють про відрендерений розмір рядка без жодної позначки на будь-якій сторінці
Чому вимірювання є помічником класу (class helper), а не новим методом у TPdf
Підтримка вимірювання реалізована як помічник класу (class helper) Delphi для TPdf, що знаходиться у власному модулі, а не як нові методи, прикріплені до класу TPdf. Помічник класу — це функція мови, яка дозволяє додавати методи до існуючого типу поза його оголошенням. Як тільки модуль потрапляє в область видимості, нові методи викликаються так само, як якби вони належали до класу, тому метод-помічник читається як Pdf.MeasureTextWidth(...) без необхідності створювати або передавати окремий об'єкт
Причиною такого поділу є сепарація (розділення). Основний тип TPdf залишається таким, як є: без додавання нових полів і без змін існуючих сигнатур, тому проєкт, який ніколи не потребує макетування, ніколи не містить коду вимірювання. Проєкт, якому це потрібно, додає один модуль у розділ uses, і методи стають доступними. Можливість стає опціональною на рівні окремого модуля, що є найчистішим способом розширити тип, яким ви не володієте або який не хочете порушувати
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // the helper unit; brings MeasureText into scope on TPdf
// With the unit in scope the methods read as members of TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W and H are now the rendered width and height in PDF user units
end;
Вимірювання без втручання в сторінку
Вимірювання має бути без побічних ефектів. Воно повинно повідомляти ширину, не залишаючи нічого після себе, оскільки ви викликаєте його багато разів під час вибору макета, і сторінка повинна виглядати точно так само, як ніби ви нічого не вимірювали взагалі. Техніка, яка робить це можливим, полягає у створенні текстового об'єкта, запиті його розміру та його знищенні ще до того, як він коли-небудь буде прикріплений до сторінки
Послідовність складається з чотирьох викликів PDFium. FPDFPageObj_NewTextObj створює текстовий об'єкт для документа на основі назви та розміру шрифту. FPDFText_SetText встановлює рядок, який містить об'єкт. FPDFPageObj_GetBounds зчитує обмежувальну рамку об'єкта. FPDFPageObj_Destroy звільняє об'єкт. Важливо, що ніщо в цій послідовності не викликає API вставлення сторінки. Об'єкт створюється, опитується та знищується ізольовано, тому документ залишається незмінним під час повернення з функції. Це одноразовий зонд, єдиним результатом якого є чотири числа його обмежувальної рамки
Це надійний спосіб зробити це, оскільки PDFium не надає зручної ширини просування (advance width) для кожного гліфа, яку ви могли б підсумувати самостійно. Метрики гліфів залежать від програми шрифту, від кодування та від того, як PDFium завантажує гарнітуру, і не існує відкритого виклику, який би надавав просування кожного символу в рядку. З іншого боку, обмежувальна рамка реального текстового об'єкта обчислюється тим самим механізмом, який би розміщував гліфи для малювання, тому він відображає фактичний відрендерений розмір, а не наближене значення. Створення одного одноразового об'єкта та зчитування його меж — це найнадійніше вимірювання, яке може надати бібліотека
// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
FontSize: Single; out Width, Height: Double);
var
TextObject: FPDF_PAGEOBJECT;
L, B, R, T: Single;
begin
Width := 0;
Height := 0;
if Self.Document = nil then
Exit;
TextObject := FPDFPageObj_NewTextObj(Self.Document,
FPDF_BYTESTRING(AnsiString(Font)), FontSize);
if TextObject = nil then
Exit;
try
if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
Exit;
if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
begin
Width := R - L;
Height := T - B;
end;
finally
FPDFPageObj_Destroy(TextObject); // probe discarded, page untouched
end;
end;
Координати та одиниці виміру результату
Обмежувальна рамка повертається у вигляді чотирьох країв: лівого, нижнього, правого та верхнього, а два виміри отримуються шляхом віднімання. Ширина — це праве значення мінус ліве, а висота — верхнє мінус нижнє. Обидва виражені в користувацьких одиницях PDF, де одна одиниця становить 1/72 дюйма, тобто в тому самому координатному просторі, в якому ви позиціонуєте текст на сторінці. На цьому етапі немає ніяких прихованих одиниць пристрою та пікселів. Ширина 36 означає півдюйма сторінки, якою б не була остаточна роздільна здатність рендерингу
Вертикальна вісь спрямована так, як її визначає PDF: значення Y збільшується вгору, саме тому висота — це верхнє значення мінус нижнє, а не навпаки. Ця деталь має значення, коли ви переміщуєте курсор вниз по стовпцю. Ви вимірюєте висоту рядка, а потім віднімаєте її від поточної базової лінії, щоб знайти наступну, оскільки рух вниз по сторінці означає рух до меншого значення Y. Якщо вашим місцем призначення є екран, а не папір, ви перетворюєте користувацькі одиниці у пікселі пристрою за допомогою роздільної здатності дисплея: значення в користувацьких одиницях, помножене на DPI та розділене на 72, дає пікселі, тому ширину стовпця, встановлену в пунктах, можна порівняти з виміряним блоком тексту перед тим, як ви вирішите, де зробити перенесення
Що відбувається при вироджених (некоректних) вхідних даних
Функції написані так, щоб зазнавати невдачі без зайвого шуму. Якщо документ не відкрито, або якщо текстовий об'єкт неможливо створити, результатом буде нульовий розмір, а не згенерований виняток. Ширина та висота ініціалізуються нулем на початку і перезаписуються лише після успішного зчитування обмежувальної рамки. Порожній рядок, відсутність документа, шрифт, який бібліотека не може розпізнати як об'єкт — усе це повертає нуль, а не викликає помилку
Такий вибір робить цикл вимірювання простим, оскільки цикл, який перебирає тисячі слів, не є місцем для обробки винятків на кожній ітерації. Платою за це є те, що перевірка лягає на того, хто викликає метод. Нульова ширина — це сигнальне значення, а не факт про текст, тому код, який ділить на виміряну ширину або очікує додатного значення, повинен захищатися від нуля, перш ніж довіряти йому. Вважайте нуль як "не вдалося виміряти", і контракт стає зрозумілим; якщо ви проігноруєте його, некоректне введення непомітно перетвориться на макет зі стовпцем гліфів, що накладаються один на одного
Жадібний алгоритм перенесення слів, побудований на вимірюванні
Маючи під рукою функцію вимірювання ширини, перенесення слів стає коротким "жадібним" циклом. Ви розбиваєте абзац на слова, ведете поточний рядок і для кожного слова вимірюєте, яким буде рядок, якщо ви додасте це слово. Поки пробний рядок поміщається в ширину стовпця, ви продовжуєте додавати слова; коли відбувається переповнення, ви виводите поточний рядок за допомогою AddText і починаєте новий рядок зі словом, яке не помістилося. Накопичення виконується повністю за допомогою MeasureTextWidth, і єдине, що потрапляє на сторінку — це рядок, який, як ви вже підтвердили, вміщується
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<string>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := string(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Measure the candidate line before drawing anything.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(Line, Font, FontSize, X, Y); // flush the line that fit
Y := Y - LineHeight; // Y decreases going down
Line := Words[I]; // overflowing word starts next line
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(Line, Font, FontSize, X, Y); // flush the final line
end;
Цикл вимірює пробний рядок, а не вимірює кожне слово окремо з подальшим підсумовуванням, оскільки ширина рядка не дорівнює сумі ширин його слів. Відстані між словами також мають значення, і вимірюваний блок фіксує це безпосередньо. "Жадібне" правило — вмістити стільки слів, скільки дозволяє стовпець, і перервати на останньому, що вміщується, — це те саме правило, яке заповнює прогалину між "сирим" AddText та справжнім абзацом. Виклик малювання ніколи не був складною частиною. Вимірювання, яке має йому передувати, — ось що складно, і це саме те, що надає помічник
Де це застосовується
Вимірювання — це прошарок між генерацією контенту та його рендерингом, тому воно природно поєднується з рештою робочого процесу створення документа "з нуля". Якщо ви створюєте сторінки та розміщуєте текст, то базові принципи описані у статті Створення PDF-документів з нуля за допомогою компонента PDFium у Delphi, де виклики AddText та налаштування сторінки розкриті повністю. Коли шрифт, який ви вимірюєте, має таке ж значення, як і рядок, оскільки метрики залежать від гарнітури, стаття Аналіз властивостей шрифтів PDF за допомогою компонента PDFium у Delphi показує, як бібліотека надає інформацію про шрифти, що визначає ці обмежувальні рамки. Обидва матеріали базуються на одній і тій самій обв'язці — Компонент PDFium для Delphi та Lazarus, де помічник з вимірювання постачається разом із API для роботи з документами, сторінками та текстом, описаними в цьому блозі