Извлечение текста из PDF выглядит простой задачей, пока вы не столкнетесь с документом, в котором текстовый слой отсутствует, поврежден или разбит на десятки мелких фрагментов без какого-либо логического порядка. PDFium Component предоставляет две точки входа: массив 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 осталось равным False, вам придется закрыть документ и открыть его снова уже с правильным паролем
Постраничное извлечение текста с помощью 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, распознанных с необычной последовательностью глифов, или для текстов с направлением письма справа налево порядок может быть неверным. В таких случаях более полезным оказывается метод ReadablePageContent
Структурированное извлечение с помощью ReadablePageContent
Метод ReadablePageContent работает на уровень выше: он возвращает запись TPdfReadableContent, массив Fragments которой содержит размеченные фрагменты содержимого. Каждый фрагмент имеет свойство Kind, идентифицирующее абзацы, заголовки, элементы списков, ячейки таблиц и т. д. Если PDF содержит дерево структуры (проверяется свойством Pdf.IsTagged), источником разметки служит rosStructure, и порядок чтения является достоверным. Для неразмеченных файлов PDFium откатывается к эвристическому анализу rosHeuristic, который группирует символы по их ограничивающим рамкам в логические блоки, но не гарантирует абсолютной точности
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 с учетом погрешности или построения карты попадания для выделения текста в просмотрщике. Если вам нужно узнать, какой символ находится под курсором мыши, метод CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) выполнит этот поиск напрямую без необходимости перебирать все прямоугольники вручную
Развертывание библиотек DLL
Компонент PDFium Component делегирует весь парсинг 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] это позволяет отличать основной текст от заголовков без использования дерева структуры. Последовательность символов, размер шрифта которых превышает определенный порог, почти наверняка является заголовком в неразмеченном документе. Тот же прием применим для поиска подписей к рисункам (мелкий текст под рамкой изображения) или сносок (мелкий текст внизу страницы). None of this requires rendering; all three properties read directly from the text layer that PDFium builds during Active := True
Один нюанс: FontSize[i] отражает размер после применения текущей матрицы преобразования страницы (CTM), поэтому в документе, где автор масштабировал всю страницу, размеры будут пропорционально скорректированы. Если вы сравниваете размеры шрифтов на страницах с разными физическими габаритами, нормализуйте значения относительно высоты MediaBox каждой страницы перед принятием пороговых решений
Запись результатов в файл
Начиная с версии XE, класс TStringList в Delphi корректно работает с кодировкой 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 внутри цикла по страницам, не накапливая весь объем текста в списке TStringList
Описанные здесь интерфейсы Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent и CharacterIndexAtPos входят в состав компонента PDFium Component для Delphi и C++Builder