Technický článek

Strukturovaná extrakce textu PDF v Delphi s PDFium VCL

PDFiumPas vrací text stránky jako strukturu, ne jako řetězec. GetStructuredText vyprodukuje TPdfStructuredTextPage obsahující bloky, z nichž každý drží řádky a každý řádek drží stylované spany, s hranicemi v prostoru stránky na každé úrovni a zachovanými indexy zdrojových znaků, takže se libovolný fragment dá zpětně namapovat na podkladovou textovou stránku

Extrakce do plochého řetězce, se kterou většina kódu začíná, je tu pořád a pro svůj účel je pořád správná. Přestává stačit v okamžiku, kdy potřebujete vědět, která slova byla nadpisem, která patřila do levého sloupce, nebo kde na stránce se shoda vlastně nachází

Proč je plochý řetězec pro většinu úloh špatný výstup?

Protože otázky, které lidé kladou nad extrahovaným textem, téměř nikdy nejsou „jaké znaky jsou na této stránce". Jsou to „jaký je titulek", „je tohle tabulka", „patří tento odstavec do kapitoly 4", „kam mám nakreslit zvýraznění". Jediný řetězec neodpoví na žádnou z nich, a každá odpověď, kterou z něj rekonstruujete, je heuristika, kterou si teď musíte udržovat sami

Dvousloupcová rozvržení tuto myšlenku konkretizují. Extrahujete-li dvousloupcový článek jako řetězec, podle toho, jak producent zapsal obsahový proud, můžete dostat sloupec jedna následovaný sloupcem dva, nebo řádek jedna sloupce jedna, řádek jedna sloupce dva, řádek dva sloupce jedna a tak dál po celé stránce. Obojí může vzejít z vyhovujícího PDF. Ani jedno není na úrovni formátu špatně, protože PDF popisuje značky na stránce, ne osnovu dokumentu. Model založený na blocích nechává extraktor učinit rozhodnutí o pořadí explicitně a sdělit vám, jaké rozhodnutí učinil

Pořadí obsahu, nebo fyzické rozvržení?

TPdfStructuredTextOptions.ReadingOrder volí mezi roContentOrder a roPhysicalLayout, a správná odpověď závisí na tom, čemu důvěřujete víc – producentovi, nebo geometrii

Pořadí obsahu vrací text v sekvenci, ve které ho kreslí obsahový proud. To je rychlé a u dokumentů vygenerovaných slušně se chovajícím producentem obvykle odpovídá zamýšlenému pořadí čtení. Fyzické rozvržení ignoruje sekvenci proudu a rekonstruuje pořadí z toho, kde znaky skutečně sedí, a shlukuje je do řádků a pak do sloupců. To je to, co chcete pro naskenované a poté OCR zpracované stránky, pro výstup z nástrojů, které vydávají text v pořadí písem místo v pořadí čtení, a pro cokoli, kde je jediné, na co se můžete spolehnout, vizuální výsledek

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;                     // číslováno od 1

    Options := TPdfStructuredTextOptions.Default;
    Options.ReadingOrder := roPhysicalLayout;
    Options.IncludeFontInfo := True;
    Options.IncludeSemantics := True;
    Options.MaxCharacters := 200000;         // uzavřený rozpočet

    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;

Co značkování přidává, co geometrie nedokáže?

Záměr. Se zapnutým IncludeSemantics nesou bloky ze značkovaného PDF Kind odvozený ze stromu struktury, takže nadpis je nadpisem proto, že to řekl producent, ne proto, že jeho písmo bylo větší než průměr. Druhy pokrývají tvary, na kterých záleží pro opakované využití: cfParagraph, cfHeading s HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure a neznačkovaný záložní cfPlain

Pole Source zaznamenává, odkud každá klasifikace pochází, rosStructure pro strom struktury a rosHeuristic pro odvození, a je to pole, které se vyplatí logovat, když se rozhodujete, jak moc důvěřovat extrakční pipeline napříč sadou dokumentů. Obrázky jsou zvláštní případ, který stojí za povšimnutí: u bloku cfFigure pochází text z alternativního popisu, ne z jakýchkoli glyfů, protože obrázek nemá vlastní znaky. Nepřiřazený alternativní text se pořád zastupuje místo toho, aby se zahodil, a právě to umožňuje auditu přístupnosti vidět, že popis existuje, i když ho na stránce nic nevykresluje. Samotný model značkování popisuje článek o validaci stromu struktury PDF/UA

