PDFium Component zamienia PDF o stałym układzie w semantyczny model, który można przepływowo przeformatować, za pomocą BuildReflowDocument, i eksportuje ten model jako samodzielny HTML przez ToHtml. Nagłówki pozostają nagłówkami, elementy list pozostają elementami list, a tabele wykryte na stronie wychodzą jako prawdziwy znacznik tabeli z zachowanymi komórkami nagłówkowymi i scaleniami. Nic w wyniku nie odwołuje się do zewnętrznego skryptu ani arkusza stylów
Powód, dla którego warto tego chcieć, jest taki, że strona PDF to zbiór pozycjonowanych glifów, co jest dokładnie złe dla ekranu telefonu, czytnika ekranu czy indeksu wyszukiwania. Każda próba rozwiązania tego przez wyodrębnienie zwykłego tekstu traci strukturę, która czyniła dokument czytelnym, a każda próba rozwiązania tego przez konwersję stron na obrazy traci tekst całkowicie. Model przepływowy zachowuje jedno i drugie: słowa oraz relacje między nimi
Skąd pochodzi informacja semantyczna?
Wszystko zaczyna się od GetStructuredText, jedynego źródła tekstu i semantyki w komponencie. Gdy PDF niesie drzewo struktury, czyli PDF znakowany zgodnie z definicją w ISO 32000-1, punkt 14.7, model podąża za logiczną hierarchią zarejestrowaną przez producenta. Gdy jej nie niesie, a większość PDF-ów spotykanych w praktyce jej nie niesie, model cofa się do fizycznej kolejności układu już obliczonej na potrzeby kolejności czytania
Ten wybór utrzymuje twardą granicę: nie wprowadza się drugiego parsera PDF ani drugiego silnika renderowania, by odpowiadać na pytania, na które już odpowiada istniejący. Mechanika kolejności czytania leżąca pod spodem jest opisana w blokach tekstu strukturalnego i kolejności czytania, a model przepływowy jest warstwą semantyczną na jej wierzchu, a nie jej zastąpieniem
Każdy węzeł rejestruje, skąd pochodzi jego informacja, dzięki czemu konsument może odróżnić nagłówek zadeklarowany przez dokument od nagłówka wywnioskowanego przez heurystyki układu. Potoki wrażliwe na pewność powinny odczytywać to pole zamiast traktować wszystkie węzły jako jednakowo wiarygodne
Płaskie drzewo i dlaczego nie jest drzewem obiektów
Model to spłaszczone drzewo w porządku prefiksowym: tablica węzłów, gdzie każdy węzeł niesie ParentIndex i Depth, zamiast rekurencyjnego rekordu albo grafu obiektów z relacją własności. Strony, nagłówki, akapity, listy, elementy list, ryciny, podpisy, tabele, wiersze i komórki — wszystko to mieszka w jednej liniowej tablicy
Stąd dwie korzyści. Konsumenci mogą przetwarzać strumieniowo tablicę w kolejności bez rekurencji, co sprowadza generowanie HTML, Markdown albo widoku drzewa do prostej pętli. A układ pozostaje przenośny między Delphi, C++Builder i Free Pascal, które różnią się sposobem obsługi rekurencyjnych typów zarządzanych na granicy ABI. Rekurencyjny rekord z tablicami dynamicznymi to dokładnie taki konstrukt, który kompiluje się wszędzie i w każdym miejscu zachowuje się subtelnie inaczej
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfReflowOptions;
Doc: TPdfReflowDocument;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.LoadDocument;
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.DetectTables := True;
Options.IncludeCss := True; // wbudowany blok stylów, bez zewnętrznego pliku
Options.MaxNodes := 200000; // budżet z bezpiecznym zamknięciem przy przekroczeniu
Options.MaxCharacters := 4000000;
Doc := Pdf.BuildReflowDocument(Options);
for I := 0 to High(Doc.Nodes) do
case Doc.Nodes[I].Kind of
prnkHeading:
Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
prnkParagraph:
Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Copy(Doc.Nodes[I].Text, 1, 60)]));
prnkTable:
Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
end;
Writeln(Format('%d node(s), %d table(s), %d character(s)',
[Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
finally
Pdf.Free;
end;
end;
Jak zapobiega się dwukrotnemu pojawieniu się tabel?
Wykrywanie tabel działa po zebraniu tekstu strukturalnego dla strony, co stwarza oczywiste zagrożenie: ta sama treść komórki istnieje jednocześnie w blokach tekstu i w wykrytej tabeli. Wyemitowanie obu daje HTML, w którym po każdej tabeli znów następuje jej własna zawartość jako luźne akapity
Reguła, która to rozwiązuje, jest geometryczna. Gdy wykryta tabela pokrywa więcej niż połowę obszaru bloku tekstu, węzeł tabeli zastępuje ten blok zamiast się z nim łączyć. Indeksowanie komórek wewnątrz wiersza jest budowane przez zliczanie do koszyków, więc budowanie modelu pozostaje liniowe względem liczby komórek plus wierszy, zamiast ponownie skanować każdą komórkę dla każdego wiersza, co ma znaczenie w dokumentach finansowych, gdzie jedna strona może nieść setki komórek
Wykryta struktura jest uczciwa co do tego, że jest wykryciem. Tabela z liniami siatki jest rozpoznawana bardziej niezawodnie niż taka wyrównana wyłącznie białymi znakami, a pewność węzła to odzwierciedla. Dla treści, gdzie błędna tabela jest lepsza niż brak tabeli, zostaw wykrywanie włączone; dla konwersji archiwalnej, gdzie błędna tabela jest gorsza, bramkuj po pewności
Eksport HTML, który pozostaje samodzielny
ToHtml przechodzi po już zbudowanym modelu i nigdy nie wraca do PDFium, więc dwukrotny eksport nic dodatkowo nie kosztuje i nie może dać innego wyniku z tego samego modelu. Wartości tekstowe i atrybutów są ujednolicone przez escapowanie, poziomy nagłówków są ograniczane do zakresu od h1 do h6, który HTML faktycznie definiuje, a komórki nagłówkowe, RowSpan i ColumnSpan przechodzą tak, jak zostały zapisane
Opcjonalny CSS to zwykły wbudowany blok stylów. Nie ma skryptu, nie ma fontu internetowego ani żadnego zewnętrznego zasobu jakiegokolwiek rodzaju, co sprawia, że wynik można bezpiecznie osadzić w wiadomości e-mail, przeglądarce pomocy czy piaskownicowej kontrolce przeglądarki:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // zachowaj widoczne granice stron
Options.PreserveLineBreaks := False; // pozwól przeglądarce zawijać akapity
Html := Pdf.BuildReflowDocument(Options).ToHtml;
Bytes := TEncoding.UTF8.GetBytes(string(Html));
Stream := TFileStream.Create('report.html', fmCreate);
try
if Length(Bytes) > 0 then
Stream.WriteBuffer(Bytes[0], Length(Bytes));
finally
Stream.Free;
end;
end;
PreserveLineBreaks to opcja najbardziej warta przemyślenia. Złamanie wiersza w PDF to decyzja składu podjęta dla stałej szerokości strony, więc zachowanie go na wąskim ekranie odtwarza dokładnie ten problem, który reflow ma rozwiązywać. Zachowuj złamania dla poezji, listingów kodu i adresów; usuwaj je dla prozy
Budżety, anulowanie i stan strony
Znaki, węzły, tabele i komórki mają każde swój pułap i każdy jest sprawdzany przed alokacją, a nie po niej, więc zniekształcony albo wrogi dokument zawodzi czysto zamiast zużywać pamięć, aż zrobi to coś innego. Token anulowania jest sprawdzany na granicach stron, bloków, tabel, wierszy i komórek, co utrzymuje responsywność anulowanego skanowania tysiącstronicowego dokumentu
Jedno zachowanie ma znaczenie specjalnie dla aplikacji GUI: całe skanowanie dokumentu działa wewnątrz zakresu, który przywraca aktywną stronę, więc sukces, awaria budżetu i anulowanie pozostawiają bieżącą stronę wywołującego nietkniętą. Przeglądarka pozwalająca użytkownikowi eksportować podczas przeglądania strony 340 po zakończeniu wciąż znajduje się na stronie 340
Do czego reflow się nadaje, a do czego nie
Wynik reflow to doskonałe wejście dla indeksowania wyszukiwania, dostępnych widoków czytania, wyświetlania mobilnego i migracji treści. To nie jest konwerter zachowujący wierność: pozycje bezwzględne, dokładne fonty, grafika wektorowa i precyzyjna geometria strony leżą poza jego celem z założenia. Gdy zadanie wymaga, by strona wyglądała tak samo, renderuj ją; gdy wymaga, by strona była czytelna gdzie indziej, przepuść ją przez reflow
Specjalnie dla technologii wspomagających model reflow łączy się z funkcjami czytania opisanymi w budowaniu dostępnego czytnika, a dokumenty niosące prawdziwe drzewo struktury dają zauważalnie lepsze modele, co jest dobrym argumentem za walidacją znakowania wcześniej w potoku, jak opisano w walidacji drzewa struktury PDF/UA
Reflow, tekst strukturalny, walidacja znakowania i renderowanie współdzielą jeden obiekt dokumentu w Delphi, C++Builder i Lazarus; pełne API jest opisane na stronie PDFium Component dla Delphi