HotPDF Delphi Component odbudowuje spacje wyrazów i końce linii w THotPDF.ExtractLoadedPageText z geometrii glifów, a nie ze znaków spacji. Spacja wchodzi, gdy odstęp za własną szerokością glifu przekracza 0.15 wysokości tekstu, a nowa linia zaczyna się tylko wtedy, gdy początek tekstu przesunie się w poprzek kierunku pisania o więcej niż połowę wysokości tekstu. Od v2.768.3 tekst strony obejmuje też tekst malowany przez Form XObjects i pomija glify poza widocznym obszarem crop. Reszta tego tekstu wyjaśnia, dlaczego każda reguła wygląda tak, jak wygląda, bo każda z nich wymieniła prostszą regułę, która na prawdziwych dokumentach produkowała wiarygodny, ale zły wynik
Objawy zna każdy, kto karmił tekst PDF-ów indeksem wyszukiwania. Strona tytułowa ekstrahuje się jako PDFReferenceManualNovember4,1998, formularz podatkowy rozpada się na 156 linii, ukośny znak wodny przychodzi znak na linię, a przycięty proof zaczyna się od linii slug drukarni, której żadna przeglądarka nie pokazuje. Żaden z tych plików nie jest zepsuty. Każdy używa w pełni legalnego sposobu pozycjonowania tekstu, który naiwny ekstraktor odczyta źle
Dlaczego wyekstrahowany tekst PDF traci spacje między wyrazami?
Wyekstrahowany tekst traci spacje, bo PDF nigdy nie musi ich zawierać. Producent może rozdzielać wyrazy pokazując znak spacji, ale równie dobrze może przesunąć pióro liczbą wewnątrz tablicy TJ (ISO 32000-1 §9.4.3) albo świeżym Td (§9.4.2), i dokładnie tak robi wyjście TeX-a, wiele plików Distillera i większość layoutów justowanych. Przed v2.766.76 HPDFAssemblePageText patrzył tylko na ruch pionowy, więc złamanie wyrazu zrobione pozycjonowaniem po prostu znikało. Assembler mierzy teraz, wzdłuż kierunku pisania poprzedniego glifu, odległość od końca własnej szerokości tego glifu do początku bieżącego glifu i wstawia jedną spację, gdy odległość przekracza 0.15 wysokości pudełka bieżącego glifu, mierzonej od ascendentu do descendentu w przestrzeni użytkownika. Spacja nie jest dodawana, gdy któraś strona jest już pusta, ani między dwoma znakami CJK, bo justowanie rozciąga ideogramy, a to rozciągnięcie nie znaczy granicy wyrazu. Rekordy glifów wystawiają tę samą geometrię, więc możesz odtworzyć decyzję, gdy konkretny plik cię zaskoczy
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// wysokość pudełka glifu od ascendentu do descendentu, w przestrzeni użytkownika
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// tekst poziomy: odstęp od końca własnej szerokości poprzedniego glifu
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
Dlaczego mierzyć od własnej szerokości glifu, a nie od pozycji pióra?
HotPDF mierzy odstępy wyrazów od GlyphEndX / GlyphEndY, bo pozycja pióra po glifie zawiera już odstępy, które odstępem nie są. ISO 32000-1 §9.4.4 definiuje przesunięcie poziome jako szerokość glifu razy rozmiar fontu, plus odstęp znakowy Tc, plus odstęp wyrazowy Tw, wszystko przeskalowane przez Tz. BaselineEndX / BaselineEndY trzymają to pełne przesunięcie, podczas gdy GlyphEndX / GlyphEndY trzymają tylko posuw fontu i Tz. Różnica ma znaczenie dla producentów, którzy ściskają tracking ujemnym Tc, a potem oddają miejsce korektą TJ po każdym glifie: mierzone od pozycji pióra oddawanie wygląda jak odstęp i chiński termin “95后” ekstrahował się jako “9 5 后”. Próg jest przywiązany do wysokości pudełka glifu, a nie do rozmiaru Tf, z podobnego powodu. Eksporty Worda często piszą 1 Tf i niosą prawdziwy rozmiar w przeskalowanym Tm, więc Tfs mówi 1, podczas gdy tekst ma 10 punktów, i reguła kluczowana do Tfs traktowałaby obie pisownie tej samej strony różnie
Reguła ma uczciwe krawędzie. Nagłówek złożony z bardzo luźnym trackingiem, gdzie samo Tc otwiera więcej niż 0.15 wysokości tekstu między literami, ekstrahuje się ze spacją między każdą literą, co jest tym, jak strona wygląda, ale chyba nie tym, co chciałeś indeksować. Kawałki rysowane w złej kolejności na jednej linii bazowej dają ujemny odstęp i sklejają się bez spacji. Żaden z tych przypadków nie jest częsty w tekście głównym, a na korpusie testowym zmiana podniosła trafienia wyrazów względem ekstraktora referencyjnego na 28 stronach, nie obniżając żadnego
Kiedy HotPDF zaczyna nową linię w wyekstrahowanym tekście?
Od v2.766.79 nowa linia zaczyna się, gdy przesunięcie od początku poprzedniego glifu do bieżącego, rzutowane na normalną poprzedniego kierunku pisania, przekracza połowę większej z dwóch wysokości pudełek. Wcześniejsza reguła porównywała surowy ruch Y z połową Tfs, co zawodziło w dwóch kierunkach. Przy 1 Tf i przeskalowanym Tm przymknął się on do połowy jednostki, więc indeks górny podniesiony text rise 0.4 albo zwykłe drganie linii bazowej łamały linię. Reguła ignorowała też całkiem X, więc tekst pod obróconym Tm schodził po stronie z każdym glifem i wychodził glif na linię. Rzutowanie na normalną kierunku sprawia, że obrócone ciągi zachowują się jak poziome, a branie większej z dwóch wysokości trzyma duże słowo próbki i jego mały podpis w jednej linii, gdy dzielą linię bazową. Na wspomnianym formularzu podatkowym liczba linii spadła ze 156 do 97. Tekst pionowy w trybie pisania 1 (§9.7.4.3) idzie osobną ścieżką: te glify są grupowane w kolumny, czytane z prawej do lewej i z góry na dół, z końcem linii przy każdej zmianie kolumny
Który tekst ExtractLoadedPageText obejmuje, a który zostawia?
ExtractLoadedPageText zwraca tekst, jaki pokazuje przeglądarka. Od v2.766.80 pracuje wyłącznie na widocznych glifach, wyrzucając każdy glif, którego środek pudełka wypada poza GetLoadedPageVisibleBox, czyli CropBox przycięty do MediaBoxa (§14.11.2). To usuwa linie slug i inne znaczniki drukarskie złożone jako tekst poza obszarem cięcia. ExtractLoadedPageGlyphs z premedytacją dalej zwraca każdy glif strumienia treści strony, więc ten materiał znajdziesz, gdy go potrzebujesz. Filtr to test pudełka, nie test widoczności: tekst ukryty ścieżką przycinającą, namalowany na biało albo przykryty obrazem nadal jest ekstrahowany
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// każdy glif strumienia treści strony, linia slug wliczona
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// tylko to, co strona pokazuje, z wklejonym tekstem Form XObject
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
Tekst malowany przez Form XObjects jest częścią tekstu strony od v2.768.3. Nagłówki, pieczątki i znaki wodne bardzo często mieszkają w formularzach, a niektóre dokumenty norm traciły przed zmianą 30 do 35 procent znaków. THotPDF.InterpretContentWithForms zapamiętuje każde Do razem z obowiązującym CTM, interpretuje formularz przy jego /Matrix razy to CTM (§8.10.1) i wkleja glify formularza w miejscu Do, rekurencyjnie zchodząc do zagnieżdżonych formularzy. Formularz bez własnych /Resources pożycza te od strumienia, który go maluje, na co pozwala §7.8.3. Glify formularzy niosą TokenIndex = -1, a ExtractLoadedPageGlyphs nadal zwraca wyłącznie glify strumienia strony, bo szukanie, zamiana i redakcja zapisują zmiany z powrotem przez TokenIndex i edytowałyby złe bajty, gdyby glif formularza się wkradł. Dwa uproszczenia warto znać: tekst formularza nie jest przycinany do /BBox formularza, a rekurencja stopuje na 12 poziomach, zamiast przez wykrywanie cykli, więc zdeformowany formularz malujący sam siebie powtarza swój tekst, aż dosięgnie tego limitu
Dlaczego tekst po operatorze Q dekodował się jako śmieci?
Tekst po Q mógł się dekodować źle przed v2.766.73, bo ekstraktor zapamiętywał na q tylko CTM. Parametry stanu tekstu, a więc font, rozmiar, Tc, Tw, Tz, TL, tryb renderowania i podniesienie, należą do stanu grafiki (§9.3.1), więc Q musi je przywrócić razem ze wszystkim innym na stosie (§8.4.2). Jeden branżowy raport wybierał dwubajtowy font Identity-H wewnątrz q … Q, a potem pokazywał jednobajtowy tekst WinAnsi bez własnego Tf. Ekstraktor trzymał wewnętrzny font, czytał kropki wiodące i słowo “Adobe” w spisie treści jako kody dwubajtowe i gubił 15% znaków strony. Stos q/Q interpretera trzyma teraz pełny stan tekstu. Reguły ekstrakcji opisane tutaj dotyczą każdej strony, więc cały dokument może pójść do pliku jednym wywołaniem
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// pusty zakres = wszystkie strony; form feed między stronami; BOM UTF-8
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
Którego API tekstowego HotPDF użyć?
ExtractLoadedPageText zostaje w kolejności strumienia treści, która jest właściwym domyślnym wyborem dla szukania i indeksowania; łańcuch dekodowania pod spodem jest omówiony w artykule o ekstrakcji tekstu z wczytanych PDF-ów w HotPDF. Dla dokumentów otagowanych, gdzie kolejność autorstwa ma znaczenie, ekstrakcja tekstu w kolejności struktury idzie drzewem struktury zamiast zgadywać z geometrii, a dla danych uwięzionych w tabelach typowana ekstrakcja tabel przez łamania stron zwraca komórki, a nie linie. Pełna referencja API i pobranie wersji próbnej są na stronie produktu HotPDF Delphi PDF Component