PDFlibPas преобразует содержимое PDF в два редактируемых формата без автоматизации Office. ExportPageMarkdown и ExportDocumentMarkdown возвращают семантический Markdown с распознанными заголовками, нумерованными и маркированными списками и таблицами в стиле pipe, а SaveDOCXToFile и SaveDOCXToStream записывают пакет WordprocessingML с абзацами, заголовками, нативной нумерацией списков, распознанными таблицами, форматированием шрифта, разрывами страниц и позиционированными изображениями PNG
Оба пути работают полностью на Pascal, на сервере, без установленного Word и без COM. Именно это ограничение — причина того, что функция существует в PDF-библиотеке, а не в настольном инструменте
Почему «PDF в Word» — действительно сложная задача?
Потому что страница PDF не содержит абзацев. Она содержит операторы вывода текста, которые размещают прогоны глифов по координатам в том порядке, в каком их выпустил генератор, без какого-либо обязательства указывать, что два прогона принадлежат одному предложению, не говоря уже об одном пункте списка. Формат был спроектирован для точного описания напечатанной страницы, и он справляется с этим именно за счёт отбрасывания структуры, которая эту страницу породила
Поэтому любому конвертеру приходится восстанавливать то, что генератор выбросил. Группировка строк выводится из вертикальных интервалов и выравнивания по базовой линии. Границы абзацев — из изменений интервалов и отступов. Заголовок — это строка, чей шрифт крупнее или жирнее, чем у основного текста, и которая обособлена от того, что следует далее. Список — это последовательность абзацев, начинающихся с символа маркера или шаблона нумерации. Таблица — это сетка текстовых блоков, чьи границы выравниваются по строкам и столбцам. Каждое из этих правил — вывод, основанный на предположении, а предположение означает хороший результат на документах, следующих обычным типографским соглашениям, и посредственный — на документах, которые им не следуют
Тегированные PDF — исключение, причём весомое. Когда документ несёт дерево структуры, роли абзаца, заголовка, списка и таблицы зафиксированы, а не угаданы, и именно поэтому работа по доступности, описанная в статье о структуре доступности тегированного PDF, окупается ещё и в качестве конвертации. Если вы контролируете генератор, разметка вывода тегами — это единственное действие с максимальной отдачей для всех, кому впоследствии придётся этот документ конвертировать
Экспорт в Markdown постранично
Путь через Markdown стоит выбирать, когда назначение — текстовый конвейер: сайт документации, поисковый индекс, корпус для поиска ассистента. Опции — это битовая маска: PDF_MARKDOWN_INCLUDE_PAGE_MARKERS, PDF_MARKDOWN_DETECT_HEADINGS, PDF_MARKDOWN_PRESERVE_STYLES, а PDF_MARKDOWN_DEFAULT объединяет все три
var
Pdf: TPDFlib;
Md: WideString;
begin
Pdf := TPDFlib.Create;
try
Pdf.LoadFromFile('handbook.pdf', '');
// Одна страница в виде строки
Md := Pdf.ExportPageMarkdown(1, PDF_MARKDOWN_DEFAULT);
// Диапазон страниц, потоково записанный на диск в UTF-8 без BOM
Pdf.SaveMarkdownToFile('1-40',
PDF_MARKDOWN_DETECT_HEADINGS or PDF_MARKDOWN_PRESERVE_STYLES,
'handbook.md');
finally
Pdf.Free;
end;
end;
Маркеры страниц окупают себя в задачах поиска и извлечения. Фрагмент текста, несущий информацию о странице, с которой он взят, можно точно процитировать, и читатель, перешедший по ссылке, попадает именно туда, где утверждение действительно находится. Отключайте их, когда Markdown предназначен для чтения человеком, где границы страниц из исходной вёрстки — это просто шум
Потоковые точки входа важны для крупных документов. SaveMarkdownToStream и SaveMarkdownToFile записывают UTF-8 постранично и не буферизуют весь вывод целиком, поэтому руководство на 900 страниц не превращается сначала в строку на 900 страниц в памяти. Отсутствие метки порядка байт (BOM) тоже намеренное: BOM в файле Markdown сбивает с толку удивительно много генераторов статических сайтов и инструментов сравнения diff
DOCX без Office на машине
Модуль записи DOCX сам формирует пакет: записи ZIP через сырой Deflate с проверкой CRC, части WordprocessingML и связывающие их отношения. Ничто не обращается к Word, а значит, конвертация работает на сервере без графической подсистемы, под служебной учётной записью, в контейнере — везде, где автоматизация Office либо нелицензирована, либо нестабильна, либо запрещена
var
Pdf: TPDFlib;
Target: TFileStream;
begin
Pdf := TPDFlib.Create;
Target := TFileStream.Create('handbook.docx', fmCreate);
try
Pdf.LoadFromFile('handbook.pdf', '');
Pdf.SaveDOCXToStream('1-40',
PDF_DOCX_INCLUDE_IMAGES or PDF_DOCX_DETECT_HEADINGS or
PDF_DOCX_PRESERVE_STYLES or PDF_DOCX_PRESERVE_PAGE_BREAKS,
Target);
finally
Target.Free;
Pdf.Free;
end;
end;
Данные изображений записываются по мере обработки каждой страницы, а не собираются и не дописываются в конце, поэтому пиковое потребление памяти отслеживает одну страницу, а не весь документ. Явный порядок страниц сохраняется, а выбранная страница PDF восстанавливается после экспорта — это важно, когда экспорт является лишь одним шагом внутри более длинного задания, в котором страница была выбрана по другим причинам
Что даёт детерминированная упаковка?
Побайтовую воспроизводимость. Две конвертации одного и того же входа с одинаковыми опциями дают одинаковый пакет, а значит, можно хешировать результат для обнаружения изменений, сравнивать diff двух сборок сгенерированного документа и агрессивно кэшировать, не опасаясь, что идентичный вход дал разный артефакт
Автоматизация Office такого не гарантирует. Она встраивает временные метки, идентификаторы ревизий и зависящие от машины метаданные, поэтому один и тот же документ, конвертированный дважды, отличается способом, который делает хеширование бессмысленным. Та же логика лежит в основе детерминированных идентификаторов файлов, обсуждаемых в статье о детерминированных PDF ID для воспроизводимых сборок: когда результат воспроизводим, проверка превращается в сравнение вместо инспекции
Где результат хорош, а где нет
Будьте честны с пользователями на этот счёт, потому что качество конвертации зависит от входных данных сильнее, чем от самого конвертера. Тегированные PDF и аккуратно сгенерированные деловые документы — счета, отчёты, договоры — конвертируются хорошо: заголовки становятся заголовками, таблицы сохраняются, списки корректно перенумеровываются в Word. Двухколоночная академическая вёрстка конвертируется приемлемо, если геометрия колонок регулярна. Таблицы, разрывающиеся между страницами, собираются заново по предположению и иногда разделяются. Насыщенно оформленные маркетинговые материалы, где текст размещён ради визуального эффекта, а не в порядке чтения, конвертируются плохо, и никакое количество эвристик этого не исправит
Отсканированные документы — это совершенно отдельный случай. Страница, представляющая собой одно большое изображение, не содержит текстовых объектов, поэтому экспортировать нечего, пока не появится текстовый слой; путь OCR, создающий такой слой, — это обязательное условие, а не опция. Прежде чем запускать крупный пакет, возьмите дюжину показательных файлов и посмотрите на результат, а также рассмотрите предварительное перечисление элементов страницы, как описано в статье о поиске текста и перечислении элементов страницы, чтобы понять, что реально содержат страницы
Для конвейеров ассистентов и поиска путь через Markdown обычно оказывается лучшей целью: заголовки становятся границами фрагментов, таблицы остаются читаемыми в виде pipe-таблиц, а маркеры страниц дают каждому фрагменту цитируемое местоположение. Для редактирования человеком ответ — DOCX, потому что пользователю нужен не сам текст, а возможность его изменить
PDFlibPas — это библиотека PDF для Delphi, C++Builder и Lazarus с соответствующими интерфейсами DLL и ActiveX, поэтому те же вызовы экспорта доступны из C#, C++ или скриптовых хостов. Полная документация и пробная сборка доступны на странице PDFlibPas Delphi PDF library