Technický článek

Přidání prohledávatelné textové vrstvy do naskenovaných PDF v Delphi

PDFium Component přidává prohledávatelnou textovou vrstvu do naskenovaných stránek PDF z Delphi přes ApplyOcrSearchLayer. Vykreslí každou vybranou stránku, předá pixely poskytovateli OCR, který dodáte vy, a rozpoznaná slova zapíše zpět jako neviditelné textové objekty umístěné nad slovy ve skenu. Původní obrázek stránky se nikdy nedekóduje, znovu nekóduje ani nenahrazuje, takže vizuální výsledek je bajt po bajtu stejný jako stránka, se kterou jste začali

Rozpoznávací motor záměrně není součástí knihovny. PDFium vystavuje vykreslování stránky, mapování souřadnic, načítání fontů, tvorbu textových objektů a neviditelné vykreslovací režimy, ale neobsahuje žádný motor OCR, a předstírat opak by znamenalo zabalit něčí rozpoznávací produkt do komponenty PDF. Rozpoznávání místo toho žije za rozhraním IPdfOcrProvider: knihovna předává pixely BGRA s pevným rozvržením a horním počátkem a poskytovatel vrací unicode text, hodnoty spolehlivosti a čtyřúhelníky slov

Co přesně je prohledávatelná textová vrstva?

Naskenované PDF je obrázek dokumentu. Obsah stránky je jeden velký obrázek a není co vybrat, hledat, kopírovat ani indexovat. Prohledávatelná textová vrstva přidá nad tento obrázek skutečné textové objekty s vykreslovacím režimem nastaveným na neviditelný, takže prohlížeče nekreslí nic, zatímco výběr, hledání a extrakce najdou slova přesně tam, kde se objevují

Celá hra spočívá v umístění. Pokud je neviditelný text posunutý o pár bodů, zvýraznění výběru dopadne vedle slov místo na ně a kopírování odstavce vytvoří text ve špatném pořadí. Proto geometrie musí vycházet ze stejných transformací, které PDFium používá k vykreslení stránky, ne z proporčního odhadu

Implementace poskytovatele

Smlouva poskytovatele je jedna metoda. Přijímá záznam obrázku stránky nesoucí rozměry, krok, DPI, formát pixelů a samotné bajty pixelů, plus token pro zrušení, a vrací slova nebo chybovou zprá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 řádky BGRA s horním počátkem o délce Image.Stride bajtů.
  // Předejte je svému motoru a poté vyplňte jednu položku na rozpoznané slovo
  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;

Čtyřúhelníky místo obdélníků, protože sken je vůči stránce jen zřídka pravoúhlý. Slovo na mírně natočené stránce zabírá rovnoběžník, a TPdfOcrQuad nese čtyři rohové body, takže zkosená a otočená slova mají přesnou oblast výběru. Motory, které hlásí pouze obdélníky zarovnané s osami, mohou použít FromRectangle, který sestaví degenerovaný čtyřúhelník

Proč nelze pozice slov škálovat proporčně?

Je lákavé převést souřadnici pixelu na souřadnici stránky vydělením šířkou vykreslení a vynásobením šířkou stránky. To funguje pouze u stránek bez otočení, s CropBox identickým s MediaBox a počátkem v nule, a řada naskenovaných dokumentů selže alespoň v jedné z těchto podmínek

PDFium Component mapuje každý ze čtyř rohů čtyřúhelníku samostatně přes FPDF_DeviceToPage, stejné mapování, jaké vykreslovač použil k vytvoření pixelů, takže položky /Rotate a posunuté ořezové rámečky jsou ošetřeny už konstrukcí. Afinní matice pro textový objekt se poté sestaví ze tří z namapovaných bodů, levého dolního, pravého dolního a levého horního rohu, což přesně stačí k vyjádření polohy, měřítka, otočení a zkosení

