A PDFium Component az ApplyOcrSearchLayer-en keresztül kereshető szövegréteget ad Delphiből szkennelt PDF-oldalakhoz. Renderel minden kiválasztott oldalt, átadja a pixeleket egy általad biztosított OCR-szolgáltatónak, majd visszaírja a felismert szavakat láthatatlan szövegobjektumként, a szkennelt szavak fölé pozicionálva. Az eredeti oldalkép soha nem kerül dekódolásra, újrakódolásra vagy cserére, így a vizuális eredmény bájtra pontosan megegyezik azzal az oldallal, amellyel kezdted
A felismerőmotor szándékosan nem része a könyvtárnak. A PDFium kiteszi az oldalrenderelést, a koordináta-leképezést, a betűtípus-betöltést, a szövegobjektum-létrehozást és a láthatatlan renderelési módokat, de nem tartalmaz OCR-motort, és ennek ellenkezőjét színlelni azt jelentené, hogy valaki más felismerő termékét ágyaznánk be egy PDF-komponensbe. Ehelyett a felismerés az IPdfOcrProvider interfész mögött él: a könyvtár rögzített elrendezésű, felülről induló BGRA pixeleket ad át, a szolgáltató pedig Unicode szöveget, megbízhatósági értékeket és szónégyszögeket ad vissza
Mi is pontosan egy kereshető szövegréteg?
Egy szkennelt PDF egy dokumentum képe. Az oldal tartalma egyetlen nagy kép, és nincs mit kijelölni, keresni, másolni vagy indexelni. Egy kereshető szövegréteg valódi szövegobjektumokat ad ennek a képnek a tetejére, láthatatlanra állított renderelési móddal, így a megjelenítők semmit nem rajzolnak ki, a kijelölés, a keresés és a kinyerés viszont pontosan ott találja meg a szavakat, ahol megjelennek
A pozicionálás az egész lényeg. Ha a láthatatlan szöveg néhány ponttal elcsúszik, a kijelölés kiemelése a szavak mellé kerül, nem rájuk, egy bekezdés másolása pedig rossz sorrendű szöveget ad. Ezért kell a geometriának ugyanazokból a transzformációkból származnia, amelyeket a PDFium az oldal rendereléséhez használ, nem egy arányos becslésből
A szolgáltató implementálása
A szolgáltató szerződése egyetlen metódusból áll. Egy oldalkép-rekordot kap, amely tartalmazza a méreteket, a stride-ot, a DPI-t, a pixelformátumot és magukat a pixelbájtokat, plusz egy megszakítási tokent, és szavakat vagy hibaüzenetet ad vissza:
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
// Az Image.Pixels felülről induló BGRA sorokat tartalmaz Image.Stride bájtonként.
// Add át őket a motorodnak, majd tölts ki egy bejegyzést minden felismert szóhoz
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;
Négyszögek, nem téglalapok, mert egy szkennelt kép ritkán négyzetes az oldalhoz képest. Egy enyhén elforgatott oldalon egy szó paralelogrammát foglal el, a TPdfOcrQuad pedig négy saroköpontot hordoz, így a ferde és elforgatott szavak pontos kijelölési régiót kapnak. Azok a motorok, amelyek csak tengelyhez igazított dobozokat jelentenek, használhatják a FromRectangle-t, amely felépíti a degenerált négyszöget
Miért nem lehet a szópozíciókat arányosan skálázni?
Csábító egy pixelkoordinátát oldalkoordinátává alakítani úgy, hogy elosztjuk a renderelési szélességgel, majd megszorozzuk az oldalszélességgel. Ez csak olyan oldalaknál működik, amelyeknél nincs forgatás, a CropBox megegyezik a MediaBox-szal, és az origó nullánál van, sok szkennelt dokumentum pedig ezen feltételek közül legalább az egyiket nem teljesíti
A PDFium Component a négyszög mind a négy sarkát egyenként leképezi az FPDF_DeviceToPage-en keresztül, ugyanazzal a leképezéssel, amelyet a renderelő a pixelek előállításához használt, így a /Rotate bejegyzéseket és az eltolt vágódobozokat eleve kezeli. A szövegobjektum affin mátrixa ezután a leképezett pontok közül háromból épül fel, a bal alsó, jobb alsó és bal felső sarokból, ami pontosan elég a pozíció, a méret, a forgatás és a nyírás kifejezéséhez
Maga a szövegobjektum egységnyi betűmérettel jön létre, hogy a valódi betűtípus-határai mérhetők legyenek, a mért objektumhatárok pedig ezután a célnégyszögre kerülnek leképezésre. Egy becsült pontméret szerinti méretezés, abban bízva, hogy illeszkedik a szkennelt szóhoz, minden betűtípus-cserénél elcsúszna; az előzetes mérés függetlenné teszi az illeszkedést attól, melyik betűtípust használja a réteg
Futtatás egy dokumentumon
Az opciórekord vezérli a felbontást, a szűrést és minden költségkeretet. A megbízhatóság szerinti szűrés jobban számít, mint amilyennek látszik: az alacsony megbízhatóságú szemétszavak tartósan szennyezik a keresési eredményeket, és egy rossz rendereléstől eltérően senki nem veszi észre, amíg egy keresés értelmetlenséget nem ad vissza:
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; // felismerési felbontás
Options.MinConfidence := 0.60; // bizonytalan szavak eldobása
Options.SkipPagesWithText := True; // az eredetileg digitális oldalakat érintetlenül hagyja
Options.ContinueOnError := True; // egyetlen rossz oldal sem állíthatja meg a feladatot
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;
A SkipPagesWithText vegyes archívumokban különös hangsúlyt érdemel. Egy olyan PDF, amely már valódi szöveget hordoz, akár eredetileg digitális, akár korábban feldolgozott, egy második szövegréteget kap, ha vakon futtatsz rajta OCR-t, a duplikáció pedig azt eredményezi, hogy a kinyerés minden szót kétszer ad vissza. Az oldalankénti popsSkippedExistingText állapot pontosan megmondja, mely oldalakat hagyta érintetlenül a rendszer
Költségkeretek, megszakítás és hibatartalmazás
Minden olyan mennyiségnek, amelyet egy rosszindulatú vagy egyszerűen csak hatalmas dokumentum felfújhat, van felső határa: oldalankénti és összesített pixelek, oldalankénti és összesített szavak, valamint szavankénti karakterek. Mindegyiket az oldal megírása előtt ellenőrzi, nem utána, a pixelbecslést pedig az oldalméretekből és a DPI-ből számítja ki, mielőtt bármilyen bitkép lefoglalásra kerülne. A DPI 150-ről 300-ra emelése négyszerezi az oldalankénti memóriát, így az oldalankénti felső határ az a paraméter, amelyet elsőként érdemes hangolni, amikor egy kötegelt feladat nagy formátumoknál elkezd elbukni
A megszakítási token végigfut a teljes útvonalon: a progresszív rendereléstől a szolgáltatóhíváson át a szavankénti beszúrási ciklusig. Ez azt jelenti, hogy egy felhasználó, aki egy 400 oldalas fájl felismerése közben megszakítja a folyamatot, egy oldalon belül megáll, nem a dokumentum végén, és a komponens más részein használt ugyanaz a token-minta, amelyet a megszakítható progresszív renderelés ismertet, itt változtatás nélkül alkalmazandó
A hibatartalmazás oldalankénti. A könyvtár összegyűjti az egy oldalon beszúrt objektumleírókat, és egyszer hívja meg az FPDFPage_GenerateContent-et, miután minden szó elhelyezésre került. Ha valami félúton elbukik, legyen az egy szolgáltatóhiba vagy egy betűtípus-probléma, az adott oldalon beszúrt objektumokat fordított sorrendben eltávolítja, és az oldaltartalmat újragenerálja, így egy sikertelen oldal visszaáll az eredeti állapotára, ahelyett hogy fél szövegréteget tartana meg. A dokumentumciklus ezután a ContinueOnError szerint folytatódik vagy áll le, az aktív oldal pedig mindig visszaáll
Annak ellenőrzése, hogy a kép valóban érintetlen maradt-e
A legerősebb elérhető ellenőrzés egyben a legegyszerűbb is: renderelj az oldalt ugyanabban a méretben a réteg alkalmazása előtt és után, és hasonlítsd össze a bitképeket. Bájtra pontosan azonosnak kell lenniük, mert a láthatatlan szöveg semmit nem rajzol ki, és a képfolyam soha nem került dekódolásra. Bármilyen eltérés azt jelenti, hogy valami más, nem a szövegréteg, megváltoztatta az oldalt
Ezután ellenőrizd a szöveges oldalt is: nyerd ki a szöveget a feldolgozott fájlból, és erősítsd meg, hogy a szópozíciók a szkennelt képre esnek. A kinyerési útvonal ugyanaz, amelyet a szöveg kinyerése PDF-dokumentumokból ismertet, a gyors vizuális illeszkedés-ellenőrzéshez pedig az oldalak képekké renderelése, ahogy a PDF-oldalak JPEG-gé konvertálása is bemutatja, lehetővé teszi a szódobozok rárétegzését a szkennelt képre
Az OCR-rétegezés, a renderelés, a kinyerés és a szerkesztés mind ugyanazon a dokumentumobjektumon fut Delphiben, C++Builderben és Lazarusban; a teljes API-felületet a PDFium Component for Delphi oldala ismerteti