Techninis straipsnis

Ieškomo teksto sluoksnio pridėjimas prie nuskaitytų PDF failų Delphi aplinkoje

PDFium Component prideda ieškomą teksto sluoksnį prie nuskaitytų PDF puslapių iš Delphi per ApplyOcrSearchLayer. Ji atvaizduoja kiekvieną pasirinktą puslapį, perduoda pikselius jūsų pateiktam OCR tiekėjui ir įrašo atpažintus žodžius atgal kaip nematomus teksto objektus, patalpintus virš žodžių nuskaitytame vaizde. Originalus puslapio vaizdas niekada nedekoduojamas, nekoduojamas iš naujo ir nepakeičiamas, todėl vizualinis rezultatas baitas po baito yra tas pats puslapis, nuo kurio pradėjote

Atpažinimo variklis sąmoningai nėra bibliotekos dalis. PDFium atskleidžia puslapio atvaizdavimą, koordinačių susiejimą, šrifto įkėlimą, teksto objektų kūrimą ir nematomus atvaizdavimo režimus, tačiau joje nėra jokio OCR variklio, o apsimesti kitaip reikštų kažkieno atpažinimo produktą sukišti į PDF komponentą. Vietoj to atpažinimas gyvena už IPdfOcrProvider sąsajos: biblioteka perduoda fiksuoto maketo, viršuje prasidedančius BGRA pikselius, o tiekėjas grąžina Unicode tekstą, pasitikėjimo reikšmes ir žodžių keturkampius

Kas iš tikrųjų yra ieškomas teksto sluoksnis?

Nuskaitytas PDF failas yra dokumento nuotrauka. Puslapio turinys yra vienas didelis vaizdas, ir nėra ką pažymėti, ieškoti, kopijuoti ar indeksuoti. Ieškomas teksto sluoksnis prideda tikrus teksto objektus virš to vaizdo su atvaizdavimo režimu, nustatytu į nematomą, todėl peržiūros priemonės nieko nepiešia, bet žymėjimas, paieška ir ištraukimas randa žodžius lygiai ten, kur jie pasirodo

Pozicionavimas yra visas žaidimas. Jei nematomas tekstas atsiduria kelis punktus per toli, žymėjimo paryškinimai atsiduria šalia žodžių, o ne ant jų, o pastraipos kopijavimas duoda tekstą neteisinga tvarka. Būtent todėl geometrija turi ateiti iš tų pačių transformacijų, kurias PDFium naudoja puslapiui atvaizduoti, o ne iš proporcingo spėjimo

Tiekėjo įgyvendinimas

Tiekėjo sutartis yra vienas metodas. Jis gauna puslapio vaizdo įrašą, nešantį matmenis, žingsnį (stride), DPI, pikselių formatą ir pačius pikselių baitus, plius atšaukimo žymenį, ir grąžina žodžius arba klaidos pranešimą:

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 laiko viršuje prasidedančias BGRA eilutes,
  // po Image.Stride baitų. Perduokite jas savo varikliui, tada
  // užpildykite po vieną įrašą kiekvienam atpažintam žodžiui
  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;

Keturkampiai, o ne stačiakampiai, nes nuskaitymas retai būna tiksliai statmenas puslapiui. Žodis šiek tiek pasuktame puslapyje užima lygiagretainį, ir TPdfOcrQuad neša keturis kampų taškus, kad iškreipti ir pasukti žodžiai išlaikytų tikslią žymėjimo sritį. Varikliai, kurie praneša tik ašims lygiagrečius stačiakampius, gali naudoti FromRectangle, kuris sukuria išsigimusį keturkampį

Kodėl žodžių pozicijų negalima keisti proporcingu masteliu?

Kyla pagunda pikselio koordinatę paversti puslapio koordinate padalijant iš atvaizdavimo pločio ir padauginant iš puslapio pločio. Tai veikia tik puslapiams be pasukimo, kai CropBox identiškas MediaBox, ir pradžios taškas yra nulyje, o daugybė nuskaitytų dokumentų neatitinka bent vienos iš šių sąlygų

PDFium Component susieja kiekvieną iš keturių keturkampio kampų atskirai per FPDF_DeviceToPage – tą patį susiejimą, kurį atvaizduotojas naudojo pikseliams sukurti, todėl /Rotate įrašai ir pastumti apkarpymo laukai apdorojami savaime, konstrukcijos lygmenyje. Afininė teksto objekto matrica tada sudaroma iš trijų susietų taškų – apatinio kairiojo, apatinio dešiniojo ir viršutinio kairiojo kampų, kurių lygiai pakanka pozicijai, masteliui, pasukimui ir šlyčiui išreikšti

Pats teksto objektas sukuriamas vieneto šrifto dydžiu, kad būtų galima pamatuoti jo realias šrifto ribas, o tada pamatuoti objekto ribos susiejamos su tiksliniu keturkampiu. Dydžio nustatymas pagal spėtą taško dydį, tikintis, kad jis sutaps su nuskaitytu žodžiu, nukryptų su kiekvienu šrifto pakeitimu; pirmiausia matuojant, atitikimas tampa nepriklausomas nuo to, kokį šriftą naudoja sluoksnis

