Odborný článok

Pridanie prehľadávateľnej textovej vrstvy k naskenovaným PDF v Delphi

PDFium Component pridáva z Delphi prehľadávateľnú textovú vrstvu k naskenovaným stránkam PDF prostredníctvom ApplyOcrSearchLayer. Vykreslí každú vybranú stránku, odovzdá pixely poskytovateľovi OCR, ktorý dodáte vy, a rozpoznané slová zapíše späť ako neviditeľné textové objekty umiestnené nad slovami v skene. Pôvodný obrázok stránky sa nikdy nedekóduje, znovu nekóduje ani nenahrádza, takže vizuálny výsledok je bajt po bajte tou istou stránkou, s ktorou ste začali

Rozpoznávací engine zámerne nie je súčasťou knižnice. PDFium sprístupňuje vykresľovanie stránok, mapovanie súradníc, načítanie písiem, tvorbu textových objektov a neviditeľné režimy vykresľovania, no neobsahuje žiadny engine OCR, a predstierať opak by znamenalo pribaliť do PDF komponentu cudzí rozpoznávací produkt. Namiesto toho rozpoznávanie žije za rozhraním IPdfOcrProvider: knižnica odovzdáva pixely BGRA s pevným rozložením a počiatkom hore a poskytovateľ vracia unicode text, hodnoty spoľahlivosti a štvoruholníky slov

Čo presne je prehľadávateľná textová vrstva?

Naskenované PDF je obrázok dokumentu. Obsah stránky je jeden veľký obrázok a nie je v ňom čo vyberať, vyhľadávať, kopírovať ani indexovať. Prehľadávateľná textová vrstva pridá nad tento obrázok skutočné textové objekty s režimom vykresľovania nastaveným na neviditeľný, takže prehliadače nič nevykreslia, no výber, vyhľadávanie a extrakcia nájdu slová presne tam, kde sa objavujú

Polohovanie je celá hra. Ak neviditeľný text sedí o pár bodov mimo, zvýraznenie výberu pristane vedľa slov namiesto na nich, a kopírovanie odseku produkuje text v nesprávnom poradí. Preto musí geometria pochádzať z tých istých transformácií, ktoré PDFium používa na vykreslenie stránky, nie z proporčného odhadu

Implementácia poskytovateľa

