Tehnički članak

Dodavanje pretraživog tekstualnog sloja skeniranim PDF-ovima u Delphiju

PDFium Component dodaje pretraživ tekstualni sloj skeniranim PDF stranicama iz Delphija kroz ApplyOcrSearchLayer. Renderuje svaku izabranu stranicu, predaje piksele OCR provajderu koji sami dostavite, i upisuje prepoznate reči nazad kao nevidljive tekstualne objekte pozicionirane preko reči na skenu. Originalna slika stranice se nikada ne dekodira, ponovo kodira ili zamenjuje, tako da je vizuelni rezultat bajt po bajt ista stranica od koje ste krenuli

Motor za prepoznavanje namerno nije deo biblioteke. PDFium izlaže renderovanje stranice, mapiranje koordinata, učitavanje fontova, kreiranje tekstualnih objekata i nevidljive režime renderovanja, ali ne sadrži OCR motor, a pretvaranje da sadrži značilo bi paketovanje nečijeg proizvoda za prepoznavanje u PDF komponentu. Umesto toga, prepoznavanje živi iza interfejsa IPdfOcrProvider: biblioteka prosleđuje piksele fiksnog rasporeda, sa poreklom u gornjem levom uglu, u BGRA formatu, a provajder vraća Unicode tekst, vrednosti pouzdanosti i četvorouglove reči

Šta je tačno pretraživ tekstualni sloj?

Skeniran PDF je slika dokumenta. Sadržaj stranice je jedna velika slika, i nema ničega za biranje, pretragu, kopiranje ili indeksiranje. Pretraživ tekstualni sloj dodaje prave tekstualne objekte preko te slike sa režimom renderovanja postavljenim na nevidljiv, tako da pregledači ne crtaju ništa, ali selekcija, pretraga i izvlačenje pronalaze reči tačno tamo gde se pojavljuju

Pozicioniranje je cela igra. Ako nevidljiv tekst sedi nekoliko tačaka van mesta, isticanja selekcije padaju pored reči umesto na njih, a kopiranje pasusa proizvodi tekst u pogrešnom redosledu. Zato geometrija mora doći iz istih transformacija koje PDFium koristi za renderovanje stranice, a ne iz proporcionalne procene

Implementacija provajdera

Ugovor provajdera je jedna metoda. Prima zapis slike stranice koji nosi dimenzije, korak, DPI, format piksela i same bajtove piksela, plus token otkazivanja, i vraća reči ili poruku greške:

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 nosi redove sa poreklom u gornjem levom uglu, u BGRA
  // formatu, dužine Image.Stride bajtova. Predajte ih vašem motoru,
  // zatim popunite po jedan unos za svaku prepoznatu reč
  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;

Četvorouglovi umesto pravougaonika, jer sken retko stoji pravougaono prema stranici. Reč na blago rotiranoj stranici zauzima paralelogram, a TPdfOcrQuad nosi četiri ugaone tačke tako da iskošene i rotirane reči zadrže tačan region selekcije. Motori koji prijavljuju samo pravougaonike poravnate sa osama mogu koristiti FromRectangle, koji gradi degenerisan četvorougao

Zašto pozicije reči ne mogu biti proporcionalno skalirane?

Primamljivo je pretvoriti koordinatu piksela u koordinatu stranice deljenjem sa širinom renderovanja i množenjem sa širinom stranice. To radi samo za stranice bez rotacije, sa CropBox identičnim MediaBox-u, i poreklom na nuli, a mnogo skeniranih dokumenata ne ispunjava barem jedan od tih uslova

PDFium Component mapira svaki od četiri ugla četvorougla pojedinačno kroz FPDF_DeviceToPage, istu mapu koju je renderer koristio za proizvodnju piksela, tako da se unosi /Rotate i pomerene granice isecanja obrađuju konstrukcijom. Afina matrica za tekstualni objekat se zatim gradi iz tri od mapiranih tačaka, donjeg levog, donjeg desnog i gornjeg levog ugla, što je tačno dovoljno da izrazi poziciju, skaliranje, rotaciju i smicanje

