Műszaki cikk

Szöveg kinyerése PDF fájlokból Delphiben a PDFium komponens segítségével

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