Spany nesou styl i původ

Každý TPdfStructuredTextSpan drží svůj text, hranice v prostoru stránky, FontName, FontSize, FontWeight a Angle, plus SourceStartIndex a SourceCharacterCount. Spany se lámou tam, kde se mění styl, takže věta se třemi tučnými slovy se stane třemi spany, a rekonstrukce zvýraznění v HTML nebo Markdown je otázkou čtení vlastností, ne hádání z názvů písem

Ta dvě pole zdrojového indexu jsou to, co mění extrakci z reportu na funkci. Ukazují zpátky do sekvence znaků stránky, což znamená, že blok, který jste našli při vyhledávání, lze převést na geometrii výběru na úrovni znaků nebo na obdélník zvýraznění bez druhého, jinak seřazeného průchodu textem; mechanismus popisuje článek o vizuálním výběru textového řádku pomocí znakových boxů. Pole Angle má větší význam, než vypadá: otočený text v razítku nebo vodoznaku přistane ve stejném souřadnicovém prostoru jako text těla, a pipeline, která úhel ignoruje, ochotně sloučí diagonální nápis „DRAFT" doprostřed odstavce

Rozpočet a dva čítače kvality

MaxCharacters je uzavřený rozpočet, ne nastavení oříznutí: stránka, která ho překročí, se zastaví, místo aby tiše vrátila jen část obsahu. Na nedůvěryhodné vstupní cestě je to chování, které chcete, protože stránka s milionem znaků je buď strojově generovaná obluda, nebo pokus proměnit váš extraktor v nejpomalejší část systému

Dva čítače na vrácené stránce popisují kvalitu extrakce přímo. UnmappedCharacterCount počítá znaky bez použitelného mapování Unicode, což je klasický příznak podmnožinového písma vloženého bez CMap /ToUnicode; takový text se vykreslí dokonale a extrahuje se jako nic užitečného. GeometryFailureCount počítá znaky, jejichž ohraničující box se nepodařilo určit, což zhoršuje pořadí u fyzického rozvržení. Logujte oba. Sadu dokumentů, kde jsou tato čísla trvale blízko nule, lze indexovat s důvěrou, a sada, kde tomu tak není, vám říká, že někteří producenti ve vaší pipeline potřebují pozornost dřív, než bude jakýkoli navazující výsledek důvěryhodný

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;

Výkon na reálných stránkách

Extrakce s fyzickým rozvržením je nákladný režim, a implementace je postavená pro stránky, které jsou skutečně velké: řazení znaků běží v O(n log n) místo opakovaného skenování, buffery řádků a spanů rostou geometricky místo realokace po jednotlivých znacích, unikódový text se sestavuje v bufferech místo řetězení řetězců a vyhledávání písem pro sousedící textové objekty se cachuje. Tato kombinace je to, co udržuje hustou stránku s 5000 znaky předvídatelnou místo kvadratické

U úlohy s velkým počtem stránek se pořád vyplatí volit levnější režim, kde to jde. Použijte roContentOrder se zapnutou sémantikou pro značkované dokumenty, kterým důvěřujete, a roPhysicalLayout si nechte pro skenovaný a starší materiál, kde je geometrie jediným signálem. Pokud potřebujete jen obyčejný řetězec, jednodušší API popsané v článku o extrakci textu z dokumentů PDF zůstává rychlejší cestou, a když potřebujete text vysledovat zpátky k identifikátorům označeného obsahu, tuto vrstvu pokrývá článek o čtení a zápisu označeného obsahu BDC a MCID

Model založený na blocích se také čistě mapuje na to, co chtějí pipeline pro vyhledávání: nadpis se svými odstavci je blok s titulkem, a hranice umožňují, aby citace ukazovala na místo na stránce, ne na celý dokument. PDFiumPas je komponenta pro Delphi a Lazarus postavená kolem enginu PDFium, zdokumentovaná s příklady na stránce PDFium Delphi component