Teknisk artikel

Tilføj et søgbart tekstlag til scannede PDF'er i Delphi

PDFium Component tilføjer et søgbart tekstlag til scannede PDF-sider fra Delphi via ApplyOcrSearchLayer. Den renderer hver valgt side, overdrager pixlerne til en OCR-udbyder, du selv leverer, og skriver de genkendte ord tilbage som usynlige tekstobjekter placeret oven på ordene i scanningen. Det oprindelige sidebillede afkodes, genkodes eller erstattes aldrig, så det visuelle resultat er byte-for-byte den side, du startede med

Genkendelsesmotoren er bevidst ikke en del af biblioteket. PDFium eksponerer siderendering, koordinatafbildning, skrifttypeindlæsning, oprettelse af tekstobjekter og usynlige rendertilstande, men den indeholder ingen OCR-motor, og at lade som andet ville betyde at bundte en andens genkendelsesprodukt ind i en PDF-komponent. I stedet bor genkendelse bag grænsefladen IPdfOcrProvider: biblioteket sender fastlayoutede, top-oprindelige BGRA-pixels, og udbyderen returnerer Unicode-tekst, konfidensværdier og ordfirkanter

Hvad er et søgbart tekstlag egentlig?

En scannet PDF er et billede af et dokument. Sideindholdet er ét stort billede, og der er intet at markere, søge i, kopiere eller indeksere. Et søgbart tekstlag tilføjer rigtige tekstobjekter oven på det billede med rendertilstanden sat til usynlig, så viewere ikke tegner noget, men markering, søgning og udtræk finder ordene præcis, hvor de optræder

Positionering er hele spillet. Hvis den usynlige tekst sidder nogle punkter forskudt, lander markeringsfremhævelser ved siden af ordene frem for på dem, og at kopiere et afsnit producerer tekst i den forkerte rækkefølge. Det er derfor, geometrien skal komme fra de samme transformationer, PDFium bruger til at rendere siden, frem for fra et proportionalt gæt

Implementering af udbyderen

Udbyderkontrakten er én metode. Den modtager en sidebillede-post, der bærer dimensioner, stride, DPI, pixelformat og selve pixel-bytene, plus en annulleringstoken, og returnerer ord eller en fejlmeddelelse:

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 indeholder top-oprindelige BGRA-rækker af Image.Stride bytes.
  // Send dem til din motor, og udfyld derefter én post pr. genkendt ord
  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 frem for rektangler, fordi en scanning sjældent er kvadratisk med siden. Et ord på en let roteret side optager et parallelogram, og TPdfOcrQuad bærer fire hjørnepunkter, så skæve og roterede ord bevarer en nøjagtig markeringsregion. Motorer, der kun rapporterer akse-justerede bokse, kan bruge FromRectangle, som bygger den degenererede quad

Hvorfor kan ordpositioner ikke skaleres proportionalt?

Det er fristende at konvertere en pixelkoordinat til en sidekoordinat ved at dividere med renderbredden og gange med sidebredden. Det virker kun for sider uden rotation, en CropBox identisk med MediaBox, og en oprindelse ved nul, og mange scannede dokumenter fejler mindst én af disse betingelser

PDFium Component afbilder hvert af de fire quad-hjørner individuelt via FPDF_DeviceToPage, den samme afbildning, rendereren brugte til at producere pixlerne, så /Rotate-poster og forskudte crop-bokse håndteres ved konstruktion. Den affine matrix for tekstobjektet bygges derefter ud fra tre af de afbildede punkter, nederste venstre, nederste højre og øverste venstre hjørne, hvilket er præcis nok til at udtrykke position, skalering, rotation og skævhed

Selve tekstobjektet oprettes med enheds-skriftstørrelse, så dets reelle skrifttypegrænser kan måles, og de målte objektgrænser afbildes derefter på mål-quadden. At dimensionere ud fra en gættet punktstørrelse og håbe, den matcher det scannede ord, ville drive med hver skrifttypesubstitution; at måle først gør tilpasningen uafhængig af, hvilken skrifttype laget bruger

At køre det over et dokument

Indstillingsposten styrer opløsning, filtrering og hvert budget. Konfidensfiltrering betyder mere, end det ser ud til: skrammelord ved lav konfidens forurener søgeresultater permanent, og i modsætning til en forkert rendering bemærker ingen det, før en søgning returnerer noget meningsløst:

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;                  // genkendelsesopløsning
    Options.MinConfidence := 0.60;       // drop usikre ord
    Options.SkipPagesWithText := True;   // lad født-digitale sider være
    Options.ContinueOnError := True;     // én dårlig side må ikke stoppe jobbet
    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 fortjener vægt i blandede arkiver. En PDF, der allerede bærer rigtig tekst, hvad enten den er født digital eller tidligere behandlet, får et andet tekstlag, hvis man kører OCR over den blindt, og duplikatet gør, at udtræk returnerer hvert ord to gange. Statussen pr. side popsSkippedExistingText fortæller præcis, hvilke sider der blev ladt urørt

Budgetter, annullering og fejlbegrænsning

Enhver mængde, som et fjendtligt eller blot enormt dokument kan oppuste, har et loft: pixels pr. side og i alt, ord pr. side og i alt, og tegn pr. ord. Alle tjekkes, før siden skrives, ikke bagefter, og pixelestimatet beregnes ud fra sidedimensioner og DPI, før noget bitmap allokeres. At hæve DPI fra 150 til 300 firdobler hukommelsen pr. side, så loftet pr. side er den parameter, man skal justere først, når et batch-job begynder at fejle på store formater

Annulleringstokenet tråder gennem hele stien: progressiv rendering, udbyderkaldet og indsættelsesløkken pr. ord. Det betyder, at en bruger, der annullerer under genkendelse af en fil på 400 sider, stopper inden for én side frem for ved slutningen af dokumentet, og det samme token-mønster, brugt andre steder i komponenten, beskrevet i annullérbar progressiv rendering, gælder her uændret

Fejlbegrænsning er pr. side. Biblioteket indsamler de objekthåndtag, det indsatte på en side, og kalder FPDFPage_GenerateContent én gang, efter alle ord er placeret. Hvis noget fejler undervejs, uanset om det er en udbyderfejl eller et skrifttypeproblem, fjernes de objekter, der er indsat på den side, i omvendt rækkefølge, og sideindholdet regenereres, så en mislykket side vender tilbage til sin oprindelige tilstand i stedet for at beholde et halvt tekstlag. Dokumentløkken fortsætter eller stopper derefter i henhold til ContinueOnError, og den aktive side gendannes altid

Verificering af, at billedet reelt var urørt

Det stærkeste tjek til rådighed er også det simpleste: rendér siden før og efter påføring af laget ved samme størrelse, og sammenlign bitmapene. De bør være identiske byte for byte, fordi usynlig tekst ikke tegner noget, og billedstrømmen aldrig blev afkodet. Enhver forskel betyder, at noget andet end tekstlaget ændrede siden

Verificér derefter tekstsiden ved at udtrække fra den behandlede fil og bekræfte, at ordpositioner lander på scanningen. Udtræksstien er den samme, der er beskrevet i udtræk af tekst fra PDF-dokumenter, og til et hurtigt visuelt tjek af justering lader rendering af sider til billeder som i konvertering af PDF-sider til JPEG dig lægge ordbokse oven på scanningen

OCR-lagdeling, rendering, udtræk og redigering kører alle mod det samme dokumentobjekt i Delphi, C++Builder og Lazarus; hele API-fladen er beskrevet på siden PDFium Component til Delphi