A PDF-ből történő szövegkinyerés egyszerűnek tűnik, amíg nem találkozik olyan dokumentummal, amelyből a szövegréteg hiányzik, sérült, vagy több tucat apró, értelmes sorrend nélküli karakterrészletre van osztva. A PDFium komponens két belépési pontot biztosít: a Character[] tömböt az oldalon lévő egyes glifák nyers, index-alapú eléréséhez, valamint a ReadablePageContent-et egy strukturált nézethez, amely a PDF címkefájából (tag tree) vagy heurisztikus elemzéséből rekonstruálja a bekezdéseket és fejléceket. Nem mindig ugyanaz a helyes választás, ezért fontos megérteni, hogy melyik mit tesz elérhetővé
Dokumentum megnyitása és a csendes hiba csapdája
A TPdf a FileName beállításával és az Active := True értékadással nyit meg egy fájlt. A kritikus részlet: az Active := True soha nem vált ki kivételt. Ha a fájl hiányzik, jelszóval védett vagy sérült, a PDFium belsőleg kezeli a hibát, és az Active egyszerűen False marad. Ez azt jelenti, hogy minden szövegkinyerési ciklusnak védekeznie kell ez ellen:
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;
// a szövegkinyerés itt következik
finally
Pdf.Active := False;
Pdf.Free;
end;
A jelszóval védett fájlok esetében az Active := True hívás előtt be kell állítani a Pdf.Password := '...' tulajdonságot. Nincs második esély: ha az Active egyszer meghiúsult, be kell zárnia a dokumentumot, majd újra meg kell nyitnia a helyes jelszóval
Oldalankénti szövegkinyerés a Character[] használatával
A legalacsonyabb szintű megközelítés végigmegy a lap minden karakterén. Állítsa be a Pdf.PageNumber-t az adott oldal szövegrétegének betöltéséhez, majd iteráljon végig a CharacterCount bejegyzéseken a Character[] tulajdonság segítségével. Két jelzőt (flag) érdemes ellenőrizni minden bejegyzésnél: a CharacterGenerated[i] jelzi a renderelő által beillesztett szintetikus glifákat (például a sorsörések lágy elválasztójeleit), amelyek nem hordoznak valós Unicode értéket, a CharacterMapError[i] pedig azt jelzi, ha a PDFium nem tudta a glifát egy kódponthoz rendelni, ami ToUnicode táblával nem rendelkező betűtípus-kódolásoknál fordul elő
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; // a CR normalizálása LF-re
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
Az eredmény Unicode kódpontok lapos karakterlánca abban a sorrendben, ahogyan a PDFium felsorolja őket — ez a tartalomfolyamban (content stream) való megjelenésük sorrendje, nem feltétlenül a balról jobbra tartó olvasási sorrend. A standard irodai eszközökkel előállított legtöbb latin betűs dokumentumnál ez rendben is van. A szokatlan glifasorozatokkal OCR-ezett szkennelt PDF-eknél, vagy a jobbról balra írt szövegeknél a sorrend hibás lehet. Ilyenkor válik hasznosabbá a ReadablePageContent
Strukturált kinyerés a ReadablePageContent segítségével
A ReadablePageContent egy szinttel feljebb lép: egy TPdfReadableContent rekordot ad vissza, amelynek Fragments tömbje címkézett tartalomtöredékeket hordoz, mindegyik rendelkezik egy Kind tulajdonsággal, amely azonosítja a bekezdéseket, fejléceket, listaelemeket, táblázatcellákat és így tovább. Ha a PDF struktúrafát tartalmaz (ellenőrizze a Pdf.IsTagged-et), a forrás a rosStructure és az olvasási sorrend hiteles. A címkézetlen fájlok esetében a PDFium a rosHeuristic-re lép vissza, amely a karaktereket a befoglaló kereteik (bounding boxes) alapján hihető olvasási egységekbe csoportosítja, de nem garantálja a pontosságot
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;
Ha a Content.Source = rosHeuristic és a kimenet zagyvának tűnik, a dokumentum szövegrétege valószínűleg nem az olvasási sorrendet szem előtt tartva lett megírva. Ezen a ponton az egyetlen megbízható megoldás a forrásalkalmazásból történő újraexportálás megfelelő címkézéssel, vagy egy olyan utófeldolgozási lépés futtatása, amely a karakterek kezdőpontjait először Y, majd X szerint rendezi
Mit nyújt a CharacterOrigin és a CharacterRectangle
Mindkét tulajdonság megadja a karakter pozícióját az oldaltérben (pontok, a kezdőpont a bal alsó sarokban, az Y felfelé növekszik). A CharacterOrigin[i] a glifa alapvonali rögzítési pontja; a CharacterRectangle[i] a teljes befoglaló keret (bounding box). Ezek az építőkövei a sima szövegen túli feladatoknak: oszlophatárok észlelése, karakterek sorokba csoportosítása az Y koordináták tűréshatáron belüli összehasonlításával, vagy egy hit-test térkép készítése a szövegkijelöléshez. Ha meg kell találnia, melyik karakter található az egérkattintás alatt, a CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) közvetlenül elvégzi ezt a keresést anélkül, hogy iterálnia kellene a téglalapokat
A DLL elhelyezése
A PDFium komponens minden PDF-elemzést egy natív DLL-re bíz, amely a célplatformtól függően pdfium32.dll vagy pdfium64.dll. A komponenshez tartozik egy CopyDlls.bat parancsfájl, amely átmásolja a megfelelő fájlt a Windows rendszertárába. Ezt egyszer Rendszergazdaként futtatni a fejlesztőgépen elegendő; a telepítéshez ehelyett az alkalmazás futtatható fájlja mellé kell másolni a DLL-t. A V8-képes változatok (pdfium32v8.dll, pdfium64v8.dll) lényeségen nagyobbak, és csak akkor szükségesek, ha a PDF-fájlok olyan JavaScript-et tartalmaznak, amelyet végre kell hajtani. Tiszta szövegkinyeréshez a standard változat a helyes választás
Ha a DLL hiányzik a futási időben, az Active := True csendben meghiúsul, ahogyan a hiányzó fájlok esetében is, mert a komponens belsőleg elkapja a betöltési hibát. Szállítás előtt mindig tesztelje az alkalmazást egy tiszta számítógépen
A FontSize[] használata a Character[] mellett az elrendezés elemzéséhez
A sima szövegen túl a karakterszintű API elérhetővé teszi a FontSize[i] tulajdonságot, amely visszaadja az egyes glifák kirajzolt pontméretét (point size). A CharacterOrigin[i] és CharacterRectangle[i] tulajdonságokkal kombinálva ez lehetővé teszi, hogy megkülönböztesse a törzsszöveget a fejlécektől a struktúrafa használata nélkül is. Az a karaktersorozat, ahol a betűméret egy küszöbérték fölé ugrik, szinte biztosan fejléc egy címkézetlen dokumentumban. Ugyanez a technika alkalmazható képaláírások (kisméretű szöveg a kép befoglaló kerete alatt) vagy lábjegyzetek (kisméretű szöveg az oldal aljához közel) észlelésére is. Ehhez nincs szükség renderelésre; mindhárom tulajdonság közvetlenül a szövegrétegből olvas, amelyet a PDFium az Active := True hívás során épít fel
Egy árnyalat: a FontSize[i] az oldal CTM-jének (current transformation matrix, aktuális transzformációs mátrix) alkalmazása utáni méretet tükrözi, így egy olyan dokumentum, ahol a szerző a teljes oldalt skálázta, arányosan módosított méreteket fog jelenteni. Ha a méreteket különböző oldaldimenziójú oldalak között hasonlítja össze, a küszöbérték-döntések meghozatala előtt normalizálja azokat az egyes oldalak MediaBox magasságához
A kimenet fájlba írása
A Delphi TStringList osztálya az XE verzió óta tisztán kezeli az UTF-8 kimenetet. Állítsa a WriteBOM := False értéket, ha BOM-mentes fájlra van szüksége (sok feldolgozó program elakad a kezdeti BOM-ban):
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;
Nagyon nagy dokumentumok esetében, ahol a memória kritikus tényező, írjon közvetlenül egy TStreamWriter-be TEncoding.UTF8 kódolással az oldalciklusban, ahelyett, hogy mindent először egy listában gyűjtene össze
Az itt bemutatott Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent és CharacterIndexAtPos API-k a Delphihez és C++Builderhez készült PDFium komponens részét képezik