Teknisk artikkel

Legge til et søkbart tekstlag i skannede PDF-er i Delphi

PDFium Component legger til et søkbart tekstlag i skannede PDF-sider fra Delphi gjennom ApplyOcrSearchLayer. Den gjengir hver valgte side, gir pikslene til en OCR-leverandør du selv sørger for, og skriver de gjenkjente ordene tilbake som usynlige tekstobjekter plassert over ordene i skanningen. Det originale sidebildet blir aldri dekodet, re-kodet eller erstattet, så det visuelle resultatet er byte-for-byte den siden du startet med

Gjenkjenningsmotoren er bevisst ikke en del av biblioteket. PDFium eksponerer sidegjengivelse, koordinatkartlegging, fontinnlasting, opprettelse av tekstobjekter og usynlige gjengivelsesmoduser, men den inneholder ingen OCR-motor, og å late som noe annet ville betydd å bunte noen andres gjenkjenningsprodukt inn i en PDF-komponent. I stedet bor gjenkjenning bak IPdfOcrProvider-grensesnittet: biblioteket sender fastlayout, øvre-opprinnelse BGRA-piksler, og leverandøren returnerer Unicode-tekst, konfidensverdier og ordfirkanter

Hva er egentlig et søkbart tekstlag?

En skannet PDF er et bilde av et dokument. Sideinnholdet er ett stort bilde, og det finnes ingenting å velge, søke i, kopiere eller indeksere. Et søkbart tekstlag legger til ekte tekstobjekter oppå det bildet med gjengivelsesmodusen satt til usynlig, slik at visningsklienter ikke tegner noe, men markering, søk og uthenting finner ordene nøyaktig der de opptrer

Posisjonering er hele spillet. Hvis den usynlige teksten sitter noen punkter feil, lander markeringsutheving ved siden av ordene i stedet for på dem, og å kopiere et avsnitt produserer tekst i feil rekkefølge. Det er derfor geometrien må komme fra de samme transformasjonene PDFium bruker for å gjengi siden, snarere enn fra en proporsjonal gjetning

Implementere leverandøren

Leverandørkontrakten er én metode. Den mottar en sidebilde-post som bærer dimensjoner, stride, DPI, pikselformat og selve pikselbytene, pluss et kanselleringstoken, og returnerer ord eller en feilmelding:

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 inneholder BGRA-rader med øvre opprinnelse på Image.Stride byte.
  // Gi dem til motoren din, og fyll deretter én oppføring per gjenkjent 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;

Firkanter snarere enn rektangler, fordi en skanning sjelden er kvadratisk mot siden. Et ord på en litt rotert side opptar et parallellogram, og TPdfOcrQuad bærer fire hjørnepunkter, slik at skjeve og roterte ord beholder en nøyaktig markeringsregion. Motorer som bare rapporterer akse-justerte bokser, kan bruke FromRectangle, som bygger den degenererte firkanten

Hvorfor kan ikke ordposisjoner skaleres proporsjonalt?

Det er fristende å konvertere en pikselkoordinat til en sidekoordinat ved å dele på gjengivelsesbredden og multiplisere med sidebredden. Det fungerer bare for sider uten rotasjon, en CropBox identisk med MediaBox, og en opprinnelse på null, og mange skannede dokumenter feiler på minst én av de betingelsene

PDFium Component kartlegger hvert av de fire firkanthjørnene enkeltvis gjennom FPDF_DeviceToPage, den samme kartleggingen gjengiveren brukte for å produsere pikslene, slik at /Rotate-oppføringer og forskjøvne beskjæringsbokser håndteres ved konstruksjon. Den affine matrisen for tekstobjektet bygges deretter fra tre av de kartlagte punktene, nedre venstre, nedre høyre og øvre venstre hjørne, som er nøyaktig nok til å uttrykke posisjon, skalering, rotasjon og skjærstilling

