Technický článek

Extrakce textu z PDF souborů pomocí PDFium Component v Delphi

Extrakce textu z PDF vypadá jednoduše, dokud nenarazíte na dokument, kde textová vrstva chybí, je poškozená nebo je rozdělena do desítek malých shluků znaků bez smysluplného pořadí. Komponenta PDFium Component vám poskytuje dva přístupové body: pole Character[] pro přímý indexovaný přístup ke každému glyfu na stránce a metodu ReadablePageContent pro strukturovaný pohled, který rekonstruuje odstavce a nadpisy na základě stromu značek (tagů) v PDF nebo heuristické analýzy. Ani jedna z možností není univerzálně nejlepší, proto je důležité pochopit, co přesně která z nich nabízí

Otevření dokumentu a past tichého selhání

Třída TPdf otevírá soubor nastavením vlastnosti FileName a přepnutím Active := True. Klíčový detail: přiřazení Active := True nikdy nevyvolává výjimku. Pokud soubor chybí, je chráněn heslem nebo je poškozen, knihovna PDFium zachytí chybu interně a vlastnost Active pouze zůstane na hodnotě False. To znamená, že každý cyklus extrakce se před tímto chováním musí chránit:

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;

U souborů chráněných heslem je nutné nastavit vlastnost Pdf.Password := '...' ještě před voláním Active := True. Druhá šance neexistuje: pokud Active selže, musíte objekt zavřít a otevřít znovu se správným heslem

Extrakce stránku po stránce pomocí Character[]

Nejnižší úroveň přístupu prochází každý znak na každé stránce. Nastavením Pdf.PageNumber načtete textovou vrstvu dané stránky a poté procházíte položky v rozsahu CharacterCount pomocí vlastnosti Character[]. U každé položky stojí za to zkontrolovat dva příznaky: CharacterGenerated[i] označuje syntetické glyfy vložené vykreslovacím jádrem (například měkké rozdělovníky na koncích řádků), které nemají reálnou hodnotu v Unicode, a CharacterMapError[i] signalizuje, že PDFium nedokázalo namapovat glyf na kódový bod, což se stává u kódování písem, kterým chybí tabulka ToUnicode

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;

Výsledkem je plochý řetězec kódových bodů Unicode v pořadí, v jakém je PDFium vyjmenovává, což odpovídá pořadí jejich zápisu v toku obsahu — nemusí to nutně odpovídat pořadí čtení zleva doprava. U většiny dokumentů psaných latinkou a vytvořených běžnými kancelářskými aplikacemi je to v pořádku. U naskenovaných PDF, která prošla OCR s neobvyklým řazením glyfů, nebo u textů psaných zprava doleva však může být pořadí chybné. V takových případech je vhodnější použít metodu ReadablePageContent

Strukturovaná extrakce pomocí ReadablePageContent

Metoda ReadablePageContent funguje o úroveň výše: vrací záznam typu TPdfReadableContent, jehož pole Fragments obsahuje označené fragmenty obsahu. Každý fragment má vlastnost Kind určující, zda jde o odstavec, nadpis, položku seznamu, buňku tabulky a podobně. Pokud PDF obsahuje strukturní strom (což ověříte přes Pdf.IsTagged), je zdrojem rosStructure a pořadí čtení je závazné. U neoznačených souborů se PDFium vrací k režimu rosHeuristic, který seskupuje znaky podle jejich ohraničujících rámečků (bounding boxes) do logických celků pro čtení, ale nemůže garantovat stoprocentní přesnost

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;

Pokud je Content.Source = rosHeuristic and váš výstup vypadá nesourodě, textová vrstva dokumentu pravděpodobně nebyla zapsána s ohledem na pořadí čtení. V takovém případě je jediným spolehlivým řešením buď opětovný export z původní aplikace s řádným strukturováním (taggováním), nebo krok postprocessingové úpravy, který seřadí souřadnice počátků znaků podle osy Y a následně podle osy X

