PDFium Component превращает PDF с фиксированной раскладкой в семантическую модель, которую можно переверстать, используя BuildReflowDocument, и экспортирует эту модель как автономный HTML через ToHtml. Заголовки остаются заголовками, элементы списка остаются элементами списка, а таблицы, обнаруженные на странице, выходят в виде настоящей табличной разметки с сохранёнными ячейками заголовков и объединениями. Ничто в выводе не ссылается на внешний скрипт или таблицу стилей
Причина, по которой это нужно, в том, что страница PDF — это набор позиционированных глифов, что абсолютно не подходит для экрана телефона, программы чтения с экрана или поискового индекса. Каждая попытка решить это извлечением обычного текста теряет структуру, делавшую документ читаемым, а каждая попытка решить это конвертацией страниц в изображения теряет текст целиком. Модель рефлоу сохраняет и то и другое: слова и связи между ними
Откуда берётся семантическая информация?
Всё начинается с GetStructuredText, единственного источника текста и семантики в компоненте. Когда PDF несёт структурное дерево — тегированный PDF, как определено в разделе 14.7 ISO 32000-1, — модель следует логической иерархии, записанной производителем. Когда его нет, а большинство встречающихся в реальности 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 — это решение вёрстки, принятое для фиксированной ширины страницы, поэтому сохранение его на узком экране воспроизводит ту самую проблему, ради решения которой существует рефлоу. Сохраняйте разрывы для поэзии, листингов кода и адресов; отбрасывайте для прозы
Бюджеты, отмена и состояние страницы
Символы, узлы, таблицы и ячейки — у каждого свой потолок, и каждый проверяется до выделения памяти, а не после, так что искажённый или враждебный документ падает аккуратно, а не потребляет память, пока что-то другое не рухнет. Токен отмены проверяется на границах страниц, блоков, таблиц, строк и ячеек, что удерживает отзывчивость при отменённом сканировании тысячестраничного документа
Одно поведение важно именно для приложений с графическим интерфейсом: всё сканирование документа выполняется внутри области видимости, восстанавливающей активную страницу, поэтому успех, сбой по бюджету и отмена одинаково оставляют текущую страницу вызывающего кода нетронутой. Просмотрщик, позволяющий пользователю экспортировать, глядя на страницу 340, обнаруживает себя всё ещё на странице 340 после операции
Для чего рефлоу хорош и для чего нет
Вывод рефлоу — отличный вход для поискового индексирования, доступных режимов чтения, мобильного отображения и миграции контента. Это не конвертер, сохраняющий точность: абсолютные позиции, точные шрифты, векторная графика и точная геометрия страницы намеренно вне его назначения. Когда задаче нужно, чтобы страница выглядела так же, рендерите её; когда нужно, чтобы страница была читаема где-то ещё, переверстывайте её
Именно для вспомогательных технологий модель рефлоу сочетается с функциями чтения, описанными в статье построение доступной программы чтения, а документы, несущие настоящее структурное дерево, дают заметно лучшие модели, что является хорошим аргументом в пользу проверки тегирования заранее, как описано в статье проверка структурного дерева PDF/UA
Рефлоу, структурированный текст, проверка тегирования и рендеринг используют один объект документа в Delphi, C++Builder и Lazarus; полный API описан на странице компонента PDFium для Delphi