Teknisk artikel

Extrahera text från PDF-filer med PDFium Component i Delphi

Textextrahering från PDF ser enkelt ut tills du stöter på ett dokument där textlagret saknas, är korrupt eller är uppdelat över dussintals små teckenkörningar utan någon meningsfull ordning. PDFium Component ger dig två ingångar: vektorn Character[] för rå, indexbaserad åtkomst till varje glyf på en sida, och ReadablePageContent för en strukturerad vy som återskapar stycken och rubriker från PDF:ens taggträd eller heuristisk analys. Ingen av dem är alltid rätt val, så det är viktigt att förstå vad var och en av dem exponerar

Öppna dokumentet och det tysta fel-skyddets fälla

TPdf öppnar en fil genom att sätta FileName och växla Active := True. Den kritiska detaljen: Active := True kastar aldrig ett undantag. Om filen saknas, är lösenordsskyddad eller korrupt, fångar PDFium felet internt och Active förblir helt enkelt False. Det betyder att varje extraheringsslinga måste skydda mot detta:

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;

Lösenordsskyddade filer behöver Pdf.Password := '...' sättas innan Active := True. Det finns ingen andra chans: när Active misslyckas, stänger du och öppnar igen med rätt lösenord

Sida-för-sida extrahering med Character[]

Den lägsta nivån går igenom varje tecken på varje sida. Sätt Pdf.PageNumber för att ladda textlagret för den sidan, iterera sedan CharacterCount poster med hjälp av egenskapen Character[]. Det finns två flaggor på varje post som är värda att kontrollera: CharacterGenerated[i] markerar syntetiska glyfer insatta av renderaren (mjuka bindestreck vid radbrytningar, till exempel) som inte har något verkligt Unicode-värde, och CharacterMapError[i] signalerar att PDFium inte kunde mappa glyfen till en kodpunkt, vilket händer med teckensnittskodningar som saknar 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 är en platt sträng av Unicode-kodpunkter i den ordning PDFium räknar upp dem, vilket är den ordning de visas i innehållsströmmen, inte nödvändigtvis i läsordning från vänster till höger. För de flesta dokument med latinsk skrift producerade av standardkontorsverktyg är detta okej. För skannade PDF-filer som har OCR-behandlats med ovanliga glyf-sekvenser, eller för höger-till-vänster-text, kan ordningen vara fel. Det är då ReadablePageContent blir mer användbar

Strukturerad extrahering med ReadablePageContent

ReadablePageContent går en nivå upp: den returnerar en TPdfReadableContent-post vars Fragments-vektor innehåller taggade innehållsfragment, var och en med en Kind som identifierar stycken, rubriker, listobjekt, tabellceller och så vidare. När PDF:en har ett strukturträd (kontrollera Pdf.IsTagged), är källan rosStructure och läsordningen är auktoritativ. För otaggade filer faller PDFium tillbaka till rosHeuristic, vilket grupperar tecken efter deras avgränsningsrutor till rimliga läsenheter men kan inte garantera korrekthet

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;

Om Content.Source = rosHeuristic och din utdata ser förvanskad ut, skrevs dokumentets textlager förebyggande inte med läsordning i åtanke. Vid den punkten är den enda pålitliga lösningen att exportera om från källapplikationen med korrekt taggning, eller köra ett efterbearbetningssteg som sorterar tecknens ursprung efter Y och sedan X

Vad CharacterOrigin och CharacterRectangle ger dig

Båda egenskaperna returnerar positionen för ett tecken i sidrymden (punkter, med ursprung i det nedre vänstra hörnet, Y ökar uppåt). CharacterOrigin[i] är glyfens baslinje-ankarpunkt; CharacterRectangle[i] är hela avgränsningsrutan. Dessa är byggstenarna för allt utöver vanlig text: upptäcka kolumngränser, gruppera tecken i rader genom att jämföra Y-koordinater inom en tolerans, eller bygga en träfftestkarta för textval i en visare. Om du behöver hitta vilket tecken som sitter under ett musklick, gör CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) den sökningen direkt utan att du behöver iterera rektanglar

Få DLL:en på plats

PDFium Component delegerar all PDF-tolkning till en inbyggd DLL, antingen pdfium32.dll eller pdfium64.dll beroende på din målplattform. Komponenten levereras med ett CopyDlls.bat-skript som kopierar rätt fil till Windows systemkatalog. Att köra det som administratör en gång på en utvecklingsmaskin räcker; för distribution kopierar du DLL:en vid sidan av applikationens körbara fil istället. De V8-aktiverade varianterna (pdfium32v8.dll, pdfium64v8.dll) är betydligt större och är endast nödvändiga om dina PDF-filer innehåller JavaScript som måste köras. För ren textextrahering är standardbygget det rätta valet

Om DLL:en saknas vid körning kommer Active := True att misslyckas tyst precis som det gör för en saknad fil, eftersom komponenten fångar laddningsfelet internt. Testa alltid på en ren maaskin innan leverans

Använda FontSize[] tillsammans med Character[] för layoutanalys

Utöver vanlig text exponerar API:et på teckennivå FontSize[i], vilket returnerar den renderade punktstorleken för varje glyf. I kombination med CharacterOrigin[i] och CharacterRectangle[i] låter detta dig skilja brödtext från rubriker utan att förlita dig på strukturträdet. En teckenkörning där teckenstorleken hoppar över ett tröskelvärde är med största sannolikhet en rubrik i ett otaggat dokument. Samma teknik gäller för att upptäcka bildtexter (liten text under en bilds avgränsningsruta) eller fotnoter (liten text nära sidans botten). Inget av detta kräver rendering; alla tre egenskaperna läser direkt från textlagret som PDFium bygger under Active := True

En nyansering: FontSize[i] återspeglar storleken efter att sidans CTM (aktuell transformationsmatris) har applicerats, så ett dokument där författaren skalade hela sidan kommer att rapportera storlekar som är proportionellt justerade. Om du jämför storlekar över sidor med olika sidodimensioner, normalisera mot varje sidas MediaBox-höjd innan du fattar tröskelbeslut

Skriva utdata till en fil

Delphis TStringList hanterar UTF-8-utdata rent sedan XE. Sätt WriteBOM := False om du behöver en BOM-fri fil (många nedströms konsumenter kvävs på en inledande 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;

För mycket stora dokument där minnet är ett bekymmer, skriv direkt till en TStreamWriter med TEncoding.UTF8 inuti sidloopen istället för att ackumulera allt i en lista först

API:erna Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent och CharacterIndexAtPos som visas här är en del av PDFium Component för Delphi och C++Builder