Selve tekstobjektet opprettes med enhetsfontstørrelse, slik at dets ekte fontgrenser kan måles, og de målte objektgrensene kartlegges deretter på målfirkanten. Å dimensjonere med en gjettet punktstørrelse og håpe den matcher det skannede ordet, ville drevet av gårde med hver fonterstatning; å måle først gjør tilpasningen uavhengig av hvilken font laget bruker

Kjøre det over et dokument

Alternativposten styrer oppløsning, filtrering og hvert budsjett. Konfidensfiltrering betyr mer enn det ser ut til: søppelord med lav konfidens forurenser søkeresultater permanent, og i motsetning til en feil gjengivelse legger ingen merke til det før et søk returnerer nonsens:

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;                  // gjenkjenningsoppløsning
    Options.MinConfidence := 0.60;       // slett usikre ord
    Options.SkipPagesWithText := True;   // la digitalt fødte sider være i fred
    Options.ContinueOnError := True;     // én dårlig side skal ikke stoppe jobben
    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 vektlegging i blandede arkiver. En PDF som allerede bærer ekte tekst, enten digitalt født eller tidligere behandlet, får et andre tekstlag hvis du kjører OCR over den blindt, og duplikatet gjør at uthenting returnerer hvert ord to ganger. Statusen per side, popsSkippedExistingText, forteller deg nøyaktig hvilke sider som ble latt være i fred

Budsjetter, kansellering og feilbegrensning

Hver mengde en fiendtlig eller bare enorm dokument kan blåse opp, har et tak: piksler per side og totalt, ord per side og totalt, og tegn per ord. Alle sjekkes før siden skrives, ikke etterpå, og pikselanslaget beregnes fra sidedimensjoner og DPI før noe bitmap blir allokert. Å heve DPI fra 150 til 300 firedobler minnebruken per side, så taket per side er parameteren å justere først når en batch-jobb begynner å feile på store formater

Kanselleringstokenet tråkler gjennom hele banen: progressiv gjengivelse, leverandørkallet og innsettingsløkken per ord. Det betyr at en bruker som kansellerer under gjenkjenning av en fil på 400 sider, stopper innenfor én side i stedet for ved slutten av dokumentet, og det samme tokenmønsteret brukt andre steder i komponenten, beskrevet i kansellerbar progressiv gjengivelse, gjelder her uendret

Feilbegrensning er per side. Biblioteket samler objekthåndtakene det satte inn på en side og kaller FPDFPage_GenerateContent én gang, etter at alle ordene er plassert. Hvis noe feiler underveis, enten en leverandørfeil eller et fontproblem, fjernes objektene som ble satt inn på den siden i motsatt rekkefølge, og sideinnholdet genereres på nytt, slik at en mislykket side går tilbake til sin opprinnelige tilstand i stedet for å beholde et halvt tekstlag. Dokumentløkken fortsetter eller stopper deretter i henhold til ContinueOnError, og den aktive siden gjenopprettes alltid

Verifisere at bildet virkelig var urørt

Den sterkeste sjekken tilgjengelig er også den enkleste: gjengi siden før og etter påføring av laget i samme størrelse og sammenlign bitmapene. De skal være identiske byte for byte, fordi usynlig tekst ikke tegner noe, og bildestrømmen aldri ble dekodet. Enhver forskjell betyr at noe annet enn tekstlaget endret siden

Etter det, verifiser tekstsiden ved å hente ut fra den behandlede filen og bekrefte at ordposisjonene lander på skanningen. Uthentingsbanen er den samme som beskrevet i uthenting av tekst fra PDF-dokumenter, og for en rask visuell sjekk av justering lar gjengivelse av sider til bilder, som i konvertering av PDF-sider til JPEG, deg legge ordbokser over skanningen

OCR-lagdeling, gjengivelse, uthenting og redigering kjører alle mot det samme dokumentobjektet i Delphi, C++Builder og Lazarus; hele API-overflaten er beskrevet på PDFium Component for Delphi-siden