Техническая статья

Извлечение текста PDF в порядке структуры с HotPDF

Каждый геометрический экстрактор текста угадывает. Он читает глифы, которые рисует страница, сортирует их по базовой линии и горизонтальной позиции и надеется, что визуальное расположение совпадает с порядком человеческого чтения. В одностролбцовом отчёте эта догадка верна. В журнальной статье в две колонки, в форме с боковой панелью или в таблице, чьи ячейки выдавались колонка за колонкой, она неверна так, что трудно заметить и дорого обнаружить ниже по течению. HotPDF отвечает на это методом ExtractLoadedPageStructureText, который игнорирует геометрию полностью: он обходит дерево структуры документа в авторском порядке, определённом в ISO 32000-1 §14.8.4, затем пересобирает глифы страницы по их идентификатору помеченного содержимого. Для тегированного PDF это не эвристика — это порядок, который заявляло порождающее приложение

Функция возвращает False, когда у страницы нет пригодного дерева структуры, — это сигнал откатиться к геометрическому экстрактору, а не отказать. Двухпутевой дизайн важнее алгоритма: реальный приём документов видит тегированные государственные формы и вывод сканера в одной папке, а конвейер, обрабатывающий лишь одно из двух, — не конвейер

Почему геометрическое извлечение ошибается в порядке чтения?

Потому что поток содержимого PDF вообще не несёт порядка чтения. Это последовательность операторов рисования, и производитель волен выдавать их в любой последовательности, какая устраивает его собственный движок вёрстки. Текстовые процессоры обычно выдают в порядке потока, и геометрическая сортировка выглядит хорошо. Инструменты вёрстки, конструкторы форм и генераторы отчётов часто нет: нижний колонтитул может быть выдан до тела, таблица может заполняться по столбцам, а двухколоночная страница может переплетать строки обеих колонок, потому что сборщик разрешал их вместе

Сравнение двухколоночной страницы PDF: геометрическое извлечение с сортировкой по базовой линии сшивает колонки против извлечения в порядке структуры по MCID в HotPDF
Сортировка глифов по базовой линии переплетает две колонки в бессмыслицу, тогда как дерево структуры воспроизводит порядок, заявленный производителем

Режим отказа тихий. Геометрический экстрактор никогда не сообщает об ошибке — он просто возвращает прозу, чьи предложения сшиты из двух колонок. Всё, что потребляет этот текст, — поисковый индекс, отображатель полей электронных счетов, конвейер поиска, питающий языковую модель, — наследует ущерб без предупреждения. HotPDF поставляет и геометрические экстракторы для загруженных документов, и для нетегированных файлов они остаются правильным инструментом; смысл пути порядка структуры — перестать угадывать, когда документ уже несёт ответ

Что дерево структуры на самом деле хранит

Тегированный PDF хранит второе, параллельное описание страницы. Каталог указывает на /StructTreeRoot, чьи дети /K образуют дерево элементов структуры: /Document, /Sect, /P, /Table, /TR, /TD и так далее. Листья этого дерева — ссылки на помеченное содержимое, целые числа, называющие участок потока содержимого страницы. На стороне содержимого эти участки открываются оператором BDC, несущим /MCID, и закрываются EMC. Каждый элемент структуры несёт также запись /Pg, называющую страницу, к которой он принадлежит, — именно это делает возможным постраничный обход в документе, чьё дерево структуры тянется на сотни страниц

Анатомия дерева структуры PDF: элементы StructTreeRoot вроде Sect, Table, TR и TD связываются с участками BDC MCID в потоке содержимого страницы HotPDF
Листья дерева — ссылки на помеченное содержимое, и каждый элемент несёт запись Pg, позволяющую обходу фильтровать до текущей страницы

HotPDF обходит это дерево с пределом глубины в 128 уровней и фильтрует по /Pg, так что вклад даёт только текущая страница. Выход обхода — не текст, а упорядоченный список значений MCID: авторский порядок участков помеченного содержимого на этой странице. Пересборка текста затем сводится к воспроизведению глифов в этом порядке

MCID записывается при извлечении глифов, а не ищется потом

Это деталь реализации, делающая возможность дешёвой. HotPDF уже записывает активный идентификатор помеченного содержимого на каждом извлекаемом глифе, в поле MCID структуры THPDFGlyphRecord, ведь интерпретатор потока содержимого знает, какая область BDC открыта в момент обработки каждого оператора Tj или TJ. Извлечение в порядке структуры поэтому не нуждается во втором проходе по потоку содержимого. Оно собирает последовательность MCID из дерева структуры, затем раскладывает уже извлечённые глифы по корзинам MCID и выдаёт их в этой последовательности

