Teknisk artikel

Lägg till ett sökbart textlager i skannade PDF-filer i Delphi

PDFium Component lägger till ett sökbart textlager på skannade PDF-sidor från Delphi via ApplyOcrSearchLayer. Den renderar varje vald sida, lämnar pixlarna till en OCR-leverantör du själv tillhandahåller, och skriver tillbaka de igenkända orden som osynliga textobjekt placerade ovanpå orden i skanningen. Den ursprungliga sidbilden avkodas, omkodas eller ersätts aldrig, så det visuella resultatet är byte för byte samma sida du började med

Igenkänningsmotorn är medvetet inte en del av biblioteket. PDFium exponerar siddrendering, koordinatmappning, typsnittsinläsning, textobjektsskapande och osynliga renderingslägen, men det innehåller ingen OCR-motor, och att låtsas annat skulle innebära att bunta in någon annans igenkänningsprodukt i en PDF-komponent. Istället bor igenkänningen bakom gränssnittet IPdfOcrProvider: biblioteket skickar fastlayoutade, toppursprungliga BGRA-pixlar, och leverantören returnerar Unicode-text, konfidensvärden och ordfyrhörningar

Vad är egentligen ett sökbart textlager?

En skannad PDF är en bild av ett dokument. Sidinnehållet är en enda stor bild, och det finns inget att markera, söka i, kopiera eller indexera. Ett sökbart textlager lägger till riktiga textobjekt ovanpå den bilden med renderingsläget satt till osynligt, så att visare inte ritar något men markering, sökning och extraktion hittar orden exakt där de förekommer

Positionering är hela spelet. Om den osynliga texten sitter några punkter fel hamnar markeringshöjdpunkter bredvid orden istället för på dem, och att kopiera ett stycke producerar text i fel ordning. Det är därför geometrin måste komma från samma transformer som PDFium använder för att rendera sidan snarare än från en proportionell gissning

Att implementera leverantören

Leverantörskontraktet är en enda metod. Den tar emot en sidbildpost som bär dimensioner, stride, DPI, pixelformat och själva pixelbyten, plus en avbrytningstoken, och returnerar ord eller ett felmeddelande:

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 lagrar toppursprungliga BGRA-rader om Image.Stride byte.
  // Skicka dem till din motor, fyll sedan en post per igenkänt 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;

Fyrhörningar snarare än rektanglar, eftersom en skanning sällan är fyrkantig mot sidan. Ett ord på en lätt roterad sida upptar en parallellogram, och TPdfOcrQuad bär fyra hörnpunkter så att skevade och roterade ord behåller en korrekt markeringsregion. Motorer som bara rapporterar axeljusterade rutor kan använda FromRectangle, som bygger den degenererade fyrhörningen

Varför kan ordpositioner inte skalas proportionellt?

Det är frestande att konvertera en pixelkoordinat till en sidkoordinat genom att dela med renderingsbredden och multiplicera med sidbredden. Det fungerar bara för sidor utan rotation, en CropBox identisk med MediaBox, och ett ursprung vid noll, och gott om skannade dokument misslyckas med minst ett av dessa villkor

PDFium Component mappar var och en av de fyra fyrhörningshörnen individuellt genom FPDF_DeviceToPage, samma mappning renderaren använde för att producera pixlarna, så /Rotate-poster och förskjutna beskärningsrutor hanteras genom konstruktion. Den affina matrisen för textobjektet byggs sedan från tre av de mappade punkterna, hörnen nedre vänster, nedre höger och övre vänster, vilket är precis tillräckligt för att uttrycka position, skala, rotation och skevning

Textobjektet självt skapas i enhetsteckenstorlek så att dess verkliga typsnittsgränser kan mätas, och de uppmätta objektgränserna mappas sedan mot målfyrhörningen. Att storleksbestämma efter en gissad punktstorlek och hoppas att den matchar det skannade ordet skulle driva iväg med varje typsnittsersättning; att mäta först gör passformen oberoende av vilket typsnitt lagret använder

Att köra det över ett dokument

Alternativposten styr upplösning, filtrering och varje budget. Konfidensfiltrering spelar större roll än det verkar: skräpord med låg konfidens förorenar sökresultat permanent, och till skillnad från en felaktig rendering märker ingen det förrän en sökning returnerar 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;                  // igenkänningsupplösning
    Options.MinConfidence := 0.60;       // släng osäkra ord
    Options.SkipPagesWithText := True;   // lämna digitalfödda sidor ifred
    Options.ContinueOnError := True;     // en dålig sida får inte stoppa 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 förtjänar betoning i blandade arkiv. En PDF som redan bär riktig text, oavsett om den är digitalfödd eller tidigare bearbetad, får ett andra textlager om du kör OCR över den blint, och dupliceringen gör att extraktion returnerar varje ord två gånger. Sidstatusen popsSkippedExistingText talar exakt om vilka sidor som lämnades ifred

Budgetar, avbrytning och felinneslutning

Varje kvantitet som ett fientligt eller bara enormt dokument kan blåsa upp har ett tak: pixlar per sida och totalt, ord per sida och totalt, samt tecken per ord. Alla kontrolleras innan sidan skrivs, inte efteråt, och pixeluppskattningen beräknas från sidans dimensioner och DPI innan någon bitmapp allokeras. Att höja DPI från 150 till 300 fyrdubblar minnet per sida, så taket per sida är den parameter att justera först när ett batchjobb börjar misslyckas på stora format

Avbrytningstoken löper genom hela vägen: progressiv rendering, leverantörsanropet och infogningsloopen per ord. Det betyder att en användare som avbryter under igenkänning av en 400-sidig fil stannar inom en sida snarare än vid slutet av dokumentet, och samma tokenmönster som används på andra ställen i komponenten, beskrivet i avbrytningsbar progressiv rendering, gäller här oförändrat

Felinneslutning sker per sida. Biblioteket samlar de objekthandtag det infogade på en sida och anropar FPDFPage_GenerateContent en gång, efter att alla ord placerats. Om något misslyckas mitt i, oavsett om det är ett leverantörsfel eller ett typsnittsproblem, tas de objekt som infogats på den sidan bort i omvänd ordning och sidinnehållet genereras om, så att en misslyckad sida återgår till sitt ursprungliga tillstånd istället för att behålla ett halvfärdigt textlager. Dokumentloopen fortsätter eller stoppar sedan enligt ContinueOnError, och den aktiva sidan återställs alltid

Att verifiera att bilden verkligen förblev orörd

Den starkaste tillgängliga kontrollen är också den enklaste: rendera sidan före och efter att lagret applicerats i samma storlek och jämför bitmapparna. De ska vara identiska byte för byte, eftersom osynlig text inte ritar något och bildströmmen aldrig avkodades. Varje skillnad betyder att något annat än textlagret ändrade sidan

Verifiera därefter textsidan genom att extrahera från den bearbetade filen och bekräfta att ordpositioner hamnar på skanningen. Extraktionsvägen är samma som beskrivs i extrahering av text från PDF-dokument, och för en snabb visuell kontroll av inpassning låter rendering av sidor till bilder som i konvertering av PDF-sidor till JPEG dig lägga ordrutor ovanpå skanningen

OCR-lagring, rendering, extraktion och redigering körs alla mot samma dokumentobjekt i Delphi, C++Builder och Lazarus; hela API-ytan beskrivs på sidan för PDFium Component för Delphi