Samotný textový objekt se vytvoří v jednotkové velikosti fontu, aby bylo možné změřit jeho skutečné hranice fontu, a naměřené hranice objektu se poté namapují na cílový čtyřúhelník. Určování velikosti podle odhadované velikosti bodu v naději, že bude odpovídat naskenovanému slovu, by se s každou náhradou fontu rozcházelo; měření nejprve dělá přizpůsobení nezávislým na tom, jaký font vrstva používá

Spuštění nad dokumentem

Záznam možností řídí rozlišení, filtrování a každý rozpočet. Na filtrování spolehlivosti záleží víc, než se zdá: nesmyslná slova s nízkou spolehlivostí trvale znečišťují výsledky hledání, a na rozdíl od chybného vykreslení si toho nikdo nevšimne, dokud hledání nevrátí nesmysl:

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;                  // rozlišení rozpoznávání
    Options.MinConfidence := 0.60;       // zahodit nejistá slova
    Options.SkipPagesWithText := True;   // ponechat digitálně vzniklé stránky bez zásahu
    Options.ContinueOnError := True;     // jedna špatná stránka nesmí zastavit celou ú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 zaslouží důraz ve smíšených archivech. PDF, které už nese skutečný text, ať už digitálně vzniklý nebo dříve zpracovaný, dostane při slepém spuštění OCR druhou textovou vrstvu, a duplicita způsobí, že extrakce vrátí každé slovo dvakrát. Stav jednotlivé stránky popsSkippedExistingText vám přesně řekne, které stránky zůstaly nedotčené

Rozpočty, zrušení a omezení selhání

Každé množství, které může nepřátelský nebo prostě obrovský dokument nafouknout, má strop: pixely na stránku i celkem, slova na stránku i celkem a znaky na slovo. Všechny se kontrolují před zápisem stránky, ne po něm, a odhad pixelů se počítá z rozměrů stránky a DPI ještě předtím, než se alokuje jakákoli bitmapa. Zvýšení DPI ze 150 na 300 zečtyřnásobí paměť na stránku, takže strop na stránku je parametr, který ladit jako první, když dávková úloha začne selhávat na velkých formátech

Token zrušení prochází celou cestou: progresivním vykreslováním, voláním poskytovatele i smyčkou vkládání slov. To znamená, že uživatel, který zruší operaci během rozpoznávání 400stránkového souboru, se zastaví během jedné stránky místo na konci dokumentu, a stejný vzor tokenu použitý jinde v komponentě, popsaný v článku zrušitelné progresivní vykreslování, platí i zde beze změny

Omezení selhání je na úrovni stránky. Knihovna shromažďuje handly objektů, které na stránku vložila, a jednou, po umístění všech slov, zavolá FPDFPage_GenerateContent. Pokud cokoli selže uprostřed, ať už chyba poskytovatele nebo problém s fontem, objekty vložené na tuto stránku se odstraní v opačném pořadí a obsah stránky se znovu vygeneruje, takže selhaná stránka se vrátí do původního stavu místo aby si ponechala poloviční textovou vrstvu. Smyčka dokumentu pak pokračuje nebo se zastaví podle ContinueOnError a aktivní stránka je vždy obnovena

Ověření, že se obrázku skutečně nikdo nedotkl

Nejsilnější dostupná kontrola je zároveň nejjednodušší: vykreslete stránku před a po aplikaci vrstvy ve stejné velikosti a porovnejte bitmapy. Měly by být identické bajt po bajtu, protože neviditelný text nic nekreslí a proud obrázku se nikdy nedekódoval. Jakýkoli rozdíl znamená, že stránku změnilo něco jiného než textová vrstva

Poté ověřte textovou stránku extrakcí ze zpracovaného souboru a potvrzením, že pozice slov dopadají na sken. Extrakční cesta je stejná jako popsaná v článku extrakce textu z dokumentů PDF, a pro rychlou vizuální kontrolu zarovnání vám vykreslení stránek do obrázků, jak popisuje článek převod stránek PDF na obrázky JPEG, umožní překrýt rámečky slov přes sken

Vrstvení OCR, vykreslování, extrakce a úpravy běží proti stejnému objektu dokumentu v Delphi, C++Builder a Lazarus; kompletní povrch API popisuje stránka PDFium Component pro Delphi