Каждый геометрический экстрактор текста угадывает. Он читает глифы, которые рисует страница, сортирует их по базовой линии и горизонтальной позиции и надеется, что визуальное расположение совпадает с порядком человеческого чтения. В одностролбцовом отчёте эта догадка верна. В журнальной статье в две колонки, в форме с боковой панелью или в таблице, чьи ячейки выдавались колонка за колонкой, она неверна так, что трудно заметить и дорого обнаружить ниже по течению. HotPDF отвечает на это методом ExtractLoadedPageStructureText, который игнорирует геометрию полностью: он обходит дерево структуры документа в авторском порядке, определённом в ISO 32000-1 §14.8.4, затем пересобирает глифы страницы по их идентификатору помеченного содержимого. Для тегированного PDF это не эвристика — это порядок, который заявляло порождающее приложение
Функция возвращает False, когда у страницы нет пригодного дерева структуры, — это сигнал откатиться к геометрическому экстрактору, а не отказать. Двухпутевой дизайн важнее алгоритма: реальный приём документов видит тегированные государственные формы и вывод сканера в одной папке, а конвейер, обрабатывающий лишь одно из двух, — не конвейер
Почему геометрическое извлечение ошибается в порядке чтения?
Потому что поток содержимого PDF вообще не несёт порядка чтения. Это последовательность операторов рисования, и производитель волен выдавать их в любой последовательности, какая устраивает его собственный движок вёрстки. Текстовые процессоры обычно выдают в порядке потока, и геометрическая сортировка выглядит хорошо. Инструменты вёрстки, конструкторы форм и генераторы отчётов часто нет: нижний колонтитул может быть выдан до тела, таблица может заполняться по столбцам, а двухколоночная страница может переплетать строки обеих колонок, потому что сборщик разрешал их вместе
Режим отказа тихий. Геометрический экстрактор никогда не сообщает об ошибке — он просто возвращает прозу, чьи предложения сшиты из двух колонок. Всё, что потребляет этот текст, — поисковый индекс, отображатель полей электронных счетов, конвейер поиска, питающий языковую модель, — наследует ущерб без предупреждения. HotPDF поставляет и геометрические экстракторы для загруженных документов, и для нетегированных файлов они остаются правильным инструментом; смысл пути порядка структуры — перестать угадывать, когда документ уже несёт ответ
Что дерево структуры на самом деле хранит
Тегированный PDF хранит второе, параллельное описание страницы. Каталог указывает на /StructTreeRoot, чьи дети /K образуют дерево элементов структуры: /Document, /Sect, /P, /Table, /TR, /TD и так далее. Листья этого дерева — ссылки на помеченное содержимое, целые числа, называющие участок потока содержимого страницы. На стороне содержимого эти участки открываются оператором BDC, несущим /MCID, и закрываются EMC. Каждый элемент структуры несёт также запись /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. Это число — сигнал качества, на который можно реагировать. Горстка глифов на странице в две тысячи — мебель страницы, её можно игнорировать. Сорок процентов страницы вне дерева структуры означают, что тегирование декоративно, и геометрический экстрактор — более честный ответ для этого файла
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