Teknisk artikel

Extracting Text from PDF Files with PDFium Component in Delphi

Udtrækning af PDF-tekst virker simpelt, indtil du støder på et dokument, hvor tekstlaget mangler, er beskadiget eller er opdelt i snesevis af bittesmå tegnforløb uden nogen meningsfuld rækkefølge. PDFium Component giver dig to indgangspunkter: Character[]-arrayet for rå, indeksbaseret adgang til hver enkelt glyph på en side, og ReadablePageContent for en struktureret visning, der genskaber afsnit og overskrifter ud fra PDF'ens tag-træ eller heuristiske analyse. Ingen af dem er altid det rigtige valg, så det betyder noget at forstå, hvad hver af dem eksponerer

Åbning af dokumentet og fælden med lydløse fejl

TPdf åbner en fil ved at indstille FileName og slå Active := True til. Den afgørende detalje: Active := True udløser aldrig en undtagelse. Hvis filen mangler, er adgangskodebeskyttet eller beskadiget, fanger PDFium fejlen internt, og Active forbliver blot False. Det betyder, at enhver udtrækningsløkke skal beskytte sig mod dette:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Could not open PDF (damaged or wrong password)');
    Exit;
  end;
  // extraction follows here
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Adgangskodebeskyttede filer kræver, at Pdf.Password := '...' er sat, før Active := True. Der er ingen anden chance: Når Active fejler, lukker og genåbner du med den korrekte adgangskode

Side-for-side udtrækning med Character[]

Tilgangen på laveste niveau gennemgår hvert enkelt tegn på hver side. Indstil Pdf.PageNumber for at indlæse tekstlaget for den side, og gentag derefter over CharacterCount-poster ved hjælp af Character[]-egenskaben. To flag på hver post er værd at kontrollere: CharacterGenerated[i] markerer syntetiske glypher indsat af rendereren (f.eks. bløde bindestreger ved linjeskift), der ikke har nogen reel Unicode-værdi, og CharacterMapError[i] signalerer, at PDFium ikke kunne kortlægge glyphen til et kodepunkt, hvilket sker med skrifttypekodninger, der mangler en ToUnicode-tabel

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // normalize CR to LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

Resultatet is a flat string of Unicode code points in the order PDFium enumerates them, which is the order they appear in the content stream, not necessarily left-to-right reading order. For most Latin-script documents produced by standard office tools this is fine. For scanned PDFs that were OCR’d with unusual glyph sequences, or for right-to-left text, the ordering can be wrong. That is when ReadablePageContent becomes more useful

Struktureret udtrækning med ReadablePageContent

ReadablePageContent går et niveau op: Det returnerer en TPdfReadableContent-post, hvis Fragments-array bærer taggede indholdsfragmenter, hver med en Kind, der identificerer afsnit, overskrifter, listepunkter, tabelceller og så videre. Når PDF'en bærer et strukturelt træ (kontroller Pdf.IsTagged), er kilden rosStructure, og læsererækkefølgen er autoritativ. For utaggede filer falder PDFium tilbage på rosHeuristic, som grupperer tegn efter deres afgrænsningsrammer (bounding boxes) i sandsynlige læseenheder, men kan ikke garantere nøjagtighed

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

Hvis Content.Source = rosHeuristic, og dit output ser forvansket ud, blev dokumentets tekstlag sandsynligvis ikke skrevet med læserækkefølge for øje. På det tidspunkt er den eneste pålidelige løsning at eksportere igen fra kildeprogrammet med korrekt tag-mærkning eller at køre et efterbehandlingstrin, der sorterer tegnernes oprindelse efter Y og derefter X

Hvad CharacterOrigin og CharacterRectangle giver dig

Begge egenskaber returnerer placeringen af et tegn i siderummet (punkter, oprindelse i nederste venstre hjørne, Y stigende opad). CharacterOrigin[i] er glyph'ens grundlinje-forankringspunkt; CharacterRectangle[i] er den fulde afgrænsningsramme. Disse er byggestenene til alt ud over ren tekst: Registrering af spaltegrænser, gruppering af tegn i linjer ved at sammenligne Y-koordinater inden for en tolerance, eller opbygning af et hit-test-kort til tekstvalg i en fremviser. Hvis du har brug for at finde, hvilket tegn der sidder under et museklik, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) foretager dette opslag direkte uden, at du skal løbe rektangler igennem

Sådan får du lagt DLL'en på plads

PDFium Component uddelegerer al PDF-fortolkning til en indfødt DLL, enten pdfium32.dll eller pdfium64.dll afhængigt af din målplatform. Komponenten leveres med et CopyDlls.bat-script, der kopierer den rigtige fil til Windows-systemmappen. Det er nok at køre det som administrator én gang på en udviklingsmaskine; til distribution kopierer du i stedet DLL'en ved siden af programmets eksekverbare fil. De V8-aktiverede varianter (pdfium32v8.dll, pdfium64v8.dll) er væsentligt større og kun nødvendige, hvis dine PDF-filer indeholder JavaScript, der skal køres. Til ren tekstudtrækning er standardversionen det rigtige valg

Hvis DLL'en mangler under kørslen, vil Active := True fejle lydløst, ligesom det gør for en manglende fil, fordi komponenten fanger indlæsningsfejlen internt. Test altid på en ren maskine, før du distribuerer

Brug FontSize[] sammen med Character[] til layoutanalyse

Ud over ren tekst eksponerer API'en på tegnniveau FontSize[i], som returnerer den rendered punktstørrelse for hver glyph. Kombineret med CharacterOrigin[i] og CharacterRectangle[i] lader dette dig skelne brødtekst fra overskrifter uden at forlade dig på strukturens tag-træ. Et tegnforløb, hvor skriftstørrelsen springer op over en tærskel, er næsten helt sikkert en overskrift i et utagget dokument. Den samme teknik gælder for registrering af billedtekster (lille tekst under et billedes afgrænsningsramme) eller fodnoter (lille tekst nær bunden af siden). Intet af dette kræver rendering; alle tre egenskaber læser direkte fra det tekstlag, som PDFium bygger under Active := True

En enkelt nuance: FontSize[i] afspejler størrelsen, efter at sidens CTM (current transformation matrix) er anvendt, so a document where the author scaled the entire page will report sizes proportionally adjusted. Hvis du sammenligner størrelser på tværs af sider med forskellige sidedimensioner, skal du normalisere i forhold til hver sides MediaBox-højde før du træffer beslutninger om tærskelværdier

Skrivning af outputtet til en fil

Delphis TStringList håndterer UTF-8-output fejlfrit siden XE. Indstil WriteBOM := False, hvis du har brug for en fil uden BOM (mange efterfølgende systemer fejler på en foranstillet BOM):

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

For meget store dokumenter, hvor hukommelsen er en bekymring, skal du skrive direkte til en TStreamWriter med TEncoding.UTF8 inde i sideløkken i stedet for at samle alt i en liste først

API'erne Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent og CharacterIndexAtPos, der er vist her, er en del af PDFium Component til Delphi og C++Builder