Odborný článok

Extrakcia textu z PDF súborov v Delphi pomocou PDFium Component

Extrakcia textu z PDF vyzerá jednoducho, kým nenarazíte na dokument, v ktorom textová vrstva chýba, je poškodená alebo rozdelená do desiatok malých sekvencií znakov bez akéhokoľvek zmysluplného poradia. PDFium Component vám ponúka dva vstupné body: pole Character[] pre priamy prístup k jednotlivým glyfom na stránke na základe indexu, a funkciu ReadablePageContent pre štruktúrovaný pohľad, ktorý rekonštruuje odseky a nadpisy zo stromu tagov PDF alebo na základe heuristickej analýzy. Žiadna z týchto možností nie je univerzálne správna, preto je dôležité rozumieť tomu, čo každá z nich ponúka

Otvorenie dokumentu a pasca tichého zlyhania

Trieda TPdf otvára súbor nastavením vlastnosti FileName a prepnutím Active := True. Kľúčový detail: nastavenie Active := True nikdy nevyvolá výnimku. Ak súbor chýba, je chránený heslom alebo poškodený, PDFium chybu interne zachytí a Active jednoducho zostane na hodnote False. To znamená, že každý cyklus extrakcie sa musí pred týmto scenárom chrániť:

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;

Súbory chránené heslom vyžadujú nastavenie Pdf.Password := '...' pred prepnutím Active := True. Druhá šanca neexistuje: ak nastavenie Active zlyhá, musíte súbor zavrieť a znova otvoriť so správnym heslom

Extrakcia po stránkach pomocou Character[]

Najnižšia úroveň prístupu prechádza každý znak na každej stránke. Nastavením vlastnosti Pdf.PageNumber načítaš textovú vrstvu pre danú stránku a potom prechádzaš položky CharacterCount pomocou vlastnosti Character[]. Pri každej položke sa oplatí skontrolovať dva príznaky: CharacterGenerated[i] označuje syntetické glyfy vložené renderovacím enginom (napríklad voliteľné rozdeľovníky na konci riadkov), ktoré nemajú reálnu Unicode hodnotu, a CharacterMapError[i] signalizuje, že PDFium nedokázalo priradiť glyf k znakovému kódu, čo sa stáva pri kódovaniach písiem, ktorým chýba tabuľka 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ýsledkom je jednoduchý reťazec znakov Unicode v poradí, v akom ich PDFium číta, čo zodpovedá ich poradiu v dátovom prúde obsahu a nemusí nutne ísť o poradie čítania zľava doprava. Pri väčšine dokumentov v latinke vytvorených bežnými kancelárskymi nástrojmi je to v poriadku. Pri naskenovaných PDF, ktoré prešli OCR s neštandardnými sekvenciami glyfov, alebo pri texte písanom sprava doľava, môže byť toto poradie nesprávne. V takýchto prípadoch je oveľa užitočnejšia funkcia ReadablePageContent

Štruktúrovaná extrakcia pomocou ReadablePageContent

Funkcia ReadablePageContent stojí o úroveň vyššie: vracia záznam typu TPdfReadableContent, ktorého pole Fragments obsahuje označené fragmenty obsahu. Každý fragment má vlastnosť Kind, ktorá identifikuje odseky, nadpisy, položky zoznamov, bunky tabuliek a podobne. Keď PDF obsahuje strom štruktúry (čo overíte vlastnosťou Pdf.IsTagged), zdrojom je rosStructure a poradie čítania je smerodajné. Pri neoznačených súboroch PDFium prechádza na heuristický režim rosHeuristic, ktorý zoskupuje znaky podľa ich ohraničujúcich rámcov (bounding boxes) do logických čítacích jednotiek, no nemôže garantovať presnosť

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;

Ak platí Content.Source = rosHeuristic a váš výstup vyzerá pomiešane, textová vrstva dokumentu pravdepodobne nebola zapísaná so zreteľom na poradie čítania. V takom prípade je jediným spoľahlivým riešením opätovný export zo zdrojovej aplikácie s riadnym označením štruktúry, alebo zaradenie kroku dodatočného spracovania, ktorý usporiada súradnice počiatkov znakov najprv podľa osi Y a potom podľa osi X