Co vám poskytují CharacterOrigin a CharacterRectangle

Obě vlastnosti vracejí pozici znaku v souřadnicovém systému stránky (v bodech, počátek v levém dolním rohu, osa Y roste směrem nahoru). CharacterOrigin[i] představuje kotevní bod základní linie glyfu; CharacterRectangle[i] vrací celý ohraničující rámeček. Tyto prvky slouží jako základní stavební kameny pro cokoli pokročilejšího než jen prostý text: detekci hranic sloupců, seskupování znaků do řádků porovnáváním Y souřadnic v určité toleranci, nebo vytváření mapy zásahů (hit-test map) pro výběr textu v prohlížeči. Pokud potřebujete zjistit, který znak se nachází pod kliknutím myši, metoda CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) provede toto vyhledání přímo, aniž byste museli sami procházet souřadnice rámečků

Zajištění přítomnosti knihovny DLL

Komponenta PDFium Component deleguje veškerou analýzu PDF na nativní knihovnu DLL — buď pdfium32.dll, nebo pdfium64.dll v závislosti na vaší cílové platformě. Součástí komponenty je skript CopyDlls.bat, který zkopíruje správný soubor do systémového adresáře Windows. Spuštění tohoto skriptu s právy administrátora na vývojářském počítači plně postačuje; při nasazení (deploymentu) pak knihovnu DLL zkopírujete přímo vedle spustitelného souboru aplikace. Varianty s podporou V8 (pdfium32v8.dll, pdfium64v8.dll) jsou výrazně větší a jsou zapotřebí pouze tehdy, pokud vaše PDF obsahují JavaScript, který se musí spouštět. Pro čistou extrakci textu je standardní sestavení optimální volbou

Pokud knihovna DLL chybí za běhu programu, přiřazení Active := True selže tiše, stejně jako u chybějícího souboru, protože komponenta chybu načtení zachytává interně. Před odesláním aplikace zákazníkům ji vždy otestujte na čistém operačním systému

Použití FontSize[] s Character[] pro analýzu rozvržení

Kromě samotného textu zpřístupňuje znakové API také vlastnost FontSize[i], která vrací vykreslenou velikost v bodech pro každý glyf. V kombinaci s CharacterOrigin[i] a CharacterRectangle[i] vám to umožní rozlišit běžný text od nadpisů i bez přítomnosti strukturního stromu. Blok znaků, u kterého velikost písma překročí stanovenou mez, je v neoznačeném dokumentu téměř jistě nadpisem. Stejnou techniku lze použít k detekci popisků (malý text pod ohraničením obrázku) nebo poznámek pod čarou (malý text u dolního okraje stránky). Nic z toho nevyžaduje samotné vykreslování; všechny tři vlastnosti čtou data přímo z textové vrstvy, kterou PDFium sestaví během inicializace Active := True

Drobný detail: FontSize[i] vyjadřuje velikost po aplikování aktuální transformační matice stránky (CTM), takže dokument, u kterého autor změnil měřítko celé stránky, bude hlásit úměrně upravené velikosti. Pokud porovnáváte velikosti napříč stránkami s odlišnými rozměry, normalizujte hodnoty proti výšce MediaBoxu dané stránky předtím, než začnete vyhodnocovat limity

Zápis výstupu do souboru

Třída TStringList v Delphi zpracovává výstup v UTF-8 správně od verze XE. Pokud potřebujete soubor bez značky BOM (BOM-free), na které se mnoho navazujících zpracovatelských systémů zasekává, nastavte vlastnost WriteBOM := False:

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;

U velmi rozsáhlých dokumentů, kde záleží na spotřebě paměti, zapisujte data přímo pomocí TStreamWriter s kódováním TEncoding.UTF8 přímo uvnitř cyklu stránek, namísto shromažďování celého textu do seznamu v paměti

Zde popsaná rozhraní API Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent a CharacterIndexAtPos jsou součástí produktu PDFium Component pro Delphi a C++Builder