Teknisk artikel

Strukturerad PDF-textextraktion i Delphi med PDFium VCL

PDFiumPas returnerar sidtext som en struktur snarare än en sträng. GetStructuredText producerar en TPdfStructuredTextPage som innehåller block, vart och ett innehållande rader, var och en innehållande stilade spann, med sidutrymmesgränser på varje nivå och källteckenindex bevarade så att vilket fragment som helst kan mappas tillbaka till den underliggande textsidan

Den platta strängextraktionen som de flesta kodbaser börjar med finns fortfarande kvar och är fortfarande korrekt för sitt syfte. Den slutar räcka till i det ögonblick du behöver veta vilka ord som var en rubrik, vilka som tillhörde vänsterspalten, eller var på sidan en träff faktiskt sitter

Varför är en platt sträng fel utdata för de flesta uppgifter?

Därför att frågorna folk ställer om extraherad text nästan aldrig är ”vilka tecken finns på den här sidan”. De är ”vad är titeln”, ”är det här en tabell”, ”hör det här stycket till avsnitt 4”, ”var ritar jag markeringen”. En enda sträng besvarar ingen av dem, och varje svar du rekonstruerar från den är en heuristik du nu äger

Tvåspaltiga layouter gör poängen konkret. Extrahera en tvåspaltig artikel som en sträng och, beroende på hur producenten skrev innehållsströmmen, kan du få spalt ett följt av spalt två, eller du kan få rad ett i spalt ett, rad ett i spalt två, rad två i spalt ett, och så vidare nerför sidan. Båda kommer ur en konform PDF. Ingendera är fel på formatnivå, eftersom PDF beskriver märken på en sida, inte en dokumentdisposition. En blockbaserad modell låter extraktorn fatta ordningsbeslutet explicit och tala om för dig vilket beslut den fattade

Innehållsordning eller fysisk layout?

TPdfStructuredTextOptions.ReadingOrder väljer mellan roContentOrder och roPhysicalLayout, och rätt svar beror på vad du litar mest på, producenten eller geometrin

Innehållsordning returnerar text i den sekvens innehållsströmmen ritar den. Det är snabbt, och för dokument genererade av en väluppfostrad producent vanligtvis den avsedda läsordningen. Fysisk layout ignorerar strömsekvensen och återskapar ordning från var tecknen faktiskt sitter, och klustrar dem till rader och sedan till spalter. Det är vad du vill ha för skannade-och-sedan-OCR-behandlade sidor, för utdata från verktyg som sänder ut text i teckensnittsordning snarare än läsordning, och för allt där det visuella resultatet är det enda du kan lita på

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfStructuredTextOptions;
  Page: TPdfStructuredTextPage;
  B, L: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'article.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;                     // 1-baserat

    Options := TPdfStructuredTextOptions.Default;
    Options.ReadingOrder := roPhysicalLayout;
    Options.IncludeFontInfo := True;
    Options.IncludeSemantics := True;
    Options.MaxCharacters := 200000;         // fail-closed-budget

    Page := Pdf.GetStructuredText(Options);

    for B := 0 to High(Page.Blocks) do
    begin
      if Page.Blocks[B].Kind = cfHeading then
        Emit(Format('H%d: %s',
          [Page.Blocks[B].HeadingLevel, Page.Blocks[B].Text]))
      else
        for L := 0 to High(Page.Blocks[B].Lines) do
          Emit(Page.Blocks[B].Lines[L].Text);
    end;
  finally
    Pdf.Free;
  end;
end;

Vad tillför taggning som geometri inte kan?

Avsikt. Med IncludeSemantics aktiverat bär block från en taggad PDF ett Kind hämtat från strukturträdet, så en rubrik är en rubrik för att producenten sade det, inte för att dess teckensnitt var större än genomsnittet. Sorterna täcker de former som spelar roll för återanvändning: cfParagraph, cfHeading med en HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure och den otaggade fallbacken cfPlain

Fältet Source registrerar varifrån varje klassificering kom, rosStructure för strukturträdet och rosHeuristic för slutledning, vilket är fältet att logga när du bestämmer hur mycket du ska lita på en extraktionspipeline över en dokumentmängd. Figurer är ett specialfall värt att känna till: för ett cfFigure-block kommer texten från den alternativa beskrivningen snarare än från några glyfer, eftersom en figur inte har några egna tecken. Omatchad alternativtext representeras ändå i stället för att kastas bort, vilket är det som låter en tillgänglighetsgranskning se att en beskrivning finns även när ingenting på sidan ritar den. Själva taggningsmodellen täcks i PDF/UA-strukturträdsvalidering

Spann bär stilen och proveniensen

