PDFium Component voegt vanuit Delphi via ApplyOcrSearchLayer een doorzoekbare tekstlaag toe aan gescande PDF-pagina's. Het rendert elke geselecteerde pagina, geeft de pixels door aan een OCR-provider die u zelf levert, en schrijft de herkende woorden terug als onzichtbare tekstobjecten die over de woorden in de scan zijn gepositioneerd. De originele pagina-afbeelding wordt nooit gedecodeerd, opnieuw gecodeerd of vervangen, dus het visuele resultaat is byte voor byte de pagina waarmee u begon
De herkenningsengine is bewust geen onderdeel van de bibliotheek. PDFium biedt paginarendering, coördinaatafbeelding, lettertypeladen, aanmaak van tekstobjecten en onzichtbare rendermodi, maar bevat geen OCR-engine, en doen alsof dat wel zo was, zou betekenen dat iemands herkenningsproduct in een PDF-component wordt gebundeld. In plaats daarvan bevindt herkenning zich achter de IPdfOcrProvider-interface: de bibliotheek geeft vast-layout, top-origin BGRA-pixels door, en de provider retourneert Unicode-tekst, betrouwbaarheidswaarden en woordvierhoeken
Wat is een doorzoekbare tekstlaag precies?
Een gescande PDF is een foto van een document. De pagina-inhoud is één grote afbeelding, en er is niets om te selecteren, doorzoeken, kopiëren of indexeren. Een doorzoekbare tekstlaag voegt echte tekstobjecten toe bovenop die afbeelding, met de rendermodus ingesteld op onzichtbaar, zodat viewers niets tekenen, maar selectie, zoeken en extractie de woorden precies vinden waar ze verschijnen
Positionering is het hele spel. Als de onzichtbare tekst een paar punten verschoven staat, landen selectiemarkeringen naast de woorden in plaats van erop, en het kopiëren van een alinea produceert tekst in de verkeerde volgorde. Daarom moet de geometrie afkomstig zijn van dezelfde transformaties die PDFium gebruikt om de pagina te renderen, in plaats van van een proportionele schatting
De provider implementeren
Het providercontract is één methode. Het ontvangt een paginaafbeeldingsrecord met afmetingen, stride, DPI, pixelformaat en de pixelbytes zelf, plus een annuleringstoken, en retourneert woorden of een foutmelding:
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 bevat top-origin BGRA-rijen van Image.Stride bytes.
// Geef ze door aan uw engine en vul dan één item per herkend woord
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;
Quads in plaats van rechthoeken, omdat een scan zelden haaks op de pagina staat. Een woord op een licht gedraaide pagina neemt een parallellogram in beslag, en TPdfOcrQuad draagt vier hoekpunten zodat scheve en geroteerde woorden een nauwkeurig selectiegebied behouden. Engines die alleen assengelijnde vakken rapporteren, kunnen FromRectangle gebruiken, dat de gedegenereerde quad opbouwt
Waarom kunnen woordposities niet proportioneel worden geschaald?
Het is verleidelijk om een pixelcoördinaat om te zetten naar een paginacoördinaat door te delen door de renderbreedte en te vermenigvuldigen met de paginabreedte. Dat werkt alleen voor pagina's zonder rotatie, met een CropBox identiek aan de MediaBox, en een oorsprong bij nul, en heel wat gescande documenten voldoen niet aan minstens een van die voorwaarden
PDFium Component beeldt elk van de vier quadhoeken afzonderlijk af via FPDF_DeviceToPage, dezelfde afbeelding die de renderer gebruikte om de pixels te produceren, dus /Rotate-vermeldingen en verschoven crop boxes worden door constructie afgehandeld. De affiene matrix voor het tekstobject wordt vervolgens opgebouwd uit drie van de afgebeelde punten, de linksonder-, rechtsonder- en linksbovenhoek, wat precies genoeg is om positie, schaal, rotatie en schuinte uit te drukken
Het tekstobject zelf wordt aangemaakt op eenheidslettergrootte, zodat de werkelijke lettertypegrenzen kunnen worden gemeten, en de gemeten objectgrenzen worden vervolgens afgebeeld op de doelquad. Formaat bepalen op basis van een geschatte puntgrootte en hopen dat die overeenkomt met het gescande woord, zou bij elke lettertypevervanging afdrijven; eerst meten maakt de pasvorm onafhankelijk van welk lettertype de laag gebruikt
Het over een document laten lopen
Het optiesrecord bepaalt resolutie, filtering en elk budget. Betrouwbaarheidsfiltering doet er meer toe dan het lijkt: rommelwoorden met een lage betrouwbaarheid vervuilen zoekresultaten permanent, en anders dan bij een verkeerde rendering merkt niemand het totdat een zoekopdracht onzin oplevert:
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; // herkenningsresolutie
Options.MinConfidence := 0.60; // laat onzekere woorden vallen
Options.SkipPagesWithText := True; // laat born-digital pagina's met rust
Options.ContinueOnError := True; // één slechte pagina mag de taak niet stoppen
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 verdient nadruk in gemengde archieven. Een PDF die al echte tekst draagt, ongeacht of deze born digital is of eerder verwerkt, krijgt een tweede tekstlaag als u er blindelings OCR overheen laat lopen, en het duplicaat zorgt ervoor dat extractie elk woord tweemaal retourneert. De per-pagina-status popsSkippedExistingText vertelt u precies welke pagina's met rust zijn gelaten
Budgetten, annulering en foutindamming
Elke grootheid die een vijandig of gewoon enorm document kan opblazen, heeft een plafond: pixels per pagina en in totaal, woorden per pagina en in totaal, en tekens per woord. Ze worden allemaal gecontroleerd voordat de pagina wordt geschreven, niet erna, en de pixelschatting wordt berekend op basis van paginadimensies en DPI voordat er een bitmap wordt toegewezen. Een DPI-verhoging van 150 naar 300 verviervoudigt het geheugen per pagina, dus het plafond per pagina is de parameter om als eerste af te stellen wanneer een batchtaak begint te falen op grote formaten
Het annuleringstoken loopt door het hele pad: progressieve rendering, de provideraanroep en de per-woord invoeglus. Dat betekent dat een gebruiker die annuleert tijdens de herkenning van een bestand van 400 pagina's binnen één pagina stopt in plaats van aan het einde van het document, en hetzelfde tokenpatroon dat elders in het component wordt gebruikt, beschreven in annuleerbare progressieve rendering, geldt hier ongewijzigd
Foutindamming gebeurt per pagina. De bibliotheek verzamelt de objecthandles die het op een pagina heeft ingevoegd en roept FPDFPage_GenerateContent eenmaal aan, nadat alle woorden zijn geplaatst. Als er halverwege iets misgaat, of het nu een providerfout of een lettertypeprobleem is, worden de op die pagina ingevoegde objecten in omgekeerde volgorde verwijderd en wordt de pagina-inhoud opnieuw gegenereerd, zodat een mislukte pagina terugkeert naar zijn oorspronkelijke staat in plaats van een halve tekstlaag te behouden. De documentlus gaat vervolgens door of stopt volgens ContinueOnError, en de actieve pagina wordt altijd hersteld
Verifiëren dat de afbeelding echt onaangeroerd bleef
De sterkste beschikbare controle is ook de eenvoudigste: render de pagina vóór en na het toepassen van de laag op dezelfde grootte en vergelijk de bitmaps. Ze zouden byte voor byte identiek moeten zijn, omdat onzichtbare tekst niets tekent en de afbeeldingsstream nooit is gedecodeerd. Elk verschil betekent dat iets anders dan de tekstlaag de pagina heeft veranderd
Verifieer daarna de tekstkant door te extraheren uit het verwerkte bestand en te bevestigen dat woordposities op de scan terechtkomen. Het extractiepad is hetzelfde als beschreven in tekst extraheren uit PDF-documenten, en voor een snelle visuele controle van de uitlijning kunt u met het renderen van pagina's naar afbeeldingen zoals in PDF-pagina's converteren naar JPEG woordvakken over de scan leggen
OCR-laagvorming, rendering, extractie en bewerking draaien allemaal tegen hetzelfde documentobject in Delphi, C++Builder en Lazarus; het volledige API-oppervlak wordt beschreven op de PDFium Component voor Delphi-pagina