PDFium Component добавляет слой текста для поиска на сканированные страницы PDF из Delphi через ApplyOcrSearchLayer. Он рендерит каждую выбранную страницу, передаёт пиксели поставщику OCR, который вы предоставляете, и записывает распознанные слова обратно как невидимые текстовые объекты, размещённые поверх слов на скане. Исходное изображение страницы никогда не декодируется, не перекодируется и не заменяется, поэтому визуальный результат побайтово совпадает со страницей, с которой вы начали
Движок распознавания намеренно не входит в состав библиотеки. PDFium предоставляет рендеринг страниц, отображение координат, загрузку шрифтов, создание текстовых объектов и невидимые режимы рендеринга, но не содержит движка OCR, и притворяться иначе означало бы включить чей-то продукт распознавания в PDF-компонент. Вместо этого распознавание живёт за интерфейсом IPdfOcrProvider: библиотека передаёт пиксели с фиксированной раскладкой, верхним началом координат и форматом BGRA, а поставщик возвращает текст в Юникоде, значения достоверности и четырёхугольники слов
Что именно представляет собой слой текста для поиска?
Сканированный PDF — это картинка документа. Содержимое страницы — одно большое изображение, и в нём нечего выделять, искать, копировать или индексировать. Слой текста для поиска добавляет настоящие текстовые объекты поверх этого изображения с режимом рендеринга, установленным на невидимый, поэтому просмотрщики ничего не рисуют, но выделение, поиск и извлечение находят слова именно там, где они находятся
Позиционирование — вся суть игры. Если невидимый текст смещён на пару пунктов, подсветка выделения ложится рядом со словами, а не на них, а копирование абзаца даёт текст в неверном порядке. Именно поэтому геометрия должна происходить из тех же преобразований, что PDFium использует для рендеринга страницы, а не из пропорционального предположения
Реализация поставщика
Контракт поставщика — один метод. Он получает запись изображения страницы, несущую размеры, шаг строки, DPI, формат пикселей и сами байты пикселей, плюс токен отмены, и возвращает слова или сообщение об ошибке:
uses
PDFium;
type
TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
public
function RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
end;
function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
I: Integer;
begin
// Image.Pixels содержит строки BGRA с верхним началом координат
// и шагом Image.Stride байт. Передайте их своему движку, затем
// заполните по одной записи на каждое распознанное слово
SetLength(Words, RecognisedCount);
for I := 0 to RecognisedCount - 1 do
begin
Words[I].Text := EngineWordText(I);
Words[I].Confidence := EngineWordConfidence(I); // 0..1
Words[I].Quad := TPdfOcrQuad.FromRectangle(
EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
end;
ErrorMessage := '';
Result := True;
end;
Четырёхугольники, а не прямоугольники, потому что скан редко бывает выровнен строго по странице. Слово на слегка повёрнутой странице занимает параллелограмм, и TPdfOcrQuad несёт четыре угловые точки, чтобы наклонённые и повёрнутые слова сохраняли точную область выделения. Движки, сообщающие только прямоугольники, выровненные по осям, могут использовать FromRectangle, который строит вырожденный четырёхугольник
Почему позиции слов нельзя масштабировать пропорционально?
Соблазнительно преобразовать координату пикселя в координату страницы, разделив на ширину рендера и умножив на ширину страницы. Это работает только для страниц без поворота, с CropBox, идентичным MediaBox, и с началом координат в нуле, а множество сканированных документов не удовлетворяют хотя бы одному из этих условий
PDFium Component отображает каждый из четырёх угловых точек четырёхугольника по отдельности через FPDF_DeviceToPage — то же отображение, что использовал рендерер для получения пикселей, — так что записи /Rotate и смещённые области обрезки обрабатываются автоматически. Аффинная матрица для текстового объекта затем строится из трёх отображённых точек — нижней левой, нижней правой и верхней левой углов, — чего в точности достаточно, чтобы выразить положение, масштаб, поворот и сдвиг
Сам текстовый объект создаётся с единичным размером шрифта, чтобы можно было измерить его реальные границы шрифта, а затем измеренные границы объекта отображаются на целевой четырёхугольник. Задание размера по угаданному размеру в пунктах в надежде, что он совпадёт со сканированным словом, дрейфовало бы при каждой замене шрифта; предварительное измерение делает подгонку независимой от того, какой шрифт использует слой
Обработка целого документа
Запись параметров управляет разрешением, фильтрацией и каждым бюджетом. Фильтрация по достоверности важнее, чем кажется: слова-мусор с низкой достоверностью навсегда засоряют результаты поиска, и, в отличие от неверного рендеринга, никто не замечает этого, пока поиск не вернёт бессмыслицу:
var
Pdf: TPdf;
Options: TPdfOcrOptions;
Report: TPdfOcrReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'scanned-contract.pdf';
Pdf.LoadDocument;
Options := TPdfOcrOptions.Default;
Options.Dpi := 300; // разрешение распознавания
Options.MinConfidence := 0.60; // отбросить неуверенные слова
Options.SkipPagesWithText := True; // не трогать изначально цифровые страницы
Options.ContinueOnError := True; // одна плохая страница не должна остановить задачу
Options.MaxPixelsPerPage := 40 * 1000 * 1000;
if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
Pdf.SaveAs('scanned-contract-searchable.pdf');
for I := 0 to High(Report.Pages) do
if Report.Pages[I].Status = popsFailed then
Writeln(Format('page %d failed: %s',
[Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
[Report.InsertedWordCount, Report.RejectedWordCount,
Report.SkippedPageCount]));
finally
Pdf.Free;
end;
end;
SkipPagesWithText заслуживает особого внимания в смешанных архивах. PDF, уже несущий настоящий текст, будь то изначально цифровой или ранее обработанный, при слепом прогоне OCR получает второй текстовый слой, и дубликат заставляет извлечение возвращать каждое слово дважды. Статус для каждой страницы popsSkippedExistingText точно сообщает, какие страницы были оставлены без изменений
Бюджеты, отмена и сдерживание сбоев
Каждая величина, которую враждебный или просто огромный документ способен раздуть, имеет потолок: пиксели на страницу и в сумме, слова на страницу и в сумме, символы на слово. Все они проверяются до записи страницы, а не после, а оценка пикселей вычисляется по размерам страницы и DPI ещё до выделения растрового изображения. Повышение DPI со 150 до 300 учетверяет память на страницу, поэтому потолок на страницу — первый параметр для настройки, когда пакетное задание начинает падать на больших форматах
Токен отмены проходит через весь путь: прогрессивный рендеринг, вызов поставщика и цикл вставки по словам. Это означает, что пользователь, отменивший операцию во время распознавания 400-страничного файла, останавливается в пределах одной страницы, а не в конце документа, и тот же паттерн токена, используемый в других местах компонента, описанный в статье отменяемый прогрессивный рендеринг, применяется здесь без изменений
Сдерживание сбоев — на уровне страницы. Библиотека собирает дескрипторы объектов, вставленных на странице, и вызывает FPDFPage_GenerateContent один раз, после размещения всех слов. Если что-то не удаётся на середине — будь то ошибка поставщика или проблема со шрифтом, — объекты, вставленные на этой странице, удаляются в обратном порядке, а содержимое страницы перегенерируется, поэтому сбойная страница возвращается в исходное состояние вместо того, чтобы сохранить наполовину готовый текстовый слой. Цикл по документу затем продолжается или останавливается согласно ContinueOnError, и активная страница всегда восстанавливается
Проверка того, что изображение действительно не тронуто
Самая надёжная проверка — она же самая простая: отрендерите страницу до и после применения слоя в одном размере и сравните растровые изображения. Они должны быть идентичны побайтово, поскольку невидимый текст ничего не рисует, а поток изображения ни разу не декодировался. Любое отличие означает, что страницу изменил не только текстовый слой
После этого проверьте текстовую сторону, извлекая содержимое из обработанного файла и убеждаясь, что позиции слов ложатся на скан. Путь извлечения тот же, что описан в статье извлечение текста из документов PDF, а для быстрой визуальной проверки выравнивания рендеринг страниц в изображения, как в статье конвертация страниц PDF в JPEG, позволяет наложить рамки слов на скан
Слои OCR, рендеринг, извлечение и редактирование работают с одним объектом документа в Delphi, C++Builder и Lazarus; полная поверхность API описана на странице компонента PDFium для Delphi