Виклик, який розміщує текст на сторінці PDF, є простим. Ви передаєте в AddText рядок, шрифт, розмір і позицію, і гліфи з'являються. Чого він не робить, так це не повідомляє вам, якої ширини буде цей рядок після його малювання, і не розбиває довгий рядок на кілька ліній. Один виклик малює один блок тексту в одній позиції. Якщо блок ширший за колонку, в яку він мав би поміститися, він просто виходить за її межі, і ніщо у виклику малювання не попереджає вас про це. У той момент, коли вам потрібен абзац, а не один підпис, елементом, якого не вистачає, є ширина рядка у вибраному шрифті та розмірі, виміряна до того, як ви зафіксуєте його на сторінці
Це класична проблема верстки (layout). Щоб розмістити абзац у колонці, ви повинні знати, слово за словом, скільки горизонтального простору займе кожен кандидат на рядок, і ви повинні знати це до початку малювання. Перенесення слів - це цикл вимірювання, що обертається навколо виклику малювання, і прив'язка (binding), яка лише малює, дає вам другу половину. Підтримка вимірювання тексту в компоненті PDFium заповнює цю прогалину двома функціями, MeasureText і MeasureTextWidth, які повідомляють про відрендерені розміри рядка, не залишаючи жодної позначки на будь-якій сторінці
Чому вимірювання є помічником класу (class helper), а не новим методом у TPdf
Підтримка вимірювання реалізована як помічник класу (class helper) Delphi для TPdf, який міститься у власному модулі (unit), а не як нові методи, жорстко прив'язані до класу TPdf. Помічник класу - це функція мови, яка дозволяє приєднувати методи до наявного типу з-поза його оголошення. Щойно модуль з'являється в області видимості, нові методи викликаються так само, як якби вони належали класу, тому допоміжний метод виглядає як Pdf.MeasureTextWidth(...) без окремого об'єкта, який потрібно створювати або передавати
Причиною такого розшарування є поділ. Основний тип TPdf залишається таким, як є, без жодних доданих полів і без жодних змін в існуючих сигнатурах, тому проєкт, який ніколи не потребує верстки, ніколи не несе в собі код для вимірювання. Проєкт, якому він потрібен, додає один модуль у розділ uses, і методи активуються. Можливість стає опціональною (opt-in) на рівні деталізації одного модуля, що є найчистішим способом розширити тип, яким ви не володієте або який не хочете порушувати
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 зчитує обмежувальну рамку об'єкта (bounding box). 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;
Координати та одиниці виміру результату
Обмежувальна рамка повертається як чотири краї: лівий, нижній, правий і верхній, а два виміри виходять шляхом віднімання. Ширина - це права координата мінус ліва, а висота - верхня мінус нижня. Обидва виражені в користувацьких одиницях (user units) PDF, де одна одиниця становить одну сімдесят другу дюйма - той самий координатний простір, у якому ви розміщуєте текст на сторінці. На цьому етапі немає прихованих одиниць пристрою і немає жодних пікселів. Ширина 36 означає пів дюйма сторінки, незалежно від кінцевої роздільної здатності рендерингу
Вертикальна вісь спрямована так, як її визначає PDF, з Y, що збільшується вгору, саме тому висота - це верхня координата мінус нижня, а не навпаки. Ця деталь має значення, коли ви переміщуєте курсор вниз по колонці. Ви вимірюєте висоту рядка, потім віднімаєте її від поточної базової лінії, щоб знайти наступну, оскільки рух вниз по сторінці означає рух до менших значень Y. Якщо вашим пунктом призначення є екран, а не папір, ви конвертуєте користувацькі одиниці в пікселі пристрою за допомогою роздільної здатності дисплея: значення в користувацьких одиницях, помножене на DPI і розділене на 72, дає пікселі, тому ширину колонки, яку ви встановлюєте в пунктах, можна зіставити з виміряним блоком тексту до того, як ви вирішите, де зробити розрив
Що відбувається при вироджених (degenerate) вхідних даних
Функції написані так, щоб завершуватися тихо (fail quietly). Якщо немає відкритого документа або якщо текстовий об'єкт неможливо створити, результатом є нульовий розмір, а не викликаний виняток (exception). Ширина та висота ініціалізуються нулем на початку і перезаписуються лише після успішного зчитування обмежувальної рамки. Порожній рядок, відсутній документ, шрифт, який бібліотека не може перетворити на об'єкт - кожне з цих значень повертає нуль замість виклику винятку
Цей вибір зберігає цикл вимірювання простим, оскільки цикл, який проходить через тисячі слів, не є місцем для обробки винятків на кожній ітерації. Платою за це є те, що перевірку виконує той, хто викликає функцію. Нульова ширина - це маркер, а не факт про текст, тому код, який ділить на виміряну ширину або передбачає позитивне значення, повинен захищатися від нуля, перш ніж довіряти йому. Ставтеся до нуля як до "не вдалося виміряти", і контракт буде зрозумілим; проігноруйте його, і вироджені вхідні дані непомітно перетворяться на макет із колонкою гліфів, що накладаються один на одного
Жадібне (greedy) перенесення слів, побудоване на вимірюванні
Маючи функцію ширини, перенесення слів стає коротким жадібним (greedy) циклом. Ви розбиваєте абзац на слова, зберігаєте поточний рядок, і для кожного слова вимірюєте, яким би став рядок, якби ви додали це слово. Поки пробний рядок все ще вміщується в ширину колонки, ви продовжуєте додавати; коли він переповнюється, ви скидаєте (flush) поточний рядок за допомогою AddText і починаєте новий зі слова, яке не помістилося. Накопичення виконується повністю за допомогою MeasureTextWidth, і єдине, що коли-небудь потрапляє на сторінку, - це рядок, щодо якого ви вже переконалися, що він підходить
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<WideString>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := WideString(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(X, Y, Font, FontSize, Line); // 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(X, Y, Font, FontSize, Line); // flush the final line
end;
Цикл вимірює пробний рядок, а не вимірює кожне слово і підсумовує їх, оскільки ширина рядка не є сумою ширин його слів. Пробіли між словами також мають вплив, і виміряний блок фіксує це безпосередньо. Жадібне правило - вмістити стільки слів, скільки дозволяє колонка, і зробити розрив на останньому, яке поміщається - це те саме правило, яке заповнює прогалину між "чистим" AddText і реальним абзацом. Виклик малювання ніколи не був складною частиною. Вимірювання, яке має йому передувати, є таким, і це саме те, що надає помічник (helper)
Де це застосовується
Вимірювання - це шар між генерацією контенту та його рендерингом, тому він природно поєднується з рештою робочого процесу створення документа з нуля. Якщо ви збираєте сторінки та розміщуєте текст у першу чергу, основа закладається в статті створення PDF-документів з нуля за допомогою компонента PDFium у Delphi, де виклики AddText та налаштування сторінки розглядаються в повному обсязі. Коли шрифт, який ви вимірюєте, має таке ж значення, як і рядок, оскільки метрика залежить від гарнітури, стаття аналіз властивостей шрифтів PDF за допомогою компонента PDFium у Delphi показує, як бібліотека повідомляє інформацію про шрифт, що визначає ці обмежувальні рамки. Обидві спираються на одну й ту саму прив'язку (binding) - PDFium Component для Delphi та Lazarus, де допоміжний модуль для вимірювання постачається разом з API документа, сторінки та тексту, описаними в цьому блозі