var
  Pdf: THotPDF;
  PageCount, I, Untagged: Integer;
  PageText, AllText: UnicodeString;
  Report: TStrings;   // принадлежащий вызывающему приёмник диагностики
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('accessible-form.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
    begin
      if Pdf.ExtractLoadedPageStructureText(I, PageText, Untagged) then
      begin
        // Авторский порядок прямо из дерева структуры
        if Untagged > 0 then
          Report.Add(Format('page %d: %d glyphs outside the structure tree',
            [I, Untagged]));
      end
      else
        // На этой странице нет пригодного дерева структуры: геометрический откат
        Pdf.ExtractLoadedPageText(I, PageText);
      AllText := AllText + PageText + #13#10;
    end;
  finally
    Pdf.Free;
  end;
end;

Нетегированные глифы считаются, но никогда не отбрасываются молча

Страница может быть тегирована частично. Производители добавляют декоративную линейку, номер страницы или поздний водяной знак вне любой области BDC, и эти глифы не принадлежат никакому MCID. Отбросить их — аккуратная реализация и неверная: тот же провал появляется, когда производитель тегирует тело, но забывает таблицу, и вы потеряли бы таблицу, не заметив

HotPDF дописывает непретендованные глифы геометрическим хвостом после текста в порядке структуры и сообщает их число через выходной параметр UntaggedGlyphCount. Это число — сигнал качества, на который можно реагировать. Горстка глифов на странице в две тысячи — мебель страницы, её можно игнорировать. Сорок процентов страницы вне дерева структуры означают, что тегирование декоративно, и геометрический экстрактор — более честный ответ для этого файла

Схема решений извлечения структурного текста HotPDF с геометрическим откатом, когда у страницы нет пригодного дерева структуры или тегирование декоративно
True означает порядок структуры с дописанным нетегированным хвостом, а False ведёт страницу к геометрическому экстрактору вместо отказа
function ExtractPageBestEffort(Pdf: THotPDF; PageIndex: Integer;
  out AText: UnicodeString; out UsedStructure: Boolean): Boolean;
var
  Untagged, TotalGlyphs: Integer;
  Glyphs: THPDFGlyphArray;
begin
  UsedStructure := False;
  if Pdf.ExtractLoadedPageStructureText(PageIndex, AText, Untagged) then
  begin
    TotalGlyphs := 0;
    if Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
      TotalGlyphs := Length(Glyphs);
    // Доверяйте дереву структуры, только когда оно покрывает большую часть страницы
    if (TotalGlyphs = 0) or (Untagged * 4 <= TotalGlyphs) then
    begin
      UsedStructure := True;
      Result := True;
      Exit;
    end;
  end;
  Result := Pdf.ExtractLoadedPageText(PageIndex, AText);
end;

Что заставляет функцию вернуть False

Три случая, и их стоит различать, потому что лишь один из них — дефект документа. Первый — обычный нетегированный PDF: нет /StructTreeRoot, нечего обходить, и False — просто правда. Второй — отсканированная страница, чей текст происходит из никогда не тегированного слоя OCR. Третий — интересный: содержимое несёт операторы BDC со значениями /MCID, но у страницы нет записи /StructParents, а дерево структуры никогда не ссылается на эти идентификаторы. Помеченное содержимое существует, сторона структуры — нет, и порядка, который можно восстановить, нет. HotPDF сообщает False, а не выдумывает его

Последний случай встречается в файлах, отредактированных вручную, и в выводе инструментов, выдающих помеченное содержимое для целей необязательного содержимого или артефактов без построения дерева структуры. Если вы сами производите тегированные PDF, та же асимметрия — то, что проверяет валидация PDF/UA, а противоположность на стороне писателя рассмотрена в DOM вёрстки, выдающей тегированный постраничный вывод

Где порядок структуры окупается

Аудит доступности — очевидный случай: если вы сертифицируете документ по PDF/UA, порядок чтения, который объявит экранный диктор, — это в точности порядок структуры, поэтому извлечь его — способ рецензировать его без экранного диктора. Захват данных — более крупный коммерческий случай. Тегированные государственные формы, регламентированные раскрытия и вложения электронных счетов несут метки полей и значения в заявленном порядке, и чтение их в этом порядке устраняет целый класс ошибок сопоставления, которые геометрическое извлечение создаёт на многоколоночных макетах

Новейший потребитель — поиск для языковых моделей. Нарезка документа на куски для вложений хороша ровно настолько, насколько хорош порядок текста, а кусок, сшивающий две колонки, порождает предложения, которых никогда не существовало. Извлечение в порядке структуры — доступное дешёвое исправление: для тегированных документов правильный порядок уже в файле, и его нужно лишь прочитать

HotPDF — нативный VCL-компонент для Delphi и C++Builder, поэтому обход дерева структуры и воспроизведение глифов оба идут в процессе над загруженным документом без внешнего рендерера. Полные детали API семейства извлечения загруженных документов — на странице продукта HotPDF Delphi PDF component