PDFium Component adaugă un strat de text căutabil la pagini PDF scanate din Delphi prin ApplyOcrSearchLayer. Randează fiecare pagină selectată, predă pixelii unui furnizor OCR pe care îl furnizezi tu, și scrie cuvintele recunoscute înapoi ca obiecte de text invizibile, poziționate peste cuvintele din scanare. Imaginea originală a paginii nu este niciodată decodată, recodificată sau înlocuită, așa că rezultatul vizual este identic octet cu octet cu pagina cu care ai început
Motorul de recunoaștere nu face parte deliberat din bibliotecă. PDFium expune randarea paginii, maparea coordonatelor, încărcarea fonturilor, crearea de obiecte de text și modurile de randare invizibile, dar nu conține niciun motor OCR, iar a pretinde altfel ar însemna împachetarea produsului de recunoaștere al altcuiva într-o componentă PDF. În schimb, recunoașterea trăiește în spatele interfeței IPdfOcrProvider: biblioteca transmite pixeli BGRA cu layout fix, origine sus, iar furnizorul returnează text Unicode, valori de încredere și patrulatere de cuvinte
Ce este mai exact un strat de text căutabil?
Un PDF scanat este o imagine a unui document. Conținutul paginii este o singură imagine mare, iar nu există nimic de selectat, căutat, copiat sau indexat. Un strat de text căutabil adaugă obiecte de text reale peste acea imagine, cu modul de randare setat la invizibil, astfel încât vizualizatorii nu desenează nimic, dar selecția, căutarea și extragerea găsesc cuvintele exact unde apar
Poziționarea este întregul joc. Dacă textul invizibil stă la câteva puncte distanță, evidențierile de selecție ajung lângă cuvinte, nu pe ele, iar copierea unui paragraf produce text în ordinea greșită. De aceea geometria trebuie să provină din aceleași transformări pe care PDFium le folosește pentru a randa pagina, nu dintr-o ghicire proporțională
Implementarea furnizorului
Contractul furnizorului este o singură metodă. Primește o înregistrare de imagine de pagină ce poartă dimensiuni, pas (stride), DPI, format de pixel și octeții de pixeli propriu-ziși, plus un token de anulare, și returnează cuvinte sau un mesaj de eroare:
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 conține rânduri BGRA cu origine sus, de Image.Stride octeți.
// Predă-le motorului tău, apoi completează câte o intrare per cuvânt recunoscut
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;
Patrulatere, nu dreptunghiuri, deoarece o scanare este rareori dreaptă (pătrată) față de pagină. Un cuvânt pe o pagină ușor rotită ocupă un paralelogram, iar TPdfOcrQuad poartă patru puncte de colț, astfel încât cuvintele înclinate și rotite păstrează o regiune de selecție corectă. Motoarele care raportează doar casete aliniate la axe pot folosi FromRectangle, care construiește patrulaterul degenerat
De ce nu pot fi scalate proporțional pozițiile cuvintelor?
Este tentant să convertești o coordonată de pixel într-o coordonată de pagină împărțind la lățimea de randare și înmulțind cu lățimea paginii. Asta funcționează doar pentru pagini fără rotație, cu un CropBox identic cu MediaBox și o origine la zero, iar destule documente scanate eșuează cel puțin una dintre acele condiții
PDFium Component mapează fiecare dintre cele patru colțuri ale patrulaterului individual prin FPDF_DeviceToPage, aceeași mapare pe care randorul a folosit-o pentru a produce pixelii, astfel încât intrările /Rotate și casetele de decupare cu offset sunt gestionate prin construcție. Matricea afină pentru obiectul de text este apoi construită din trei dintre punctele mapate, colțurile stânga-jos, dreapta-jos și stânga-sus, ceea ce e exact suficient pentru a exprima poziția, scala, rotația și forfecarea
Obiectul de text în sine este creat la dimensiune de font unitară, astfel încât limitele reale ale fontului să poată fi măsurate, iar limitele măsurate ale obiectului sunt apoi mapate pe patrulaterul țintă. Dimensionarea printr-o mărime de punct ghicită, sperând că se potrivește cu cuvântul scanat, ar deriva la fiecare substituire de font; măsurarea în avans face potrivirea independentă de fontul folosit de strat
Rularea peste un document
Înregistrarea de opțiuni controlează rezoluția, filtrarea și fiecare buget. Filtrarea după încredere contează mai mult decât pare: cuvintele gunoi cu încredere scăzută poluează permanent rezultatele de căutare, iar spre deosebire de o randare greșită, nimeni nu observă până când o căutare nu returnează prostii:
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; // rezoluție de recunoaștere
Options.MinConfidence := 0.60; // elimină cuvintele nesigure
Options.SkipPagesWithText := True; // lasă în pace paginile native digitale
Options.ContinueOnError := True; // o pagină proastă nu trebuie să oprească jobul
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 merită subliniat în arhive mixte. Un PDF care poartă deja text real, fie nativ digital, fie procesat anterior, primește un al doilea strat de text dacă rulezi OCR orbește peste el, iar duplicatul face ca extragerea să returneze fiecare cuvânt de două ori. Starea per pagină popsSkippedExistingText îți spune exact ce pagini au fost lăsate în pace
Bugete, anulare și izolarea eșecurilor
Fiecare cantitate pe care un document ostil sau pur și simplu enorm o poate umfla are un plafon: pixeli per pagină și în total, cuvinte per pagină și în total, și caractere per cuvânt. Toate sunt verificate înainte ca pagina să fie scrisă, nu după, iar estimarea de pixeli este calculată din dimensiunile paginii și DPI înainte ca vreun bitmap să fie alocat. Creșterea DPI de la 150 la 300 cvadruplează memoria per pagină, așa că plafonul per pagină este parametrul de ajustat primul când un job pe loturi începe să eșueze pe formate mari
Token-ul de anulare traversează întreaga cale: randarea progresivă, apelul furnizorului și bucla de inserare per cuvânt. Asta înseamnă că un utilizator care anulează în timpul recunoașterii unui fișier de 400 de pagini se oprește în interiorul unei singure pagini, nu la finalul documentului, iar același model de token folosit în alte locuri din componentă, descris în randarea progresivă anulabilă, se aplică aici neschimbat
Izolarea eșecurilor este per pagină. Biblioteca colectează handle-urile de obiect pe care le-a inserat pe o pagină și apelează FPDFPage_GenerateContent o singură dată, după ce toate cuvintele sunt plasate. Dacă ceva eșuează pe parcurs, fie o eroare de furnizor, fie o problemă de font, obiectele inserate pe acea pagină sunt eliminate în ordine inversă, iar conținutul paginii este regenerat, astfel încât o pagină eșuată revine la starea ei originală, în loc să păstreze jumătate de strat de text. Bucla de document apoi continuă sau se oprește conform ContinueOnError, iar pagina activă este întotdeauna restaurată
Verificarea că imaginea chiar a rămas neatinsă
Cea mai puternică verificare disponibilă este și cea mai simplă: randează pagina înainte și după aplicarea stratului, la aceeași dimensiune, și compară bitmap-urile. Ar trebui să fie identice octet cu octet, deoarece textul invizibil nu desenează nimic, iar fluxul de imagine nu a fost niciodată decodat. Orice diferență înseamnă că altceva decât stratul de text a schimbat pagina
După aceea, verifică partea de text extrăgând din fișierul procesat și confirmând că pozițiile cuvintelor cad pe scanare. Calea de extragere este aceeași descrisă în extragerea textului din documente PDF, iar pentru o verificare vizuală rapidă a alinierii, randarea paginilor ca imagini, ca în conversia paginilor PDF în JPEG, îți permite să suprapui casetele de cuvinte peste scanare
Stratificarea OCR, randarea, extragerea și editarea rulează toate pe același obiect document în Delphi, C++Builder și Lazarus; suprafața completă a API-ului este descrisă pe pagina componentei PDFium pentru Delphi