Wyciąganie tekstu, obrazów i czcionek z istniejącego pliku PDF brzmi jak problem rozwiązany, dopóki nie przepuścisz przez to prawdziwego korpusu. Wyceluj indekser wyszukiwania w czterdzieści tysięcy plików klientów, a usterki poukładają się w kilka rozpoznawalnych stosów. Słowa zlewają się ze sobą, bo nikt nie powiedział ekstraktorowi, jak szeroka przerwa liczy się jako spacja. Inne strony wracają jako bełkot, bo czcionka podzbioru nie niesie żadnej mapy z kodów glifów na faktyczne znaki. A „logo firmy” okazuje się dziewięcioma osobnymi obiektami obrazu ułożonymi za maską miękką. Nic z tego nie jest błędem biblioteki. To różnica między wywołaniem funkcji ekstrakcji a zrozumieniem, co ta funkcja może, a czego nie może odzyskać z bajtów na dysku
losLab PDF Library w edycji dla Pascala daje kodowi w Delphi i C++Builder więcej niż jeden sposób odczytu każdego z tych trzech strumieni, a poziomy różnią się tym, co gwarantują. Sztuka polega na dopasowaniu poziomu do zadania: indeks wyszukiwania, narzędzie do przeglądu redakcji i przebieg preflight PDF/A chcą z tej samej strony różnych rzeczy, a sięgnięcie po niewłaściwe wywołanie marnuje wysiłek albo produkuje wyjście, któremu nie możesz ufać
Poziomy ekstrakcji tekstu i co każdy z nich obiecuje
GetPageText przyjmuje wartość opcji od 0 do 8, a ta liczba wybiera silnik, a nie format. Wartości od 0 do 2 uruchamiają lekki przebieg, który wystarcza do szybkiego podglądu. Wartości od 3 do 8 idą przez silnik świadomy układu, który odbudowuje wiersze i odstępy na podstawie tego, gdzie glify faktycznie leżą na stronie. W tym przedziale wariacje mają znaczenie: 4 i 6 dzielą wyjście na słowa, 5 i 6 emitują szerokości poszczególnych glifów, a 7 zwraca czysty tekst z celowo odrzuconymi metadanymi czcionki, koloru i bloku. Opcja 7 to ta, którą karmi się indeks wyszukiwania, ponieważ indeks chce słów i niczego więcej
Żadne ustawienie opcji nie uratuje dokumentu, który od początku nie niósł potrzebnej informacji. PDF odwzorowuje kody znaków na kształty glifów, a jedynym, co odwzorowuje te kody z powrotem na czytelny tekst, jest CMap ToUnicode czcionki (ISO 32000-1 §9.10). Gdy czcionka podzbioru przychodzi bez niej, każdy ekstraktor jest bezradny. Ta biblioteka, kopiuj-wklej w przeglądarce, konkurencyjny zestaw narzędzi: wszystkie sprowadzają się do zgadywania z nazw glifów albo do zwracania niczego. Praktyczną odpowiedzią jest wykrywanie, a nie heroizm. Oceń stronę jako mało wiarygodną i wyślij ją do OCR, ponieważ ciche zaindeksowanie śmieci jest gorsze od przyznania, że nie potrafisz jej odczytać
Dla przypadków, których płaskie opcje nie pokrywają, czyli własnej tokenizacji, forensyki strumienia treści, lejka tekstowego zbudowanego według twoich reguł, dekoder jest dostępny warstwę niżej. TPDFExtractor konstruuje się nad słownikiem zasobów strony i jej kolekcją czcionek. Jego metoda ExtractTextW przepuszcza surowe operacje tekstowe strumienia treści z powrotem przez tę samą maszynerię czcionek, by odzyskać Unicode, a zdarzenie OnFindObject podaje ci każdy obiekt, gdy przepływa obok. Większość kodu nigdy nie musi schodzić tak głęboko. Aplikacje, które muszą, są tymi, które cieszą się, że ta warstwa jest publiczna, a nie zakopana
Pozycjonowane bloki: jednostka trafień wyszukiwania i przeglądu redakcji
Czysty tekst mówi ci, co strona mówi. Prędzej czy później produkt musi też wiedzieć, gdzie to mówi, żeby podświetlić trafienie wyszukiwania, obrysować kandydata do redakcji albo zakotwiczyć adnotację we właściwym miejscu. ExtractPageTextBlocks zwraca uchwyt do listy przebiegów tekstu, a każdy przebieg niesie swój tekst, swój prostokąt ograniczający oraz nazwę i rozmiar czcionki, w której go złożono:
var
Pdf: TPDFlib;
Blocks, I: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
raise Exception.Create('load failed');
Pdf.SelectPage(1);
Blocks := Pdf.ExtractPageTextBlocks(0);
for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
Writeln(Format('%s [%s %.1f pt at %.0f,%.0f]',
[Pdf.GetTextBlockText(Blocks, I),
Pdf.GetTextBlockFontName(Blocks, I),
Pdf.GetTextBlockFontSize(Blocks, I),
Pdf.GetTextBlockBound(Blocks, I, 0),
Pdf.GetTextBlockBound(Blocks, I, 1)]));
Pdf.ReleaseTextBlocks(Blocks);
finally
Pdf.Free;
end;
end;
Jeden szczegół w tym obszarze potyka integracje bardziej niż jakikolwiek inny. SetTextExtractionArea, SetTextExtractionWordGap i SetTextExtractionOptions to trwały stan na poziomie dokumentu, a nie argumenty przekazywane przy każdym wywołaniu. Skonfiguruj ograniczenie obszaru dla jednej funkcji, powiedzmy odczyt samego pasa nagłówka w celu klasyfikacji dokumentu, a po cichu obetnie ono każdą kolejną ekstrakcję na tym samym uchwycie, łącznie z poziomami GetPageText świadomymi układu, po które sięgniesz później. Albo resetuj stan ekstrakcji między zadaniami logicznymi, albo daj każdemu zadaniu własny uchwyt dokumentu
Próg przerwy międzywyrazowej to dźwignia dla pierwszego stosu usterek, czyli zlewających się słów. SetTextExtractionWordGap mówi silnikowi układu, ile przestrzeni poziomej, mierzonej względem własnych odstępów między glifami na stronie, oddziela jedno słowo od następnego. Gęsta tabela chce mniejszej przerwy niż luźno złożona strona marketingowa, więc próg dostrojony do klasy dokumentu bije jedną globalną stałą. Jest trwały na dokumencie jak reszta stanu ekstrakcji, więc zaplanuj ustawianie go świadomie, a nie raz i na zawsze
Obrazy: oryginalne strumienie, a nie zrzuty ekranu
Złym sposobem na wydobycie obrazów z PDF jest wyrenderowanie strony i jej przycięcie. To ponownie próbkuje piksele, wypieka wszelki obrót i wyrzuca to, czym był oryginał. GetPageImageList wylicza zamiast tego faktyczne zasoby obrazu, do których odwołuje się strona, a każda pozycja oddaje swoje właściwości i swoje oryginalne, nienaruszone dane:
var
ImgList, I: Integer;
begin
Pdf.SelectPage(1);
ImgList := Pdf.GetPageImageList(0);
for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
begin
Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
Format('page1-img%.2d.bin', [I]));
end;
Pdf.ReleaseImageList(ImgList);
end;
Sprawdź GetImageListItemFormatDesc, zanim cokolwiek o pozycji założysz, ponieważ to, do czego odwołuje się strona, rzadko bywa jednym schludnym obrazkiem na każdy widoczny obraz. Maska miękka pojawia się jako własny osobny wpis. Ten sam XObject często powtarza się na wielu stronach, więc odsiej duplikaty po skrócie treści, zanim zarchiwizujesz eksport „wszystkich obrazów”, bo inaczej zapiszesz to samo logo sto razy. JPEG-i w CMYK wymagają zarządzania kolorem dalej w potoku, inaczej renderują się z odwróconymi barwami w przeglądarkach, które biorą kanały dosłownie. Gdy chcesz inwentarza obejmującego cały dokument, a nie strony po kolei, FindImages razem z SetFindImagesMode skanuje cały plik w jednym przebiegu
Jest jedna granica warta podniesienia z interesariuszami, zanim ktokolwiek napisze kryteria odbioru: ekstrakcja obrazów zwraca wyłącznie zasoby rastrowe. Logo albo wykres narysowany ścieżkami wektorowymi nie jest obrazem w sensie zasobu i nigdy nie pojawi się na żadnej liście obrazów, choćby na ekranie czytał się najwyraźniej jak obrazek. Gdy wymaganiem naprawdę jest dostarczenie tego wykresu jako pliku, uczciwym podejściem jest wyrenderowanie obszaru strony do bitmapy, co jest inną operacją o innej wierności. Te dwa rodzaje wyjścia nie należą do tego samego folderu eksportu bez etykiety mówiącej, co jest czym
Czcionki: powierzchnia audytu, a nie funkcja eksportu
API czcionek odpowiada na pytania o czcionki. Nie wręcza ci samych plików czcionek, a to rozróżnienie kształtuje wszystko, co możesz na nim zbudować. Gdy FindFonts przeskanuje dokument, wyliczanie obchodzi czcionki po identyfikatorach, a wywołania właściwości raportują o czcionce aktualnie wybranej:
var
I: Integer;
begin
Pdf.FindFonts;
for I := 1 to Pdf.FontCount do // indeksy czcionek startują od 1, nie od 0
if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
Writeln(Format('%s type=%d embedded=%d subset=%d',
[Pdf.FontName, Pdf.FontType,
Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;
Uważaj na granice pętli. Indeksy czcionek biegną od 1 do FontCount, podczas gdy indeksy bloków tekstu i listy obrazów kilka akapitów wyżej liczone są od zera. Przenieś jedną konwencję w drugie miejsce, a dostaniesz pomyłkę o jeden, która albo pominie pierwszą czcionkę, albo wybiegnie poza koniec, i przejdzie przez pobieżne testy, bo większość dokumentów ma kilka czcionek, a ta niewłaściwa nadal wygląda wiarygodnie. Bądź też jasny co do zakresu. To API nie ma eksportu czcionek na poziomie bajtów. Żadne wywołanie nie zwraca osadzonego programu czcionki jako pliku TTF czy OTF, a wyliczanie plus wgląd w metadane to cały zamierzony model. Ten model i tak pokrywa to, czego praca produkcyjna faktycznie od czcionek wymaga: wykrywanie podzbiorów po wzorcu nazwy, audyty osadzenia przed konwersją archiwalną (nieosadzona czcionka jest twardą blokadą PDF/A, co omawia artykuł o preflight PDF/A i PDF/UA w Delphi) oraz diagnostykę kodowania na wypadek spadku wiarygodności ekstrakcji. Jest też powód licencyjny, dla którego granica leży właśnie tu. Program czcionki podzbioru to materiał licencjonowany, a pozbawiony większości glifów i tak bezużyteczny jako czcionka do instalacji. Traktowanie go jako metadanych audytowych, a nie zasobu do wyeksportowania, to stanowisko, którego da się bronić
To ostatnie wywołanie zarabia na siebie przy segregacji. Uruchom GetFontEncoding na każdej czcionce, odczytaj to obok flagi podzbioru, a przewidzisz jakość ekstrakcji, zanim wyciągniesz choćby jeden znak. Strona, której wszystkie czcionki są podzbiorami z niestandardowymi kodowaniami, jest kandydatem do OCR już na podstawie samych oględzin, co pozwala potokowi wsadowemu skierować ją właściwie bez marnowania na nią najpierw nieudanego przebiegu ekstrakcji
Ekstrakcja na skalę bez wczytywania dokumentów
W potoku wsadowym wczytywanie całego dokumentu tylko po to, by odczytać jedną stronę, to zmarnowane operacje wejścia-wyjścia, a na całym korpusie szybko się to sumuje. Warianty jednowywołaniowe, ExtractFilePageText i ExtractFilePageTextBlocks, przyjmują wprost nazwę pliku, hasło i numer strony i pomijają pełne wczytanie. Dla plików rzędu gigabajtów jest jeszcze niższy bieg. Ścieżka dostępu bezpośredniego otwiera plik przez strumieniowe odczyty xref, więc DAOpenFileReadOnly, a po nim DAExtractPageText, dotyka wyłącznie obiektów, których ta jedna strona faktycznie potrzebuje. Przychodzi z tym zmiana konwencji warta zapamiętania: funkcje DA adresują strony przez PageRef, uchwyt odwołania do obiektu otrzymywany z DAFindPage, nigdy przez surowy numer strony. Podaj numer tam, gdzie należy uchwyt, a wywołanie zadziała na niewłaściwym obiekcie bez zgłoszenia błędu, co jest najgorszym rodzajem pomyłki do wytropienia. Resztę zestawu narzędzi dostępu bezpośredniego rozkłada artykuł o scalaniu, dzieleniu i bezpośrednim dostępie do dużych plików PDF
Jeśli jest jeden nawyk odróżniający kod ekstrakcji, który przeżyje prawdziwy korpus, od kodu, który kuleje, to traktowanie strony jako wejścia niezaufanego, a nie czystego źródła danych. Tekst niezgodny z tym, co renderuje przeglądarka, jest niemal zawsze problemem kodowania, ligaturą zwiniętą do jednego glifu albo czcionką podzbioru bez wpisów ToUnicode, a lekarstwem jest zmierzenie wiarygodności i skierowanie złych stron do OCR, a nie walka z bajtami. API czcionek nigdy nie wyprodukuje TTF ani OTF, i tak to zaprojektowano, więc buduj procesy wokół pytań audytowych. A trwały stan ekstrakcji, przede wszystkim prostokąt obszaru, to ustawienie, które posiadasz przez całe życie uchwytu dokumentu, a nie parametr, o którym zapominasz po jednym wywołaniu. Wyrób te trzy odruchy, a reszta API zachowuje się przyzwoicie
Wersje ewaluacyjne, projekty demonstracyjne i pełne odniesienie do API ekstrakcji znajdują się na stronie produktu losLab PDF Library for Delphi