HotPDF zapisuje zlinearyzowane pliki PDF, układ, który Acrobat nazywa Fast Web View, za pomocą właściwości LinearizeOutput na THotPDF. Ustawienie jej przed BeginDoc sprawia, że HotPDF przeporządkowuje gotowy graf obiektów tak, by czytnik świadomy zakresów bajtów mógł wyświetlić stronę pierwszą po pobraniu tylko początkowej części pliku, zamiast najpierw ściągać cały dokument. Mechanizm to ISO 32000-1 Annex F
Powód, dla którego to ma znaczenie, jest mało efektowny. Zwykły PDF umieszcza swoją tablicę cross-reference na końcu, więc przeglądarka musi dotrzeć do ostatniego bajtu, zanim będzie wiedziała, gdzie cokolwiek jest. Podaj przeglądarce 200-stronicowy zeskanowany raport, a użytkownik patrzy na spinner przez cały transfer, mimo że chciał tylko strony 1. Linearyzacja to naprawia, płacąc koszt w momencie zapisu. Ten artykuł dotyczy konkretnie tej ścieżki zapisu, podziału, pętli pomiarowej i twardych limitów; tło koncepcyjne o tym, co daje Fast Web View, pokrywa wcześniejsze wyjaśnienie linearyzacji PDF i Fast Web View
Co faktycznie gwarantuje zlinearyzowany układ
Zlinearyzowany plik to zwykły PDF z bardzo konkretnym uporządkowaniem fizycznym, a każda gwarancja, jaką oferuje, wynika z tego uporządkowania, nie z żadnego nowego typu obiektu. HotPDF emituje części w sekwencji przepisanej przez Annex F: słownik parametrów linearyzacji w pierwszych 1024 bajtach, wczesną tablicę cross-reference, obiekty na poziomie dokumentu, główny strumień podpowiedzi, pierwszą stronę i jej prywatne obiekty, potem pozostałe strony, potem obiekty współdzielone, potem wszystko inne, i na końcu główną tablicę cross-reference
Podział jest wyprowadzany, nie deklarowany. HotPDF przechodzi graf referencji od każdego obiektu strony i zapisuje, dla każdego obiektu pośredniego, ile stron do niego dociera i która strona dotarła pierwsza. Obiekt użyty przez dokładnie jedną stronę staje się dla niej prywatny. Obiekt osiągalny z więcej niż jednej strony staje się współdzielony. Katalog, plus wszystko, na co wskazuje pod /ViewerPreferences, /OpenAction, /Threads i /AcroForm, plus słownik szyfrowania, gdy ochrona jest aktywna, tworzą grupę na poziomie dokumentu, która musi poprzedzać wszystko inne. Węzły drzewa stron są celowo wstrzymywane, by nie zanieczyszczały sekcji pierwszej strony
Słownik parametrów niesie liczby, których czytnik potrzebuje, zanim przeczyta cokolwiek innego: /L dla całkowitej długości pliku, /H dla przesunięcia i długości strumienia podpowiedzi, /O dla numeru obiektu pierwszej strony, /E dla bajtu, w którym kończy się sekcja pierwszej strony, /N dla liczby stron oraz /T dla przesunięcia wpisu głównej tablicy cross-reference. Każda z tych wartości to przesunięcie bajtowe w pliku, który jeszcze nie istnieje w momencie, gdy trzeba je zapisać
Dlaczego przesunięcia tabeli podpowiedzi muszą być zbieżne?
Ponieważ liczby w słowniku parametrów opisują plik, który je zawiera, a zmiana którejkolwiek z nich zmienia plik. To centralna trudność zlinearyzowanego zapisu i dlatego HotPDF mierzy wielokrotnie zamiast pisać jednorazowo. Rozszerz /T z 6 cyfr do 7, a słownik parametrów rośnie o bajt; nagłówek rośnie; każdy obiekt się przesuwa; główna tablica cross-reference się przesuwa; /T potrzebuje teraz innej wartości. Układ musi osiągnąć punkt stały, zanim choćby jeden bajt rzeczywistego wyniku zostanie zatwierdzony
HotPDF radzi sobie z tym przez ograniczoną iterację. Najpierw serializuje każdy obiekt do strumienia liczącego, który rejestruje długość bez zachowywania bajtów, więc każdy obiekt ma znany rozmiar zserializowany. Następnie uruchamia przebieg układu, który przypisuje przesunięcia grupie na poziomie dokumentu, strumieniowi podpowiedzi, grupie pierwszej strony, grupom kolejnych stron, grupie współdzielonej i reszcie, i raportuje, gdzie wylądowałaby główna tablica cross-reference. Ten wynik jest podawany z powrotem jako wejście do kolejnego przebiegu. Pętla jest ograniczona do ośmiu prób, a brak zbieżności zgłasza wyjątek zamiast produkować plik z prawdopodobnie wyglądającymi, lecz błędnymi przesunięciami
CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
CalculateLayout(CandidateMainOffset, FirstXRefData,
HintOffset, EndFirstPage, NewMainOffset);
if NewMainOffset = CandidateMainOffset then
Break;
CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
raise Exception.Create('Linearization layout did not converge');
Dwa szczegóły powstrzymują pętlę przed miotaniem się. Słownik parametrów jest zapisywany w stałym slocie 384 bajtów, dopełnianym spacjami, więc jego własny wzrost nigdy nie może zdestabilizować układu; gdyby tekst słownika kiedykolwiek przekroczył tę rezerwację, HotPDF zgłasza wyjątek zamiast po cichu przesuwać wszystko. A po osiągnięciu zbieżności HotPDF uruchamia jeszcze jeden potwierdzający przebieg układu i ponownie sprawdza długość strumienia podpowiedzi, ponieważ sam strumień podpowiedzi koduje przesunięcia, które były znane dopiero, gdy układ się ustabilizował. Zyskiem z całego tego mierzenia jest to, że HotPDF nigdy nie buforuje drugiej kopii dokumentu: gdy przesunięcia są już ustalone, obiekty są serializowane prosto do strumienia docelowego, z asercją na każdej granicy sekcji, że zapisane bajty odpowiadają obiecanemu przesunięciu
Włączanie tego z Delphi
Powierzchnia API to jeden Boolean, a jedynym wymogiem jest ustawienie go przed rozpoczęciem generowania. LinearizeOutput domyślnie ma wartość False, a przebieg układu uruchamia się w momencie zapisu dokumentu, więc przypisanie go po EndDoc niczego nie osiąga
var
PDF: THotPDF;
begin
PDF := THotPDF.Create(nil);
try
PDF.FileName := 'fast-view.pdf';
PDF.Version := pdf17;
PDF.LinearizeOutput := True; // must precede BeginDoc
PDF.BeginDoc;
PDF.Canvas.TextOut(72, 72, 'First page');
PDF.EndDoc;
finally
PDF.Free;
end;
end;
Jedno zastrzeżenie wdrożeniowe przebija wszystko po stronie kodu. Linearyzacja opłaca się tylko wtedy, gdy warstwa transportu obsługuje żądania HTTP range. Podaj ten sam plik z endpointu, który strumieniuje go w całości, albo z konfiguracji CDN ignorującej Range, a kupisz sobie wolniejszą ścieżkę zapisu i większy plik bez żadnej korzyści widocznej dla użytkownika. Sprawdź serwer, zanim sprawdzisz kod
Dlaczego linearyzacja nadpisuje UseXRefStream i UseObjectStreams?
Ponieważ zlinearyzowany zapis potrzebuje, by każdy obiekt miał własne, bezpośrednio adresowalne przesunięcie bajtowe, a obie te funkcje to odbierają. HotPDF emituje więc tradycyjne tekstowe tablice cross-reference i rozpakowane obiekty pośrednie zawsze, gdy LinearizeOutput jest włączone, nawet jeśli wywołujący ustawił też UseXRefStream lub UseObjectStreams. To celowe nadpisanie, nie konflikt, który trzeba rozwiązać samodzielnie
Rozumowanie wynika z tabel podpowiedzi. Tabela podpowiedzi opisuje, gdzie zaczyna się sekcja strony i jak jest długa, więc czytnik może zażądać dokładnie tego zakresu. Obiekt spakowany do kontenera /ObjStm nie ma żadnego niezależnego przesunięcia; istnieje wyłącznie jako wycinek wewnątrz innego skompresowanego strumienia, który trzeba pobrać i rozpakować jako całość. Jeśli liczyłeś na strumienie obiektów dla rozmiaru pliku, wiedz, że linearyzacja i kompresja ciągną tutaj w przeciwnych kierunkach — kompromis opisuje towarzyszący tekst o strumieniach obiektów i aktualizacjach przyrostowych w HotPDF. Ta sama tensja kształtuje pliki z hybrydowym cross-reference, które istnieją właśnie po to, by starsze czytniki działały obok tablic opartych na strumieniach, co omawia artykuł o hybrydowych strumieniach cross-reference w PDF-ach generowanych przez Office
Jest też dolny próg wersji. Linearyzacja wymaga PDF 1.2 lub nowszego. Jeśli wybrana wersja jest starsza, HotPDF podnosi ją automatycznie, chyba że ustawiono StrictVersionLock, w takim przypadku zapis zgłasza wyjątek zamiast po cichu awansować dokument, który celowo przypiąłeś
Ściana 4 GiB i dlaczego HotPDF odmawia zamiast obcinać
Tabele podpowiedzi linearyzacji przechowują przesunięcia jako wartości 32-bitowe, więc zlinearyzowany plik nie może adresować niczego przy lub powyżej 4 GiB, a HotPDF odrzuca taki wynik jawnym wyjątkiem zamiast zapisywać plik z zawiniętymi przesunięciami. Limit nie jest wyborem implementacyjnym HotPDF; to szerokość pól zdefiniowanych przez Annex F
Sprawdzenie jest stosowane w trzech miejscach i wszystkie trzy mają znaczenie. HotPDF waliduje każdy obiekt, gdy tylko znana jest jego zserializowana długość, waliduje długość każdej sekcji strony podczas budowania wpisów podpowiedzi i waliduje ostateczną długość pliku po ustaleniu rozmiaru głównej tablicy cross-reference. Wczesna porażka to cały sens: tabela podpowiedzi z po cichu obciętym przesunięciem produkuje plik, który otwiera się poprawnie w czytniku pobierającym go w całości i zawodzi tylko dla klienta korzystającego z zakresów bajtów, dla którego linearyzacja w ogóle istniała, co jest najgorszym możliwym trybem awarii, ponieważ Twoja przeglądarka testowa nigdy tego nie odtworzy. Jeśli produkujesz wynik wielogigabajtowy, linearyzacja nie jest właściwym narzędziem, a kierunkiem, w który warto spojrzeć, jest podejście strumieniowe opisane w notatkach o Direct File API dla dużych przepływów pracy PDF
Wykrywanie linearyzacji we wczytanym pliku
THotPDF.IsLoadedLinearized raportuje, czy aktualnie wczytany dokument został już zapisany w formie zlinearyzowanej, i odpowiada na podstawie migawki wziętej przed parsowaniem, nie z aktywnego strumienia. HotPDF czyta pierwsze 1024 bajty od pozycji zero strumienia źródłowego, skanuje je w poszukiwaniu pierwszego słowa kluczowego obj, a potem wpisu /Linearized z wartością 1, i buforuje wynik jako boolean
var
PDF: THotPDF;
PageCount: Integer;
begin
PDF := THotPDF.Create(nil);
try
PageCount := PDF.LoadFromFile('incoming.pdf');
if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
Writeln('Source is not Fast Web View ready');
finally
PDF.Free;
end;
end;
Dwa ograniczenia w tym opisie są kluczowe. Wykrywanie nie może polegać na pozycji strumienia, ponieważ zanim kod aplikacji zada pytanie, parser już ją przesunął, i nie może ponownie czytać na żądanie, ponieważ LoadFromFile zwalnia wewnętrzny strumień źródłowy po zakończeniu wczytywania. Stąd projekt „przechwyć-przed-parsowaniem-i-zbuforuj”. Skanowanie jest też celowo dosłowne co do wartości: akceptowane jest wyłącznie /Linearized 1 albo forma numerycznie równoważna z całkowicie zerową częścią ułamkową, ponieważ plik, którego słownik parametrów mówi coś innego, nie składa obietnicy z Annex F
Pułapka rekordów Delphi warta zapamiętania
Lokalne rekordy zawierające tablice dynamiczne inicjalizują swoje pola zarządzane i nic więcej, a jeśli obok tablicy trzymasz zwykłe pole Count, musisz je wyczyścić samodzielnie. To ugryzło podział linearyzacji podczas developmentu i jest to rodzaj błędu, który kosztuje dzień właśnie dlatego, że jedna platforma go ukrywa
type
THPDFLinearIndexList = record
Values: THPDFIntegerArray; // managed field: cleared for you
Count: Integer; // plain field: whatever was on the stack
end;
// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);
Pole tablicy dynamicznej jest zliczane referencyjnie, więc kompilator je zeruje. Count obok niego to zwykły integer bez takiej gwarancji, a niezainicjalizowany Count wysyła sam pierwszy append pod dowolny indeks. Pod Win32 slot na stosie akurat trzymał zero, append lądował pod indeksem 0 i każdy test przechodził. Pod Win64 ten sam kod pisał poza koniec tablicy. Lekcja uogólnia się daleko poza linearyzację: gdy rekord miesza pola zarządzane i niezarządzane, przypisz Default(TRecord) i przestań rozważać, które pola kompilator pokrywa, i nigdy nie traktuj zielonego uruchomienia pod Win32 jako dowodu poprawnej inicjalizacji
Składowe LinearizeOutput i IsLoadedLinearized opisane tutaj są dostarczane ze standardowym HotPDF Component dla Delphi i C++Builder; strona produktu niesie pełną dokumentację właściwości, wraz z regułami interakcji ze strumieniami cross-reference, strumieniami obiektów i blokadą wersji