PDFium Component перетворює PDF із фіксованою версткою на семантичну модель, придатну для переверстки, за допомогою BuildReflowDocument, і експортує цю модель як самодостатній HTML через ToHtml. Заголовки лишаються заголовками, елементи списку лишаються елементами списку, а таблиці, виявлені на сторінці, виходять як справжня розмітка таблиці зі збереженими комірками заголовків і об'єднаннями. Ніщо у виводі не посилається на зовнішній скрипт чи таблицю стилів
Причина, з якої це потрібно, у тому, що сторінка PDF — це набір позиціонованих гліфів, а це саме те, що не підходить для екрана телефону, програми читання з екрана чи пошукового індексу. Будь-яка спроба вирішити це вилученням простого тексту втрачає структуру, що робила документ читабельним, а будь-яка спроба вирішити це конвертацією сторінок у зображення втрачає текст повністю. Модель переверстки зберігає обидва: слова та зв'язки між ними
Звідки береться семантична інформація?
Усе починається з GetStructuredText — єдиного джерела тексту й семантики в компоненті. Коли PDF несе структурне дерево, тегований PDF, як визначено в ISO 32000-1, пункт 14.7, модель слідує логічній ієрархії, записаній виробником. Коли ж ні — а більшість PDF у реальному світі не мають цього — модель відкочується до фізичного порядку верстки, вже обчисленого для цілей порядку читання
Цей вибір зберігає жорстку межу: жодного другого парсера PDF і жодного другого рушія рендерингу не вводиться для відповіді на питання, на які вже може відповісти наявний. Механіка порядку читання під сподом описана у статті структуровані текстові блоки та порядок читання, а модель переверстки — це семантичний шар поверх неї, а не заміна
Кожен вузол записує, звідки надійшла його інформація, тож споживач може відрізнити заголовок, який декларував документ, від заголовка, який вивели евристики верстки. Конвеєри, чутливі до впевненості, повинні читати це поле, а не трактувати всі вузли як однаково авторитетні
Плоске дерево і чому це не дерево об'єктів
Модель — це сплющене дерево в прямому порядку обходу: масив вузлів, де кожен вузол несе ParentIndex і Depth, а не рекурсивний запис чи граф об'єктів із володінням. Сторінки, заголовки, абзаци, списки, елементи списків, ілюстрації, підписи, таблиці, рядки та комірки — усе живе в цьому одному лінійному масиві
З цього випливають дві переваги. Споживачі можуть потоково обробляти масив по порядку без рекурсії, що робить видачу HTML, Markdown чи деревовидного перегляду простим циклом. І компонування лишається переносним між Delphi, C++Builder і Free Pascal, які по-різному обробляють рекурсивні керовані типи через межу ABI. Рекурсивний запис із динамічних масивів — це саме той тип конструкції, що компілюється всюди й поводиться в кожному трохи інакше
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfReflowOptions;
Doc: TPdfReflowDocument;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.LoadDocument;
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.DetectTables := True;
Options.IncludeCss := True; // вбудований блок стилів, жодного зовнішнього файлу
Options.MaxNodes := 200000; // бюджет із відмовою за замовчуванням
Options.MaxCharacters := 4000000;
Doc := Pdf.BuildReflowDocument(Options);
for I := 0 to High(Doc.Nodes) do
case Doc.Nodes[I].Kind of
prnkHeading:
Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
prnkParagraph:
Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Copy(Doc.Nodes[I].Text, 1, 60)]));
prnkTable:
Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
end;
Writeln(Format('%d node(s), %d table(s), %d character(s)',
[Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
finally
Pdf.Free;
end;
end;
Як таблиці уникають подвійної появи?
Виявлення таблиць виконується після того, як для сторінки зібрано структурований текст, що створює очевидну небезпеку: той самий вміст комірки існує і в текстових блоках, і у виявленій таблиці. Видача обох дає HTML, де за кожною таблицею знову йде її власний вміст як окремі абзаци
Правило, що це розв'язує, геометричне. Коли виявлена таблиця покриває більше половини площі текстового блока, вузол таблиці замінює цей блок, а не приєднується до нього. Індексування комірок усередині рядка будується підрахунком у кошики, тож побудова моделі лишається лінійною за кількістю комірок плюс рядків, а не пересканує кожну комірку для кожного рядка, що важливо для фінансових документів, де одна сторінка може нести сотні комірок
Виявлена структура чесна щодо того, що вона саме виявлена. Таблиця з лініями розмежування розпізнається надійніше, ніж вирівняна лише пробілами, і впевненість вузла це відображає. Для вмісту, де неправильна таблиця краща за жодну, тримайте виявлення увімкненим; для архівної конвертації, де неправильна таблиця гірша, ставте бар'єр за впевненістю
Експорт HTML, що лишається самодостатнім
ToHtml обходить уже побудовану модель і ніколи не звертається повторно до PDFium, тож повторний експорт нічого додатково не коштує й не може дати інший результат з тієї самої моделі. Значення тексту й атрибутів екрануються однорідно, рівні заголовків обмежуються діапазоном h1–h6, який справді визначає HTML, а комірки заголовків, RowSpan і ColumnSpan проходять так, як записані
Необов'язковий CSS — це простий вбудований блок стилів. Немає жодного скрипту, вебшрифту чи зовнішнього ресурсу будь-якого роду, і саме це робить вивід безпечним для вбудовування в електронний лист, переглядач довідки чи ізольований елемент керування браузером:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // зберігати межі сторінок видимими
Options.PreserveLineBreaks := False; // дозволити браузеру переносити абзаци
Html := Pdf.BuildReflowDocument(Options).ToHtml;
Bytes := TEncoding.UTF8.GetBytes(string(Html));
Stream := TFileStream.Create('report.html', fmCreate);
try
if Length(Bytes) > 0 then
Stream.WriteBuffer(Bytes[0], Length(Bytes));
finally
Stream.Free;
end;
end;
PreserveLineBreaks — той параметр, над яким найбільше варто подумати. Розрив рядка в PDF — це рішення набору, ухвалене для фіксованої ширини сторінки, тож збереження його на вузькому екрані відтворює саме ту проблему, для розв'язання якої й існує переверстка. Зберігайте розриви для поезії, лістингів коду й адрес; відкидайте їх для прози
Бюджети, скасування та стан сторінки
Символи, вузли, таблиці та комірки — кожен має стелю, і кожна перевіряється до виділення пам'яті, а не після, тож деформований чи ворожий документ провалюється чисто, замість того щоб споживати пам'ять, доки цього не зробить щось інше. Токен скасування перевіряється на межах сторінки, блока, таблиці, рядка й комірки, що утримує скасоване сканування тисячосторінкового документа чуйним
Одна поведінка важлива саме для GUI-застосунків: усе сканування документа виконується всередині області, що відновлює активну сторінку, тож успіх, збій бюджету й скасування — усі лишають поточну сторінку викликача недоторканою. Переглядач, що дозволяє користувачеві експортувати, дивлячись на сторінку 340, після цього виявляє себе досі на сторінці 340
Для чого переверстка добра, а для чого ні
Вивід переверстки — чудовий вхід для пошукового індексування, доступних режимів читання, мобільного відображення та міграції вмісту. Це не конвертер, що зберігає точність відтворення: абсолютні позиції, точні шрифти, векторна графіка та точна геометрія сторінки лежать поза його призначенням за задумом. Коли завданню потрібно, щоб сторінка виглядала так само, рендерте її; коли потрібно, щоб сторінку можна було прочитати десь інде, переверстайте її
Саме для допоміжних технологій модель переверстки поєднується з можливостями читання, описаними у статті побудова доступного читача, а документи, що несуть справжнє структурне дерево, дають помітно кращі моделі, що є вагомим аргументом на користь валідації теговання вище за течією, як описано у статті валідація структурного дерева PDF/UA
Переверстка, структурований текст, валідація теговання й рендеринг спільно використовують один об'єкт документа в Delphi, C++Builder і Lazarus; повний API описано на сторінці PDFium Component для Delphi