Technical Article

Extrahovanie textu z PDF súborov pomocou PDFium VCL v Delphi

Extrahia textu z PDF vyzerá jednoducho, kým nenarazíte na dokument, kde textová vrstva úplne chýba, je poškodená alebo rozdelená do desiatok malých úsekov bez akéhokoľvek zmysluplného poradia. PDFium VCL vám poskytuje dva vstupné body: pole Character[] pre surový, indexovaný prístup ku každému glyfu na stránke a metódu ReadablePageContent pre štruktúrovaný pohľad, ktorý rekonštruuje odseky a nadpisy na základe stromu značiek PDF alebo heuristickej analýzy. Žiadna z týchto možností nie je univerzálne správnou voľbou, 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 FileName a prepnutím Active := True. Kľúčový detail: priradenie Active := True nikdy nevyvolá výnimku. Ak súbor chýba, je chránený heslom alebo poškodený, PDFium chybu zachytí interne a vlastnosť Active jednoducho zostane na hodnote False. To znamená, že každý cyklus extrakcie sa musí pred takýmto správaním 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;

Heslom chránené súbory vyžadujú nastavenie Pdf.Password := '...' pred nastavením Active := True. Druhá šanca neexistuje: ak aktivácia zlyhá, musíte dokument zatvoriť a otvoriť ho znova so správnym heslom.

Extrakcia po stránkach pomocou Character[]

Nízkoúrovňový prístup prechádza každý znak na každej stránke. Nastavením vlastnosti Pdf.PageNumber načítate textovú vrstvu pre danú stránku a potom prechádzate položky od 0 do CharacterCount - 1 pomocou vlastnosti Character[]. Pri každej položke stojí za to skontrolovať dva príznaky: CharacterGenerated[i] označuje syntetické glyfy vložené vykresľovačom (napríklad mäkké pomlčky pri zlome riadku), ktoré nemajú skutočnú hodnotu Unicode, a CharacterMapError[i] signalizuje, že PDFium nedokázalo mapovať glyf na kódový bod, čo sa stáva pri kódovaní 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 plochý reťazec kódových bodov Unicode v poradí, v akom ich PDFium prechádza, čo je poradie, v akom sa objavujú v prúde obsahu, a nie nevyhnutne poradie čítania zľava doprava. Pre väčšinu dokumentov v latinke vytvorených štandardnými kancelárskymi nástrojmi je to v poriadku. Pri naskenovaných súboroch PDF, ktoré prešli procesom OCR s neobvyklým usporiadaním glyfov, alebo pri texte písanom sprava doľava, môže byť toto poradie nesprávne. V takýchto prípadoch je užitočnejšia metóda ReadablePageContent.

Štruktúrovaná extrakcia pomocou ReadablePageContent

Metóda ReadablePageContent ide o úroveň vyššie: vracia záznam typu TPdfReadableContent, ktorého pole Fragments nesie označené fragmenty obsahu. Každý fragment má vlastnosť Kind, ktorá identifikuje odseky, nadpisy, položky zoznamu, bunky tabuľky a podobne. Keď PDF obsahuje strom štruktúry (skontrolujte cez Pdf.IsTagged), zdrojom dát je rosStructure a poradie čítania je smerodajné. Pre neoznačené (untagged) súbory sa PDFium vracia k režimu rosHeuristic, ktorý zoskupuje znaky podľa ich ohraničujúcich rámcov do pravdepodobných čitateľných jednotiek, no nemôže zaručiť 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 and váš výstup je skomolený, textová vrstva dokumentu pravdepodobne nebola zapísaná so zreteľom na poradie čítania. V takom momente je jedinou spoľahlivou nápravou opätovný export zo zdrojovej aplikácie so správnym označkovaním (tagging) alebo spustenie kroku dodatočného spracovania, ktorý usporiada znaky podľa ich súradníc Y a následne X.

Čo vám poskytujú CharacterOrigin a CharacterRectangle

Obe vlastnosti vracajú pozíciu znaku v priestore stránky (v bodoch, s počiatkom v ľavom dolnom rohu a osou Y rastúcou nahor). CharacterOrigin[i] predstavuje kotviaci bod znaku na účaří (baseline); CharacterRectangle[i] vracia celý ohraničujúci rámec (bounding box). Tieto vlastnosti sú stavebnými kameňmi pre čokoľvek zložitejšie než len bežný text: pre detekciu hraníc stĺpcov, zoskupovanie znakov do riadkov porovnávaním súradníc Y v rámci určitej tolerancie alebo pre stavbu mapy zásahov 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.

Zabezpečenie prítomnosti DLL knižnice

PDFium VCL deleguje všetku analýzu PDF na natívny DLL súbor, 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. Jedno jeho spustenie s právami administrátora na vývojárskom počítači stačí; pre nasadenie v produkcii skopírujete DLL súbor vedľa spustiteľného súboru aplikácie. Varianty s povoleným V8 (pdfium32v8.dll, pdfium64v8.dll) sú podstatne väčšie a potrebné sú len vtedy, ak vaše PDF súbory obsahujú JavaScript, ktorý sa musí vykonať. Pre čistú extrakciu textu je správnou voľbou štandardná zostava.

Ak DLL súbor počas behu chýba, volanie Active := True zlyhá ticho rovnako ako pri chýbajúcom súbore, pretože komponent interne zachytí chybu načítania. Pred distribúciou softvéru ho vždy otestujte na čistom systéme.

Použitie FontSize[] spoločne s Character[] pre analýzu rozvrhnutia

Použitie TPdfView vo formulárovej aplikácii

Pamäť a výkon pri hromadných úlohách

Pri veľkom archíve je hlavnou vecou, ktorú treba sledovať, spotreba pamäte. Každé volanie Bitmap[] alokuje nový objekt TBitmap na halde a pri naskenovanej stránke s rozlíšením 300 DPI to môže byť aj 25 MB surových pixelových dát pred akýmkoľvek kódovaním. Ak spracovávate stránky v rýchlom cykle bez uvoľňovania pamäte medzi iteráciami, pracovná pamäť rastie lineárne s počtom obrázkov. Správny postup je vždy: získať jednu bitmapu, urobiť s ňou to potrebné, uvoľniť ju a až potom získať ďalšiu. Ak pedagóg potrebuje držať odkazy na viacero bitmap naraz (napríklad kvôli porovnávaniu), najprv zistite ich počet pomocou BitmapCount a podľa toho alokujte svoj kontajner. Každú bitmapu uvoľnite hneď, ako s ňou skončíte, a neodkladajte to na koniec celého dokumentu. Pri dokumente s 500 naskenovanými stránkami môže tento rozdiel znamenať rozdiel medzi 25 MB a 12 GB špičkovej alokácie pamäte.

Komponent TPdfView sprístupňuje rovnaké vlastnosti BitmapCount a Bitmap[], ale stránka, z ktorej číta, je aktuálne zobrazená stránka v zobrazení, nie vlastnosť TPdf.PageNumber. Tieto dva ukazovatele stránok sú nezávislé; nastavenie jedného nepresunie druhý. V aplikácii s VCL formulárom a živým náhľadom môžete nastaviť Pdf.PageNumber := N na riadenie extrakcie cez TPdf, zatiaľ čo prehliadač zostane na stránke, na ktorú používateľ naposledy prešiel. Toto oddelenie je zámerné a udržiava stav zobrazení prehliadača čistý aj počas behu extrakcie na pozadí.

Rozhrania Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent a CharacterIndexAtPos popísané v tomto článku sú súčasťou komponentu PDFium VCL Component pre Delphi a C++Builder.