PDFiumPas повертає текст сторінки як структуру, а не як рядок. GetStructuredText створює TPdfStructuredTextPage, що містить блоки, кожен з яких утримує рядки, кожен з яких утримує стильові spans, з межами в просторі сторінки на кожному рівні та збереженими індексами вихідних символів, тож будь-який фрагмент можна зіставити назад із базовою текстовою сторінкою
Вилучення плоским рядком, з якого починається більшість коду, все ще присутнє і все ще коректне для свого призначення. Воно перестає бути достатнім у той момент, коли вам потрібно знати, які слова були заголовком, які належали лівій колонці, або де на сторінці насправді знаходиться збіг
Чому плоский рядок — неправильний результат для більшості завдань?
Тому що питання, які люди ставлять до вилученого тексту, майже ніколи не звучать як «які символи є на цій сторінці». Вони звучать як «який тут заголовок», «чи це таблиця», «чи належить цей абзац до розділу 4», «де намалювати виділення». Єдиний рядок не відповідає на жодне з них, і кожна відповідь, яку ви реконструюєте з нього, — це евристика, яка тепер належить вам
Двоколонкові макети роблять цю думку конкретною. Вилучіть двоколонкову статтю як рядок, і залежно від того, як виробник документа записав потік вмісту, ви можете отримати колонку один, за якою йде колонка два, або ж рядок перший колонки один, рядок перший колонки два, рядок другий колонки один — і так далі вниз по сторінці. Обидва варіанти можуть вийти з відповідного специфікації PDF. Жоден із них не є неправильним на рівні формату, бо PDF описує позначки на сторінці, а не структуру документа. Блокова модель дозволяє екстрактору явно приймати рішення про порядок і повідомляти, яке саме рішення він прийняв
Порядок вмісту чи фізичний макет?
TPdfStructuredTextOptions.ReadingOrder обирає між roContentOrder і roPhysicalLayout, і правильна відповідь залежить від того, чому ви довіряєте більше — виробнику документа чи геометрії
Порядок вмісту повертає текст у тій послідовності, в якій його малює потік вмісту. Це швидко, і для документів, згенерованих сумлінним виробником, зазвичай це і є задуманий порядок читання. Фізичний макет ігнорує послідовність потоку й реконструює порядок з того, де символи насправді розташовані, кластеризуючи їх у рядки, а потім у колонки. Саме це потрібно для сторінок, спершу сканованих, а потім розпізнаних OCR, для виводу з інструментів, що видають текст у порядку шрифту, а не в порядку читання, і для всього, де єдине, на що можна покластися, — це візуальний результат
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfStructuredTextOptions;
Page: TPdfStructuredTextPage;
B, L: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'article.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 1; // індексація з 1
Options := TPdfStructuredTextOptions.Default;
Options.ReadingOrder := roPhysicalLayout;
Options.IncludeFontInfo := True;
Options.IncludeSemantics := True;
Options.MaxCharacters := 200000; // fail-closed бюджет
Page := Pdf.GetStructuredText(Options);
for B := 0 to High(Page.Blocks) do
begin
if Page.Blocks[B].Kind = cfHeading then
Emit(Format('H%d: %s',
[Page.Blocks[B].HeadingLevel, Page.Blocks[B].Text]))
else
for L := 0 to High(Page.Blocks[B].Lines) do
Emit(Page.Blocks[B].Lines[L].Text);
end;
finally
Pdf.Free;
end;
end;
Що додає тегування, чого не може геометрія?
Намір. З увімкненим IncludeSemantics блоки з позначеного PDF несуть Kind, узятий з дерева структури, тож заголовок є заголовком, бо так сказав виробник документа, а не тому, що його шрифт був більшим за середній. Типи покривають форми, важливі для повторного використання: cfParagraph, cfHeading з HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure та резервний тип для непозначеного вмісту cfPlain
Поле Source фіксує, звідки взялася кожна класифікація, rosStructure для дерева структури й rosHeuristic для висновку за непрямими ознаками, — саме це поле варто логувати, коли ви вирішуєте, наскільки довіряти конвеєру вилучення на всьому наборі документів. Рисунки — особливий випадок, про який варто знати: для блоку cfFigure текст береться з альтернативного опису, а не з будь-яких гліфів, бо рисунок не має власних символів. Неспівпадаючий альтернативний текст усе одно представлений, а не відкидається, — саме це дозволяє аудиту доступності побачити, що опис існує, навіть коли ніщо на сторінці його не малює. Сама модель тегування розглянута в статті про перевірку дерева структури PDF/UA
Spans несуть стилізацію та походження
Кожен TPdfStructuredTextSpan утримує свій текст, свої межі в просторі сторінки, FontName, FontSize, FontWeight і Angle, а також SourceStartIndex і SourceCharacterCount. Spans розриваються там, де змінюється стилізація, тож речення з трьома жирними словами перетворюється на три spans, а відтворення виділення в HTML чи Markdown стає питанням читання властивостей, а не здогадок за назвами шрифтів
Саме два поля з індексами джерела перетворюють вилучення на функцію, а не просто на звіт. Вони вказують назад у послідовність символів сторінки, а це означає, що блок, знайдений під час пошуку, можна перетворити на геометрію вибору на рівні символів чи прямокутник виділення без другого, по-іншому впорядкованого проходу по тексту; механіка описана в статті про візуальне виділення рядків тексту з рамками символів. Поле Angle має більше значення, ніж здається: повернутий текст у штампі чи водяному знаку потрапляє в той самий координатний простір, що й основний текст, і конвеєр, що ігнорує кут, із задоволенням вклеїть діагональний напис «DRAFT» просто в середину абзацу
Бюджет і два лічильники якості
MaxCharacters — це fail-closed бюджет, а не налаштування обрізання: сторінка, що перевищує його, зупиняється, а не тихо повертає частину вмісту. На шляху приймання недовірених документів це саме та поведінка, яка потрібна, бо сторінка з мільйоном символів — це або машинно згенероване чудовисько, або спроба зробити ваш екстрактор найповільнішою частиною системи
Два лічильники на поверненій сторінці безпосередньо описують якість вилучення. UnmappedCharacterCount рахує символи без придатного зіставлення Unicode — класичний симптом підмножини шрифту, вбудованої без CMap /ToUnicode; такий текст відображається бездоганно, а вилучається як щось абсолютно некорисне. GeometryFailureCount рахує символи, чий обмежувальний прямокутник не вдалося визначити, що погіршує впорядкування за фізичним макетом. Логуйте обидва. Набір документів, де ці числа стабільно близькі до нуля, можна індексувати з упевненістю, а той, де це не так, повідомляє, що деякі виробники документів у вашому конвеєрі потребують уваги, перш ніж будь-який подальший результат стане надійним
var
Page: TPdfStructuredTextPage;
B, S, L: Integer;
Emphasised: Boolean;
begin
Page := Pdf.GetStructuredText(Options);
if Page.UnmappedCharacterCount > 0 then
Log(Format('page %d: %d characters without a Unicode mapping',
[Page.PageNumber, Page.UnmappedCharacterCount]));
if Page.GeometryFailureCount > 0 then
Log(Format('page %d: %d characters without geometry',
[Page.PageNumber, Page.GeometryFailureCount]));
for B := 0 to High(Page.Blocks) do
for L := 0 to High(Page.Blocks[B].Lines) do
for S := 0 to High(Page.Blocks[B].Lines[L].Spans) do
begin
Emphasised := Page.Blocks[B].Lines[L].Spans[S].FontWeight >= 600;
AppendRun(Page.Blocks[B].Lines[L].Spans[S].Text, Emphasised,
Page.Blocks[B].Lines[L].Spans[S].SourceStartIndex);
end;
end;
Продуктивність на реальних сторінках
Вилучення з фізичним макетом — це дорогий режим, і реалізація побудована для сторінок, які справді великі: впорядкування символів виконується за O(n log n) замість повторного сканування, буфери рядків і spans зростають геометрично замість перевиділення на кожен символ, текст Unicode будується в буферах замість конкатенації рядків, а пошуки шрифтів для сусідніх текстових об'єктів кешуються. Саме ця комбінація тримає щільну сторінку на 5000 символів передбачуваною, а не квадратичною
Для завдання з великою кількістю сторінок усе одно варто обирати дешевший режим там, де можна. Використовуйте roContentOrder із увімкненою семантикою для позначених документів, яким довіряєте, і залишайте roPhysicalLayout для сканованих і застарілих матеріалів, де геометрія — єдиний сигнал. Якщо все, що вам потрібно, — це звичайний рядок, простіший API, описаний у статті про вилучення тексту з документів PDF, залишається швидшим шляхом, а коли потрібно простежити текст назад до ідентифікаторів позначеного вмісту, стаття про читання та запис позначеного вмісту BDC і MCID охоплює цей шар
Блокова модель також чисто зіставляється з тим, чого хочуть конвеєри пошуку за вибіркою: заголовок з його абзацами — це фрагмент із назвою, а межі дозволяють цитаті вказувати на місце на сторінці, а не на документ загалом. PDFiumPas — це компонент для Delphi та Lazarus навколо рушія PDFium, задокументований із прикладами на сторінці PDFium Delphi component