Zmluva poskytovateľa je jedna metóda. Prijíma záznam obrázka stránky nesúci rozmery, stride, DPI, formát pixelov a samotné bajty pixelov, plus token na zrušenie, a vracia slová alebo chybovú správu:

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 obsahuje riadky BGRA s počiatkom hore a dĺžkou Image.Stride bajtov.
  // Odovzdajte ich svojmu enginu a potom vyplňte jeden záznam na každé rozpoznané slovo
  SetLength(Words, RecognisedCount);
  for I := 0 to RecognisedCount - 1 do
  begin
    Words[I].Text := EngineWordText(I);
    Words[I].Confidence := EngineWordConfidence(I);   // rozsah 0..1
    Words[I].Quad := TPdfOcrQuad.FromRectangle(
      EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
  end;
  ErrorMessage := '';
  Result := True;
end;

Štvoruholníky namiesto obdĺžnikov, pretože sken je len zriedka presne zarovnaný so stránkou. Slovo na mierne pootočenej stránke zaberá rovnobežník, a TPdfOcrQuad nesie štyri rohové body, aby si skosené a otočené slová zachovali presnú oblasť výberu. Enginy, ktoré hlásia iba obdĺžniky zarovnané na osi, môžu použiť FromRectangle, ktorá zostaví degenerovaný štvoruholník

Prečo sa pozície slov nedajú škálovať proporčne?

Je lákavé previesť súradnicu pixela na súradnicu stránky delením šírkou vykreslenia a násobením šírkou stránky. To funguje iba pri stránkach bez otočenia, s CropBox identickým s MediaBox a počiatkom na nule, a mnoho naskenovaných dokumentov nespĺňa aspoň jednu z týchto podmienok

PDFium Component mapuje každý zo štyroch rohov štvoruholníka jednotlivo cez FPDF_DeviceToPage, teda rovnaké mapovanie, aké renderer použil na vytvorenie pixelov, takže záznamy /Rotate a posunuté orezávacie rámce sú ošetrené už konštrukciou. Afinná matica pre textový objekt sa potom zostaví z troch namapovaných bodov — ľavého dolného, pravého dolného a ľavého horného rohu —, čo presne stačí na vyjadrenie polohy, mierky, otočenia a skosenia

Samotný textový objekt sa vytvorí s jednotkovou veľkosťou písma, aby sa dali zmerať jeho skutočné hranice písma, a namerané hranice objektu sa potom namapujú na cieľový štvoruholník. Nastavenie veľkosti podľa odhadnutého bodového rozmeru s nádejou, že sa zhodne s naskenovaným slovom, by sa pri každej náhrade písma posúvalo; meranie vopred robí prispôsobenie nezávislým od toho, ktoré písmo vrstva používa

Spustenie nad dokumentom

Záznam možností riadi rozlíšenie, filtrovanie a každý rozpočet. Na filtrovaní podľa spoľahlivosti záleží viac, než sa zdá: nezmyselné slová s nízkou spoľahlivosťou natrvalo znečisťujú výsledky vyhľadávania, a na rozdiel od chybného vykreslenia si to nikto nevšimne, kým vyhľadávanie nevráti nezmysel:

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;                  // rozlíšenie rozpoznávania
    Options.MinConfidence := 0.60;       // zahodiť neisté slová
    Options.SkipPagesWithText := True;   // nechať natívne digitálne stránky bez zmeny
    Options.ContinueOnError := True;     // jedna chybná stránka nesmie zastaviť úlohu
    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 si zaslúži zdôraznenie v zmiešaných archívoch. PDF, ktoré už nesie skutočný text, či už natívne digitálny alebo predtým spracovaný, získa druhú textovú vrstvu, ak nad ním OCR spustíte naslepo, a duplicita spôsobí, že extrakcia vráti každé slovo dvakrát. Stav na úrovni stránky popsSkippedExistingText vám presne povie, ktoré stránky zostali bez zmeny

Rozpočty, zrušenie a obmedzenie dosahu zlyhaní

Každá veličina, ktorú môže nahustiť nepriateľský alebo jednoducho obrovský dokument, má svoj strop: pixely na stránku a celkovo, slová na stránku a celkovo, a znaky na slovo. Všetky sa kontrolujú pred zápisom stránky, nie po ňom, a odhad pixelov sa počíta z rozmerov stránky a DPI ešte pred alokáciou akejkoľvek bitmapy. Zvýšenie DPI zo 150 na 300 zoštvornásobí pamäť na stránku, takže strop na stránku je parameter, ktorý treba ladiť ako prvý, keď dávková úloha začne zlyhávať na veľkých formátoch

Token na zrušenie prechádza celou cestou: postupným vykresľovaním, volaním poskytovateľa aj slučkou vkladania jednotlivých slov. To znamená, že používateľ, ktorý zruší operáciu počas rozpoznávania 400-stranového súboru, sa zastaví do jednej stránky namiesto na konci dokumentu, a rovnaký vzor tokenu použitý inde v komponente, opísaný v časti zrušiteľné postupné vykresľovanie, sa tu uplatňuje bez zmeny

Obmedzenie dosahu zlyhaní funguje na úrovni stránky. Knižnica zhromažďuje handle objektov, ktoré na stránku vložila, a po umiestnení všetkých slov raz zavolá FPDFPage_GenerateContent. Ak čokoľvek zlyhá uprostred, či už ide o chybu poskytovateľa alebo problém s písmom, objekty vložené na tejto stránke sa odstránia v opačnom poradí a obsah stránky sa znovu vygeneruje, takže zlyhaná stránka sa vráti do pôvodného stavu namiesto toho, aby si ponechala polovičnú textovú vrstvu. Slučka cez dokument potom pokračuje alebo sa zastaví podľa ContinueOnError, a aktívna stránka sa vždy obnoví

Overenie, že sa obrázka naozaj nikto nedotkol

Najsilnejšia dostupná kontrola je zároveň najjednoduchšia: vykreslite stránku pred a po aplikácii vrstvy v rovnakej veľkosti a porovnajte bitmapy. Mali by byť identické bajt po bajte, pretože neviditeľný text nič nevykresľuje a stream obrázka sa nikdy nedekódoval. Akýkoľvek rozdiel znamená, že stránku zmenilo niečo iné než textová vrstva

Potom overte textovú stránku extrakciou zo spracovaného súboru a potvrdením, že pozície slov pristanú na skene. Cesta extrakcie je tá istá, akú opisuje časť extrakcia textu z dokumentov PDF, a pre rýchlu vizuálnu kontrolu zarovnania vám vykresľovanie stránok do obrázkov, ako v časti konverzia stránok PDF do JPEG, umožní prekryť rámiky slov na sken

Vrstvenie OCR, vykresľovanie, extrakcia a úpravy bežia nad tým istým objektom dokumentu v Delphi, C++Builder a Lazarus; kompletné rozhranie API opisuje stránka PDFium Component pre Delphi