Varje TPdfStructuredTextSpan håller sin text, sina sidutrymmesgränser, FontName, FontSize, FontWeight och Angle, plus SourceStartIndex och SourceCharacterCount. Spann bryts där stilen ändras, så en mening med tre fetstilta ord blir tre spann, och att återuppbygga betoning i HTML eller Markdown är en fråga om att läsa egenskaper snarare än att gissa utifrån teckensnittsnamn

De två källindexfälten är de som förvandlar extraktion till en funktion snarare än en rapport. De pekar tillbaka in i sidans teckensekvens, vilket betyder att ett block du matchade i en sökning kan omvandlas till teckennivåmarkeringsgeometri eller en markeringsrektangel utan en andra, annorlunda ordnad genomgång av texten; mekaniken beskrivs i visuell textradmarkering med teckenboxar. Fältet Angle spelar större roll än det ser ut: roterad text i en stämpel eller ett vattenmärke hamnar i samma koordinatrymd som brödtext, och en pipeline som ignorerar vinkel slår gladeligen ihop ett diagonalt ”DRAFT” mitt i ett stycke

Budget, och de två kvalitetsräknarna

MaxCharacters är en fail-closed-budget, inte en trunkeringsinställning: en sida som överskrider den stannar i stället för att tyst returnera en del av innehållet. På en opålitlig intagsväg är det beteendet du vill ha, eftersom en sida med en miljon tecken antingen är ett maskingenererat monster eller ett försök att göra din extraktor till den långsammaste delen av systemet

Två räknare på den returnerade sidan beskriver extraktionskvaliteten direkt. UnmappedCharacterCount räknar tecken utan någon användbar Unicode-mappning, vilket är det klassiska symptomet på ett subset-teckensnitt inbäddat utan en /ToUnicode-CMap; sådan text renderas perfekt och extraheras som ingenting användbart. GeometryFailureCount räknar tecken vars omslutande ruta inte kunde bestämmas, vilket försämrar den fysiska layoutordningen. Logga båda. En dokumentmängd där de talen konsekvent ligger nära noll kan indexeras med förtroende, och en där de inte gör det talar om för dig att vissa producenter i din pipeline behöver uppmärksamhet innan något nedströmsresultat är pålitligt

var
  Page: TPdfStructuredTextPage;
  B, S, L: Integer;
  Emphasised: Boolean;
begin
  Page := Pdf.GetStructuredText(Options);

  if Page.UnmappedCharacterCount > 0 then
    Log(Format('page %d: %d characters without a Unicode mapping',
      [Page.PageNumber, Page.UnmappedCharacterCount]));
  if Page.GeometryFailureCount > 0 then
    Log(Format('page %d: %d characters without geometry',
      [Page.PageNumber, Page.GeometryFailureCount]));

  for B := 0 to High(Page.Blocks) do
    for L := 0 to High(Page.Blocks[B].Lines) do
      for S := 0 to High(Page.Blocks[B].Lines[L].Spans) do
      begin
        Emphasised := Page.Blocks[B].Lines[L].Spans[S].FontWeight >= 600;
        AppendRun(Page.Blocks[B].Lines[L].Spans[S].Text, Emphasised,
          Page.Blocks[B].Lines[L].Spans[S].SourceStartIndex);
      end;
end;

Prestanda på verkliga sidor

Fysisk-layout-extraktion är det dyra läget, och implementationen är byggd för sidor som faktiskt är stora: teckenordning körs i O(n log n) i stället för genom upprepad skanning, rad- och spannbuffertar växer geometriskt i stället för att omallokeras per tecken, Unicode-text byggs i buffertar i stället för genom strängsammanslagning, och teckensnittsuppslag för intilliggande textobjekt cachas. Den kombinationen är det som håller en tät sida med 5 000 tecken förutsägbar i stället för kvadratisk

För ett sidantalstungt jobb lönar det sig ändå att välja det billigare läget där du kan. Använd roContentOrder med semantik aktiverad för taggade dokument du litar på, och reservera roPhysicalLayout för skannat och äldre material där geometri är den enda signalen. Om allt du behöver är en vanlig sträng är det enklare API:et som beskrivs i att extrahera text från PDF-dokument fortfarande den snabbare vägen, och när du behöver spåra text tillbaka till markerat-innehåll-identifierare täcker att läsa och skriva BDC och MCID markerat innehåll det lagret

Blockmodellen kartläggs också rent mot vad hämtningspipelines vill ha: en rubrik med sina stycken är en chunk med en titel, och gränserna låter ett citat peka på en plats på en sida snarare än på ett dokument. PDFiumPas är en Delphi- och Lazarus-komponent runt PDFium-motorn, dokumenterad med exempel på sidan för PDFium Delphi-komponent