Teknisk artikkel

Trekke ut tekst fra PDF-filer med PDFium-komponent i Delphi

Å trekke ut tekst fra PDF ser enkelt ut til du treffer et dokument der tekstlaget er fraværende, korrupt eller delt opp over dusinvis av små tegnkjøringer (character runs) uten meningsfull rekkefølge. PDFium-komponenten gir deg to inngangspunkter: Character[]-arrayen for rå, indeksbasert tilgang to hver glyf på en side, og ReadablePageContent for en strukturert visning som rekonstruerer avsnitt og overskrifter fra PDF-ens tagg-tre (tag tree) eller heuristisk analyse. Ingen av dem er alltid det riktige valget, så det er viktig å forstå hva hver av dem synliggjør

Åpne dokumentet og fellen med stille feil

TPdf åpner en fil ved å sette FileName og vippe (flip) Active := True. Den kritiske detaljen: Active := True hever aldri et unntak. Hvis filen mangler, er passordbeskyttet eller korrupt, fanger PDFium feilen internt og Active forbrir rett og slett False. Det betyr at hver utvinningsløkke må vokte mot 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;

Passordbeskyttede filer krever at Pdf.Password := '...' settes før Active := True. Det er ingen ny sjanse: når Active feiler, lukker du og åpner på nytt med riktig passord

Side-for-side utvinning med Character[]

Tilnærmingen på laveste nivå går gjennom hvert tegn på hver side. Sett Pdf.PageNumber for å laste tekstlaget for den siden, og iterer deretter CharacterCount-oppføringer ved å bruke Character[]-egenskapen. To flagg på hver oppføring er verdt å sjekke: CharacterGenerated[i] markerer syntetiske glyfer satt inn av gjengiveren (myke bindestreker ved linjeskift, for eksempel) som ikke har noen reell Unicode-verdi, og CharacterMapError[i] signaliserer at PDFium ikke kunne tilordne glyfen til et kodepunkt, noe som skjer med skrifttypekodinger som mangler en ToUnicode-tabell

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 er en flat streng med Unicode-kodepunkter i den rekkefølgen PDFium lister dem opp, som er den rekkefølgen de vises i innholdsstrømmen, ikke nødvendigvis lese-rekkefølge fra venstre mot høyre. For de fleste latin-skrift dokumenter produsert av standard kontorverktøy er dette greit. For skannede PDF-er som ble OCR-behandlet med uvanlige glyf-sekvenser, eller for høyre-til-venstre tekst, kan rekkefølgen være feil. Det er da ReadablePageContent blir mer nyttig

Strukturert utvinning med ReadablePageContent

ReadablePageContent går ett nivå opp: den returnerer en TPdfReadableContent-post hvis Fragments-array bærer taggede innholdsfragmenter, hver med en Kind som identifiserer avsnitt, overskrifter, listeelementer, tabellceller og så videre. Når PDF-en bærer et strukturtre (sjekk Pdf.IsTagged), er kilden rosStructure og leserekkefølgen er autoritativ. For utaggede filer faller PDFium tilbake til rosHeuristic, som grupperer tegn etter deres avgrensende bokser (bounding boxes) inn i plausible leseenheter, men kan ikke garantere nøyaktighet

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 utdataene dine ser forvrengt ut, ble dokumentets tekstlag sannsynligvis ikke skrevet med leserekkefølge i tankene. På det punktet er den eneste pålitelige løsningen å eksportere på nytt fra kildeapplikasjonen med opprinnelig tagging, eller å kjøre et etterbehandlingstrinn som sorterer tegnopprinnelser etter Y og deretter X

Hva CharacterOrigin og CharacterRectangle gir deg

Begge egenskapene returnerer posisjonen til et tegn i siderommet (punkter, opprinnelse i nederste venstre hjørne, Y øker oppover). CharacterOrigin[i] is glyfens grunnlinjeankerpunkt; CharacterRectangle[i] er den fulle avgrensende boksen. Dette er byggeklossene for alt utover ren tekst: oppdage kolonnegrenser, gruppere tegn i linjer ved å sammenligne Y-koordinater innenfor en toleranse, eller bygge et treff-test kart (hit-test map) for tekstutvalg i et visningsprogram. Hvis du trenger å finne hvilket tegn som sitter under et museklikk, gjør CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) det oppslaget direkte uten at du må iterere rektangler

Få DLL-en på plass

PDFium-komponenten delegerer all PDF-parsing til en opprinnelig (native) DLL, enten pdfium32.dll eller pdfium64.dll avhengig av målplattformen din. Komponenten leveres med et CopyDlls.bat-skript som kopierer den riktige filen til Windows-systemkatalogen. Å kjøre det som Administrator én gang på en utviklingsmaskin er nok; for utrulling kopierer du DLL-en sammen med applikasjonens kjørbare fil i stedet. De V8-aktiverte variantene (pdfium32v8.dll, pdfium64v8.dll) er betydelig større og kun nødvendige hvis PDF-ene dine inneholder JavaScript som må kjøres. For ren tekstutvinning er standardbygget det riktige valget

If DLL-en er fraværende under kjøring (runtime), vil Active := True feile i det stille akkurat som det gjør for en manglende fil, fordi komponenten fanger lastefeilen internt. Test alltid på en ren maskin før utgivelse

Bruke FontSize[] sammen med Character[] for layout-analyse

Utover ren tekst, eksponerer tegn-nivå API-et FontSize[i], som returnerer den gjengitte punktstørrelsen til hver glyf. Kombinert med CharacterOrigin[i] og CharacterRectangle[i], lar dette deg skille brødtekst fra overskrifter uten å stole på strukturtreet. En tegnkjøring (character run) der skrifttypestørrelsen hopper over en terskel, er nesten helt sikkert en overskrift i et utagget dokument. Den samme teknikken gjelder for å oppdage bildetekster (liten tekst under en bildes avgrensende boks) eller fotnoter (liten tekst nær bunnen av siden). Ingenting av dette krever gjengivelse; alle tre egenskapene leser direkte fra tekstlaget som PDFium bygger under Active := True

En nyanse: FontSize[i] reflekterer størrelsen etter at sidens CTM (current transformation matrix) er påført, så et dokument der forfatteren skalerte hele siden vil rapportere størrelser proporsjonalt justert. Hvis du sammenligner størrelser på tvers av sider med forskjellige sidedimensjoner, normaliser mot hver sides MediaBox-høyde før du tar terskelbeslutninger

Skrive utdataene til en fil

Delphis TStringList håndterer UTF-8-utdata rent siden XE. Sett WriteBOM := False hvis du trenger en BOM-fri fil (mange nedstrøms konsumenter kveles av en ledende 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 svært store dokumenter der minnet er en bekymring, skriv direkte til en TStreamWriter med TEncoding.UTF8 inni sideløkken i stedet for å akkumulere alt til en liste først

Character[]-, CharacterCount-, CharacterOrigin[]-, CharacterRectangle[]-, ReadablePageContent- og CharacterIndexAtPos-API-ene vist her er en del av PDFium-komponenten for Delphi og C++Builder