Wyodrębnianie tekstu z pliku PDF wydaje się proste, dopóki nie natrafisz na dokument, w którym warstwa tekstowa jest nieobecna, uszkodzona lub podzielona na dziesiątki drobnych fragmentów znaków bez logicznego porządku. Komponent PDFium Component oferuje dwa punkty wejścia: tablicę Character[] zapewniającą bezpośredni dostęp do każdego glifu na stronie na podstawie indeksu oraz funkcję ReadablePageContent oferującą ustrukturyzowany widok rekonstruujący akapity i nagłówki z drzewa znaczników PDF lub analizy heurystycznej. Żadne z tych rozwiązań nie jest uniwersalne, dlatego warto rozumieć różnice między nimi
Otwieranie dokumentu i pułapka cichego niepowodzenia
Komponent TPdf otwiera plik poprzez ustawienie właściwości FileName i zmianę stanu na Active := True. Kluczowy szczegół: ustawienie Active := True nigdy nie wywołuje wyjątku. Jeśli plik nie istnieje, jest chroniony hasłem lub uszkodzony, biblioteka PDFium obsłuży ten błąd wewnętrznie, a właściwość Active po prostu pozostanie w stanie False. Oznacza to, że każda pętla wyodrębniania musi zawierać następujące zabezpieczenie:
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;
// dalsza procedura wyodrębniania
finally
Pdf.Active := False;
Pdf.Free;
end;
Dokumenty chronione hasłem wymagają ustawienia Pdf.Password := '...' przed wykonaniem przypisania Active := True. Nie ma tu drugiej szansy: jeśli przypisanie Active się nie powiedzie, należy zamknąć dokument i otworzyć go ponownie z właściwym hasłem
Wyodrębnianie strona po stronie za pomocą Character[]
Najbardziej niskopoziomowe podejście polega na analizie każdego znaku na poszczególnych stronach. Ustaw właściwość Pdf.PageNumber, aby załadować warstwę tekstową dla danej strony, a następnie wykonaj iterację CharacterCount znaków korzystając z właściwości Character[]. Warto sprawdzać dwie flagi dla każdego znaku: CharacterGenerated[i] oznacza syntetyczne glify wstawione przez silnik renderujący (np. miękkie łączniki przy przełamaniu linii), które nie mają odzwierciedlenia w kodowaniu Unicode, natomiast CharacterMapError[i] sygnalizuje, że PDFium nie powiązało glifu z punktem kodowym, co zdarza się przy czcionkach bez tabeli 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; // znormalizuj znak powrotu karetki (CR) do nowej linii (LF)
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
Wynikiem jest prosty ciąg znaków Unicode w kolejności, w jakiej PDFium je wylicza — jest to kolejność ich zapisu w strumieniu zawartości, która nie zawsze odpowiada kolejności czytania od lewej do prawej. Dla większości dokumentów z pismem łacińskim wygenerowanych przez programy biurowe to wystarczające rozwiązanie. W przypadku zeskanowanych plików PDF poddanych procesowi OCR z nietypową kolejnością glifów lub dla tekstów pisanych od prawej do lewej kolejność może być zaburzona. W takich sytuacjach lepiej sprawdza się funkcja ReadablePageContent
Wyodrębnianie strukturalne za pomocą ReadablePageContent
Funkcja ReadablePageContent działa na wyższym poziomie: zwraca rekord TPdfReadableContent, którego tablica Fragments zawiera oznaczone fragmenty zawartości, z parametrem Kind określającym akapity, nagłówki, elementy listy, komórki tabeli itp. Jeśli plik PDF posiada strukturę logiczną (weryfikowaną przez Pdf.IsTagged), źródłem danych jest rosStructure, co gwarantuje poprawną kolejność czytania. Dla plików bez struktury PDFium korzysta z metody heurystycznej rosHeuristic, która grupuje znaki w bloki na podstawie ich współrzędnych, co jednak nie daje pełnej gwarancji dokładności
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;
Jeśli Content.Source = rosHeuristic, a wynikowy tekst jest nieuporządkowany, oznacza to, że warstwa tekstowa dokumentu nie została zapisana z myślą o naturalnej kolejności czytania. W takim przypadku jedynym trwałym rozwiązaniem jest ponowny eksport pliku z aplikacji źródłowej z włączonym oznaczaniem struktury (tagging) lub wdrożenie procedury sortującej pozycje znaków w pionie (Y), a następnie w poziomie (X)
Zastosowanie właściwości CharacterOrigin i CharacterRectangle
Obie te właściwości zwracają pozycję znaku w układzie współrzędnych strony (punkty, z punktem początkowym w lewym dolnym rogu, współrzędna Y rośnie w górę). CharacterOrigin[i] określa punkt bazowy linii pisma glifu, natomiast CharacterRectangle[i] to jego pełny obszar otaczający (bounding box). Informacje te są niezbędne do zaawansowanych analiz: wykrywania kolumn tekstu, grupowania znaków w linie (poprzez porównywanie współrzędnej Y z określoną tolerancją) czy tworzenia mapy zaznaczania tekstu w przeglądarce. Do wyszukiwania znaku pod kursorem myszy służy funkcja CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY), eliminująca potrzebę ręcznego przeszukiwania współrzędnych
Instalacja biblioteki DLL
Komponent PDFium Component deleguje zadania przetwarzania plików PDF do natywnej biblioteki DLL: pdfium32.dll lub pdfium64.dll, zależnie od docelowej platformy sprzętowej. Pakiet zawiera skrypt CopyDlls.bat kopiujący odpowiednie pliki do katalogu systemowego Windows. Jednorazowe uruchomienie go z uprawnieniami administratora na komputerze programisty jest wystarczające; w instalacjach klienckich pliki te kopiuje się bezpośrednio do katalogu z plikiem wykonywalnym aplikacji. Wersje z silnikiem V8 (pdfium32v8.dll, pdfium64v8.dll) are znacznie większe i wymagane wyłącznie wtedy, gdy pliki PDF zawierają skrypty JavaScript. Do samego wyodrębniania tekstu w zupełności wystarcza wersja standardowa
Brak biblioteki DLL w czasie uruchomienia aplikacji spowoduje ciche niepowodzenie przy wywołaniu Active := True (tak samo jak przy braku pliku), ponieważ komponent przechwytuje błąd ładowania wewnętrznie. Przed wdrożeniem oprogramowania u klientów zawsze przeprowadź testy na czystym systemie
Wykorzystanie FontSize[] obok Character[] do analizy układu
Oprócz odczytu znaków, API poziomu znaku udostępnia właściwość FontSize[i] zwracającą stopień powiększenia każdego glifu w punktach. W połączeniu z CharacterOrigin[i] oraz CharacterRectangle[i] pozwala to odróżnić tekst akapitu od nagłówka bez korzystania z drzewa struktury. Ciąg znaków o rozmiarze wyraźnie większym od reszty tekstu w dokumencie bez struktury najpewniej stanowi nagłówek. Ta sama technika służy do wykrywania podpisów pod zdjęciami (mały tekst pod ramką obrazu) oraz przypisów dolnych. Analizy te nie wymagają renderowania graficznego; wszystkie trzy właściwości odczytują dane bezpośrednio z warstwy tekstowej tworzonej przez PDFium przy ustawieniu Active := True
Mały niuans: właściwość FontSize[i] zwraca rozmiar po uwzględnieniu macierzy przekształceń strony (CTM), więc dokument, w którym przeskalowano całą stronę, wykaże odpowiednio zmienione rozmiary pisma. Porównując rozmiary czcionek na stronach o różnych formatach, warto znormalizować te wartości względem wysokości MediaBox każdej strony przed ustaleniem progów podziału
Zapisywanie tekstu do pliku
Klasa TStringList w środowisku Delphi od wersji XE prawidłowo obsługuje kodowanie UTF-8. Ustaw właściwość WriteBOM := False, jeśli chcesz zapisać plik bez znacznika BOM (wiele zewnętrznych parserów zgłasza błędy przy napotkaniu początkowego znacznika BOM):
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;
W przypadku bardzo dużych dokumentów, w celu oszczędzania pamięci, zaleca się zapisywanie danych bezpośrednio przez TStreamWriter z kodowaniem TEncoding.UTF8 w pętli przetwarzania stron, zamiast gromadzenia całego tekstu w jednej liście
Opisane tu interfejsy API Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent oraz CharacterIndexAtPos wchodzą w skład PDFium Component dla Delphi i C++Buildera