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