Čo vám poskytujú vlastnosti CharacterOrigin a CharacterRectangle

Obe vlastnosti vracajú pozíciu znaku v súradnicovom systéme stránky (v bodoch, s počiatkom v ľavom dolnom rohu, kde os Y rastie smerom nahor). CharacterOrigin[i] predstavuje kotevný bod na základnej linke (baseline) glyfu; CharacterRectangle[i] vracia celý ohraničujúci rámec. Tieto údaje sú stavebnými kameňmi pre čokoľvek, čo presahuje čistý text: detekciu hraníc stĺpcov, zoskupovanie znakov do riadkov porovnávaním súradníc Y s určitou toleranciou, alebo budovanie mapy pre testovanie zásahov (hit-test) pri výbere textu v prehliadači. Ak potrebujete zistiť, ktorý znak sa nachádza pod kliknutím myši, metóda CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) vykoná toto vyhľadanie priamo bez toho, aby ste museli ručne prechádzať obdĺžniky

Nasadenie knižnice DLL

PDFium Component deleguje všetku analýzu PDF na natívnu knižnicu DLL, buď pdfium32.dll alebo pdfium64.dll v závislosti od vašej cieľovej platformy. Súčasťou komponentu je skript CopyDlls.bat, ktorý skopíruje správny súbor do systémového adresára Windows. Na vývojárskom počítači ho stačí spustiť raz ako správca; pre nasadenie v produkcii skopírujete knižnicu DLL priamo k spustiteľnému súboru aplikácie. Variant s povoleným jadrom V8 (pdfium32v8.dll, pdfium64v8.dll) sú podstatne väčšie a sú potrebné iba vtedy, ak vaše dokumenty PDF obsahujú JavaScript, ktorý sa musí spustiť. Na samotnú extrakciu textu je správnou voľbou štandardná kompilácia

Ak knižnica DLL chýba pri spúšťaní programu, nastavenie Active := True potichu zlyhá presne tak, ako keby chýbal samotný súbor, pretože komponent chybu načítania interne zachytí. Pred vydaním aplikácie ju preto vždy otestujte na čistom počítači

Použitie FontSize[] spolu s Character[] na analýzu rozvrhnutia

Okrem čistého textu poskytuje znakové rozhranie API vlastnosť FontSize[i], ktorá vracia vykreslenú veľkosť bodu každého glyfu. V kombinácii s CharacterOrigin[i] a CharacterRectangle[i] vám to umožní rozlíšiť bežný text od nadpisov aj bez spoliehania sa na strom štruktúry. Postupnosť znakov, pri ktorej veľkosť písma presiahne určitý limit, je v neoznačenom dokumente takmer určite nadpisom. Rovnaká technika sa dá použiť na detekciu popiskov (malý text pod ohraničujúcim rámcom obrázka) alebo poznámok pod čiarou (malý text blízko dolného okraja stránky). Nič z toho nevyžaduje renderovanie; všetky tri vlastnosti sa čítajú priamo z textovej vrstvy, ktorú PDFium zostavuje počas stavu Active := True

Jedna drobnosť: FontSize[i] odráža veľkosť po uplatnení transformačnej matice stránky (CTM), takže dokument, v ktorom autor zmenil mierku celej stránky, bude vracať proporčne upravené veľkosti. Ak porovnávate veľkosti naprieč stránkami s rôznymi rozmermi, pred rozhodovaním o limitoch ich normalizujte voči výške MediaBoxu každej stránky

Zápis výstupu do súboru

Trieda TStringList v Delphi spracováva UTF-8 výstup bez problémov od verzie XE. Nastavte vlastnosť WriteBOM := False, ak potrebujete súbor bez označenia BOM (mnohí ďalší spracovatelia majú s úvodným BOM problémy):

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;

Pri veľmi rozsiahlych dokumentoch, kde záleží na spotrebe pamäte, zapisujte dáta priamo do TStreamWriter s kódovaním TEncoding.UTF8 v cykle stránok a nezhromažďujte všetko najprv do zoznamu

Rozhrania API pre Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent a CharacterIndexAtPos zobrazené v tomto článku sú súčasťou produktu PDFium Component pre Delphi a C++Builder