Vykdymas per visą dokumentą

Parinkčių įrašas valdo skiriamąją gebą, filtravimą ir kiekvieną biudžetą. Pasitikėjimo filtravimas svarbesnis, nei atrodo: šiukšlių žodžiai su žemu pasitikėjimu užteršia paieškos rezultatus visam laikui, ir skirtingai nei neteisingas atvaizdavimas, niekas to nepastebi, kol paieška negrąžina nesąmonių:

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;                  // atpažinimo skiriamoji geba
    Options.MinConfidence := 0.60;       // atmesti neaiškius žodžius
    Options.SkipPagesWithText := True;   // nepaliesti skaitmeniškai sukurtų puslapių
    Options.ContinueOnError := True;     // vienas blogas puslapis neturi sustabdyti darbo
    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 verta pabrėžti mišriuose archyvuose. PDF failas, kuris jau turi tikrą tekstą – ar tai būtų nuo pat pradžių skaitmeninis, ar anksčiau apdorotas – aklai paleidus per jį OCR gauna antrą teksto sluoksnį, o dubliuotas turinys priverčia ištraukimą grąžinti kiekvieną žodį du kartus. Kiekvieno puslapio būsena popsSkippedExistingText tiksliai parodo, kurie puslapiai buvo palikti nepaliesti

Biudžetai, atšaukimas ir gedimo lokalizavimas

Kiekvienas dydis, kurį priešiškas ar tiesiog milžiniškas dokumentas gali išpūsti, turi lubą: pikseliai vienam puslapiui ir iš viso, žodžiai vienam puslapiui ir iš viso, simboliai vienam žodžiui. Visi jie tikrinami prieš puslapį parašant, o ne po to, ir pikselių įvertis apskaičiuojamas iš puslapio matmenų ir DPI dar prieš paskirstant bet kokį bitmap. DPI padidinimas nuo 150 iki 300 keturis kartus padidina atminties naudojimą vienam puslapiui, todėl puslapio riba yra parametras, kurį pirmiausia verta koreguoti, kai partinis darbas pradeda žlugti su dideliais formatais

Atšaukimo žymuo eina per visą kelią: laipsniškas atvaizdavimas, tiekėjo iškvietimas ir kiekvieno žodžio įterpimo ciklas. Tai reiškia, kad vartotojas, kuris atšaukia atpažinimą 400 puslapių failo viduryje, sustos per vieną puslapį, o ne dokumento pabaigoje, ir tas pats žymens modelis, naudojamas kitur komponente, aprašytas straipsnyje atšaukiamas laipsniškas atvaizdavimas, taikomas ir čia be pakeitimų

Gedimo lokalizavimas vyksta puslapio lygiu. Biblioteka surenka objektų saugiklius, kuriuos ji įterpė puslapyje, ir vieną kartą, kai visi žodžiai patalpinti, iškviečia FPDFPage_GenerateContent. Jei kas nors nepavyksta pusiaukelėje – ar tai tiekėjo klaida, ar šrifto problema – tame puslapyje įterpti objektai pašalinami atvirkštine tvarka, o puslapio turinys sugeneruojamas iš naujo, todėl nepavykęs puslapis grįžta į savo pradinę būseną, o ne palieka pusę teksto sluoksnio. Dokumento ciklas tada tęsiasi arba sustoja pagal ContinueOnError, o aktyvus puslapis visada atkuriamas

Kaip patikrinti, ar vaizdas iš tikrųjų liko nepaliestas

Stipriausia turima patikra taip pat yra paprasčiausia: atvaizduokite puslapį prieš pridedant sluoksnį ir po to tuo pačiu dydžiu, ir palyginkite bitmap'us. Jie turėtų būti identiški baitas po baito, nes nematomas tekstas nieko nepiešia, o vaizdo srautas niekada nebuvo dekoduotas. Bet koks skirtumas reiškia, kad puslapį pakeitė kažkas kitas, ne teksto sluoksnis

Po to patikrinkite teksto pusę, ištraukdami tekstą iš apdoroto failo ir patvirtindami, kad žodžių pozicijos atsiduria ant nuskaitymo. Ištraukimo kelias yra tas pats, aprašytas straipsnyje teksto ištraukimas iš PDF dokumentų, o greitam vizualiam lygiavimo patikrinimui puslapių atvaizdavimas į vaizdus, kaip aprašyta straipsnyje PDF puslapių konvertavimas į JPEG, leidžia uždėti žodžių langelius ant nuskaitymo

OCR sluoksniavimas, atvaizdavimas, ištraukimas ir redagavimas visi veikia su tuo pačiu dokumento objektu Delphi, C++Builder ir Lazarus aplinkose; pilnas API paviršius aprašytas PDFium Component Delphi puslapyje