Витяг тексту з PDF виглядає простою задачею, доки ви не натрапите на документ, де текстовий шар відсутній, пошкоджений або розбитий на десятки дрібних груп символів без жодного змістовного порядку. Компонент PDFium надає вам дві точки входу: масив Character[] для прямого доступу до кожного гліфа на сторінці за індексом, та ReadablePageContent для структурованого подання, що відновлює абзаци та заголовки з дерева тегів PDF або за допомогою евристичного аналізу. Жоден з них не є універсально правильним вибором, тому важливо розуміти, що саме надає кожен підхід
Відкриття документа та пастка тихого збою
TPdf відкриває файл шляхом встановлення FileName і перемикання Active := True. Важлива деталь: Active := True ніколи не викликає виключень. Якщо файл відсутній, захищений паролем або пошкоджений, PDFium перехоплює помилку внутрішньо, а Active просто залишається False. Це означає, що кожен цикл вилучення тексту повинен передбачати таку перевірку:
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage('Could not open PDF (damaged or wrong password)');
Exit;
end;
// extraction follows here
finally
Pdf.Active := False;
Pdf.Free;
end;
Для файлів, захищених паролем, потрібно встановити Pdf.Password := '...' перед викликом Active := True. Другого шансу немає: якщо Active не вдалося виконати, ви закриваєте і відкриваєте знову з правильним паролем
Посторінковий витяг за допомогою Character[]
Найнижчий рівень підходу передбачає обхід кожного символу на кожній сторінці. Встановіть Pdf.PageNumber, щоб завантажити текстовий шар для цієї сторінки, а потім переберіть CharacterCount записів за допомогою властивості Character[]. Варто перевіряти два прапорці на кожному записі: CharacterGenerated[i] позначає синтетичні гліфи, вставлені рендером (наприклад, м'які перенесення при розриві рядка), які не мають справжнього значення Unicode, а CharacterMapError[i] сигналізує, що PDFium не зміг зіставити гліф із кодовою точкою, що трапляється при кодуванні шрифтів, у яких відсутня таблиця ToUnicode
procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
Page, I: Integer;
Line: string;
Ch: WideChar;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Line := '';
for I := 0 to Pdf.CharacterCount - 1 do
begin
if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
Continue;
Ch := Pdf.Character[I];
if Ch = #13 then
Ch := #10; // normalize CR to LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
Результатом є плоский рядок кодових точок Unicode у тому порядку, в якому PDFium перелічує їх — це порядок, у якому вони з'являються в потоці вмісту, а не обов'язково порядок читання зліва направо. Для більшості документів на основі латиниці, створених стандартними офісними інструментами, це цілком підходить. Але для відсканованих PDF, розпізнаних (OCR) з незвичними послідовностями гліфів, або для тексту, що читається справа наліво, порядок може бути хибним. Саме тоді ReadablePageContent стає у нагоді
Структурований витяг за допомогою ReadablePageContent
ReadablePageContent працює на вищому рівні: він повертає запис TPdfReadableContent, чий масив Fragments містить фрагменти тегованого контенту, кожен з яких має Kind, що ідентифікує абзаци, заголовки, елементи списку, комірки таблиці тощо. Коли PDF містить дерево структури (перевірте Pdf.IsTagged), джерелом є rosStructure, і порядок читання є достовірним. Для нетегованих файлів PDFium повертається до rosHeuristic, який групує символи за їхніми обмежувальними рамками (bounding boxes) у правдоподібні одиниці для читання, але не може гарантувати точність
procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
Page: Integer;
Content: TPdfReadableContent;
Fragment: TPdfContentFragment;
begin
for Page := 1 to Pdf.PageCount do
begin
Content := Pdf.ReadablePageContent(Page);
for Fragment in Content.Fragments do
begin
case Fragment.Kind of
cfHeading : Output.Add('# ' + Fragment.Text);
cfParagraph : Output.Add(Fragment.Text);
cfListItem : Output.Add('- ' + Fragment.Text);
else
Output.Add(Fragment.Text);
end;
end;
end;
end;
Якщо Content.Source = rosHeuristic, а ваш результат виглядає спотвореним, скоріше за все, текстовий шар документа не створювався з урахуванням порядку читання. На цьому етапі єдиним надійним виправленням є повторний експорт із вихідної програми з належним тегуванням або запуск кроку постобробки, який сортує джерела символів спочатку за Y, а потім за X
Що дають CharacterOrigin та CharacterRectangle
Обидві властивості повертають позицію символу в просторі сторінки (в пунктах, початок координат у лівому нижньому куті, Y зростає вгору). CharacterOrigin[i] — це базова точка прив'язки гліфа; CharacterRectangle[i] — це повна обмежувальна рамка. Це будівельні блоки для всього, що виходить за межі простого тексту: визначення меж стовпців, групування символів у рядки шляхом порівняння координат Y у межах допуску, або створення карти для перевірки влучань (hit-test) для виділення тексту в переглядачі. Якщо вам потрібно дізнатися, який символ знаходиться під місцем кліку миші, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) виконує цей пошук напряму без необхідності перебирати всі рамки
Встановлення DLL
Компонент PDFium делегує весь синтаксичний аналіз PDF нативній DLL: або pdfium32.dll, або pdfium64.dll, залежно від вашої цільової платформи. З компонентом постачається скрипт CopyDlls.bat, який копіює потрібний файл до системного каталогу Windows. Запустити його від імені адміністратора один раз на машині розробника цілком достатньо; для розгортання ви копіюєте DLL разом із виконуваним файлом програми. Варіанти з підтримкою V8 (pdfium32v8.dll, pdfium64v8.dll) значно більші і потрібні лише в тому разі, якщо ваші PDF містять JavaScript, який повинен виконуватись. Для суто вилучення тексту найкращим вибором є стандартна збірка
Якщо DLL відсутня під час виконання, Active := True завершиться тихою помилкою, так само як і для відсутнього файлу, оскільки компонент перехоплює помилку завантаження внутрішньо. Завжди тестуйте на "чистій" машині перед випуском продукту
Використання FontSize[] разом із Character[] для аналізу макета
Крім простого тексту, API рівня символів надає FontSize[i], що повертає розмір у пунктах для кожного відрендереного гліфа. Разом із CharacterOrigin[i] та CharacterRectangle[i] це дозволяє відрізняти основний текст від заголовків без покладання на дерево структури. Група символів, де розмір шрифту стрибає вище певного порогу, майже напевно є заголовком у нетегованому документі. Такий самий метод застосовується для виявлення підписів (дрібний текст під обмежувальною рамкою зображення) або виносок (дрібний текст біля нижнього краю сторінки). Ніщо з цього не вимагає рендерингу; всі три властивості зчитуються напряму з текстового шару, який PDFium створює під час Active := True
Один нюанс: FontSize[i] відображає розмір після застосування поточної матриці трансформації (CTM) сторінки, тому в документі, де автор масштабував усю сторінку, розміри будуть відповідно змінені. Якщо ви порівнюєте розміри на різних сторінках із різними розмірами сторінки, нормалізуйте їх відносно висоти MediaBox кожної сторінки перед прийняттям порогових рішень
Запис результату у файл
TStringList у Delphi починаючи з версії XE бездоганно обробляє виведення в UTF-8. Встановіть WriteBOM := False, якщо вам потрібен файл без BOM (багато подальших споживачів мають проблеми з початковим BOM):
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
ExtractAllText(Pdf, Lines);
Lines.WriteBOM := False;
Lines.SaveToFile('output.txt', TEncoding.UTF8);
finally
Lines.Free;
end;
end;
Для дуже великих документів, де використання пам'яті є критичним, пишіть напряму в TStreamWriter за допомогою TEncoding.UTF8 всередині циклу сторінок замість попереднього накопичення всього у список
Показані тут API Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent та CharacterIndexAtPos є частиною компонента PDFium для Delphi та C++Builder