Sam tekstualni objekat se kreira sa jediničnom veličinom fonta tako da se njegove stvarne granice fonta mogu izmeriti, a izmerene granice objekta se zatim mapiraju na ciljni četvorougao. Određivanje veličine pogodenom tačkastom veličinom i nadanjem da se poklapa sa skeniranom rečju bi lutalo sa svakom zamenom fonta; merenje unapred čini uklapanje nezavisnim od toga koji font sloj koristi

Pokretanje preko dokumenta

Zapis opcija kontroliše rezoluciju, filtriranje i svaki budžet. Filtriranje pouzdanosti je bitnije nego što izgleda: reči sa niskom pouzdanošću trajno zagađuju rezultate pretrage, a za razliku od pogrešnog renderovanja, niko to ne primeti dok pretraga ne vrati besmislice:

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;                  // rezolucija prepoznavanja
    Options.MinConfidence := 0.60;       // odbaci nesigurne reči
    Options.SkipPagesWithText := True;   // ostavi rođeno-digitalne stranice na miru
    Options.ContinueOnError := True;     // jedna loša stranica ne sme zaustaviti posao
    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 zaslužuje naglasak u mešovitim arhivama. PDF koji već nosi pravi tekst, bilo rođen digitalno ili prethodno obrađen, dobija drugi tekstualni sloj ako pokrenete OCR preko njega naslepo, a duplikat čini da izvlačenje vraća svaku reč dvaput. Status po stranici popsSkippedExistingText vam govori tačno koje su stranice ostavljene na miru

Budžeti, otkazivanje i zadržavanje otkaza

Svaka veličina koju neprijateljski ili tek ogroman dokument može naduvati ima plafon: piksele po stranici i ukupno, reči po stranici i ukupno, i karaktere po reči. Sve se proveravaju pre nego što se stranica upiše, ne posle, a procena piksela se izračunava iz dimenzija stranice i DPI pre nego što se ijedna bitmapa dodeli. Podizanje DPI sa 150 na 300 učetvorostručuje memoriju po stranici, pa je plafon po stranici parametar koji treba prvi podesiti kada serijski posao počne da otkazuje na velikim formatima

Token otkazivanja se provlači kroz celu putanju: progresivno renderovanje, poziv provajdera i petlju umetanja po reči. To znači da korisnik koji otkaže tokom prepoznavanja fajla od 400 stranica stane unutar jedne stranice umesto na kraju dokumenta, a isti obrazac tokena koji se koristi drugde u komponenti, opisan u otkazivom progresivnom renderovanju, primenjuje se ovde nepromenjen

Zadržavanje otkaza je po stranici. Biblioteka prikuplja ručke objekata koje je umetnula na stranicu i poziva FPDFPage_GenerateContent jednom, nakon što su sve reči postavljene. Ako bilo šta ne uspe usput, bilo greška provajdera ili problem fonta, objekti umetnuti na tu stranicu se uklanjaju unazad, a sadržaj stranice se ponovo generiše, tako da neuspela stranica vrati svoje originalno stanje umesto da zadrži polovinu tekstualnog sloja. Petlja dokumenta zatim nastavlja ili se zaustavlja prema ContinueOnError, a aktivna stranica se uvek vraća

Provera da li je slika zaista ostala netaknuta

Najjača dostupna provera je i najjednostavnija: renderujte stranicu pre i posle primene sloja na istoj veličini i uporedite bitmape. Trebalo bi da su identične bajt po bajt, jer nevidljiv tekst ne crta ništa, a tok slike nikada nije dekodiran. Bilo kakva razlika znači da je nešto drugo osim tekstualnog sloja izmenilo stranicu

Nakon toga, proverite stranu teksta izvlačenjem iz obrađenog fajla i potvrđivanjem da pozicije reči padaju na sken. Putanja izvlačenja je ista ona opisana u izvlačenju teksta iz PDF dokumenata, a za brzu vizuelnu proveru poravnanja, renderovanje stranica u slike kao u konverziji PDF stranica u JPEG vam omogućava da preklopite okvire reči preko skena

OCR slojevi, renderovanje, izvlačenje i uređivanje svi se izvode nad istim objektom dokumenta u Delphiju, C++Builderu i Lazarusu; kompletna API površina je opisana na stranici PDFium Component za Delphi