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