PDFium Component добавя слой с текст за търсене към сканирани PDF страници от Delphi чрез ApplyOcrSearchLayer. Той рендира всяка избрана страница, подава пикселите на OCR доставчик, който вие предоставяте, и записва разпознатите думи обратно като невидими текстови обекти, позиционирани върху думите в сканирания образ. Оригиналното изображение на страницата никога не се декодира, прекодира или заменя, така че визуалният резултат е байт по байт същата страница, с която сте започнали
Двигателят за разпознаване съзнателно не е част от библиотеката. PDFium излага рендиране на страници, съпоставяне на координати, зареждане на шрифтове, създаване на текстови обекти и невидими режими на рендиране, но не съдържа никакъв OCR двигател, а да се преструва, че е иначе, би означавало да се пакетира нечий продукт за разпознаване в PDF компонент. Вместо това разпознаването живее зад интерфейса IPdfOcrProvider: библиотеката подава пиксели с фиксирано оформление, BGRA с горен произход, а доставчикът връща Unicode текст, стойности на увереност и четириъгълници на думи
Какво точно представлява слой с текст за търсене?
Сканиран PDF е снимка на документ. Съдържанието на страницата е едно голямо изображение, и няма нищо за избиране, търсене, копиране или индексиране. Слой с текст за търсене добавя истински текстови обекти върху това изображение с режим на рендиране, зададен на невидим, така че визуализаторите не изчертават нищо, но избирането, търсенето и извличането намират думите точно там, където се появяват
Позиционирането е цялата игра. Ако невидимият текст седи на няколко пункта встрани, маркировките от избиране кацат до думите, вместо върху тях, а копирането на параграф произвежда текст в грешен ред. Затова геометрията трябва да идва от същите трансформации, които PDFium използва, за да рендира страницата, а не от пропорционално предположение
Реализиране на доставчика
Договорът на доставчика е един метод. Той получава запис с изображение на страницата, носещ размери, стъпка (stride), 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 Component за Delphi