Articol tehnic

Adaugă un strat de text căutabil la PDF-uri scanate în Delphi

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