Кожен геометричний витягувач тексту вгадує. Він читає гліфи, які малює сторінка, сортує їх за базовою лінією та горизонтальною позицією і сподівається, що візуальне розташування збігається з порядком, у якому читала б людина. На одностовпчиковому звіті та вгадка права. На статті журналу у два стовпчики, на формі з бічною панеллю чи на таблиці, чиї клітинки виводилися стовпчик за стовпчиком, вона неправильна так, що це важко помітити і дорого виявити внизу за течією. 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