Artykuł techniczny

Linearizacja PDF dla Fast Web View z PDFium w Delphi

PDFium Component produkuje Fast Web View (zlinearizowane) PDF-y z Delphi przez TPdf.SaveAsLinearized. Metoda zapisuje bieżący dokument przez PDFium, a potem przepisuje wynik zgodnie z ISO 32000-1 Annex F: obiekty pierwszej strony idą na przód, zapisywane są dwie sekcje odwołań krzyżowych, a tablice podpowiedzi offsetów stron i obiektów współdzielonych mówią przeglądarce, które zakresy bajtów pobrać dla dowolnej strony

Powodem, dla którego ktokolwiek się tym przejmuje, jest sieć. Katalog 40 MB serwowany z object storage albo portalu dokumentów powinien pokazać stronę pierwszą po pierwszych kilobajtach, a skok do strony 380 powinien kosztować parę żądań HTTP Range, nie pełne pobranie. Przeglądarki potrafią to tylko, gdy plik jest zlinearizowany. Większość PDF-ów wychodzących z potoku edycji nie jest, bo naturalny sposób pisania PDF-a kładzie tabelę odwołań na końcu i rozrzuca obiekty stron tam, gdzie akurat writer je wyemitował

Dlaczego PDFium czyta pliki zlinearizowane, ale ich nie zapisuje?

PDFium ma kompletnego czytnika linearizacji i żadnego writera. API dostępności FPDFAvail_* wykrywa słownik linearizacji, dekoduje tablice podpowiedzi i prosi gospodarza dokładnie o te zakresy bajtów, których potrzebuje, na czym stoi workflow pobierania progresywnego i anulowania w PDFium Component. Publiczne ABI zapisu oferuje z kolei tylko zwykły zapis kopii i zapis z docelową wersją PDF. Nic w nim nie porządkuje obiektów ani nie emituje strumieni podpowiedzi

PDFium Component zamyka tę lukę, nie reimplementując tego, co PDFium już robi dobrze. TPdf.SaveAsLinearized najpierw woła istniejące SaveAs, więc PDFium dalej włada zapisem semantycznym: edytowane strony, stan pól formularza i obsługa wersji idą przez ścieżkę kodu ćwiczoną od lat. Komponent puszcza potem przebieg przebudowy Annex F po tym stabilnym pośrednim PDF i zapisuje wynik zlinearizowany do twojego celu. Podział pracy w ten sposób znaczy, że linearizacja nigdy nie musi rozumieć adnotacji ani formularza, tylko obiekty, odwołania i offsety bajtowe

Wywoływanie TPdf.SaveAsLinearized z Delphi

TPdf.SaveAsLinearized ma dwa przeciążenia, jedno dla TStream i jedno dla nazwy pliku, i oba biorą te same opcjonalne parametry Option: TSaveOption i PdfVersion: TPdfVersion co SaveAs. Oba zwracają True przy sukcesie. Przeciążenie plikowe zapisuje do pliku tymczasowego w katalogu celu i podmienia cel atomowo, więc nieudany zapis nigdy nie zostawia półnapisanego PDF-a tam, gdzie stał dobry

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.Active := True;
    // ... edytuj strony, wypełniaj formularze, stempluj, co tam potok robi ...
    if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

Reguły parametrów są ścisłe z zamierzenia. saIncremental podnosi błąd, bo aktualizacja przyrostowa dokleja nową rewizję na koniec pliku i natychmiast zniszczyłaby układ z przodu załadowany. Docelowa wersja poniżej PDF 1.2 też jest odrzucana; pvUnknown trzyma wersję źródła. Zaszyfrowane wejście potrzebuje saRemoveSecurity, bo writer przenumerowuje obiekty, a klucze szyfrowania poziomu obiektów PDF są wyprowadzane z numeru obiektu. A bo linearizacja to pełny rewrite, każda istniejąca sygnatura cyfrowa traci ważność. Jeśli musisz dostarczyć podpisane i zlinearizowane, zlinearizuj najpierw, a podpisz potem

Jak writer grupuje obiekty dla Fast Web View

Linearizacja to nie tylko przeniesienie strony pierwszej na szczyt pliku. Writer parsuje bieżącą rewizję, czy jej dane odwołań to klasyczna tabela, plik hybrydowy, czy strumień odwołań, i rozwija każdego członka aktywnego strumienia obiektów do osobnego obiektu. Potem buduje graf odwołań i sortuje obiekty do pięciu grup: obiekty poziomu dokumentu potrzebne do otwarcia pliku, obiekty pierwszej strony, obiekty prywatne jednej późniejszej strony, obiekty współdzielone przez kilka późniejszych stron i wszystko inne

Pułapką w tym przechodzeniu grafu jest /Parent. Każdy słownik strony wskazuje w górę drzewa stron, a naiwne przejście idzie tym wskaźnikiem do węzła rodzica, potem w dół przez /Kids do każdej strony siostrzanej i jej strumieni treści. Krok później grupa pierwszej strony zawiera cały dokument, a plik jest technicznie zlinearizowany i bezużyteczny dla dostępu range-first. PDFium Component ignoruje łącze /Parent strony i węzły drzewa stron poza zbieraną stroną, co trzyma sekcję pierwszej strony małą. Na 51-stronicowej próbce regresyjnej z głębokim drzewem stron sekcja pierwszej strony ma 5289 bajtów, 0,48% wyjścia o 1105354 bajtach

Strumienie obiektów i strumienie odwołań w wejściu są spłaszczane do zwykłych obiektów pośrednich generacji 0, każde odwołanie jest przepisywane na nowe numery, a wyjście używa klasycznych sekcji odwołań krzyżowych. Kosztuje to trochę rozmiaru pliku wobec skompresowanych strumieni obiektów, ale upraszcza pokrycie tablic podpowiedzi i jest układem, który każda przeglądarka świadoma linearizacji ogarnia. Jeśli kompaktowość znaczy dla danego wyjścia więcej niż Fast Web View, PDFium Component ma do tej roboty osobny writer strumieni obiektów

Co wchodzi do słownika linearizacji i dwóch sekcji xref?

Zlinearizowany plik napisany przez PDFium Component niesie dwie klasyczne sekcje odwołań krzyżowych i słownik linearizacji w samym pierwszym obiekcie. Pierwsza sekcja xref idzie za słownikiem linearizacji i pokrywa obiekty poziomu dokumentu i pierwszej strony, więc przeglądarka potrafi wyrenderować stronę pierwszą z samego przodu pliku. Główna sekcja xref siedzi na końcu i pokrywa wszystko inne

Sam słownik linearizacji trzyma liczby, których przeglądarka potrzebuje, zanim przeczyta cokolwiek innego: /L długość pliku, /H offset i długość strumienia podpowiedzi, /O numer obiektu pierwszej strony, /E koniec sekcji pierwszej strony, /N liczbę stron i /T offset pierwszego wpisu w głównej tabeli xref. Wszystko to zależy od końcowego układu, a słownik siedzi przed układem. Zapisanie go liczbami naturalnej szerokości tworzy pętlę: długość rosnąca z 999999 do 1000000 dodaje bajt, przesuwa każdy kolejny offset, co może zmienić szerokość innej liczby, i tak dalej

PDFium Component łamie pętlę, rezerwując stałe 320 bajtów na słownik linearizacji. Każdy obiekt, strumień podpowiedzi, obie sekcje xref i przyczepy są zapisywane najpierw; gdy wszystkie offsety są ostateczne, słownik jest wypełniany w miejscu i dociągany wypełnieniem do zarezerwowanej szerokości. Subtelny szczegół to /T: musi wskazywać na wpis obiektu 0 głównej tabeli xref, nie na słowo kluczowe xref, które je poprzedza. Ścisłe walidatory flagują offset słowa kluczowego, mimo że pobłażliwe przeglądarki i tak otwierają plik

Dlaczego offsety tablic podpowiedzi ignorują strumień podpowiedzi?

Offsety tablic podpowiedzi w zlinearizowanym PDF są interpretowane tak, jakby strumienia podpowiedzi w pliku nie było. Annex F tak to definiuje, żeby strumień podpowiedzi mógł siedzieć między sekcją pierwszej strony a resztą bez sprzężenia własnego rozmiaru z powrotem do liczb, które zawiera. Praktyczną konsekwencją dla writera jest łatwe do sparcia: każdy prawdziwy offset leżący za strumieniem podpowiedzi musi być zredukowany, zanim trafi do tablicy podpowiedzi offsetów stron albo obiektów współdzielonych

Pytanie brzmi, o ile zredukowany. Kusząca odpowiedź to długość danych strumienia, bo to część, która czuje się jak treść podpowiedzi. Poprawna odpowiedź to długość całego obiektu pośredniego podpowiedzi, włącznie z N 0 obj, słownikiem strumienia, słowami kluczowymi stream i endstream oraz endobj. Odejmij tylko długość danych, a każdy późniejszy offset jest zepsuty o kilkadziesiąt bajtów. Strona pierwsza dalej się renderuje, bo mieszka przed strumieniem podpowiedzi, więc błąd przeżywa szybki sprawdzian wzrokowy i wychodzi dopiero, gdy przeglądarka skacze do późniejszej strony i prosi o zakres startujący w środku złego obiektu

Obie tablice podpowiedzi są bit-packowane od najbardziej znaczącego bitu, znowu zgodnie z Annex F. Komponent weryfikuje wynik tak, jak zrobiłby to prawdziwy klient: rzadki magazyn zakresów dostarcza tylko pierwsze 1 KiB 51-stronicowej próbki, a natywna obsługa podpowiedzi PDFium prosi o resztę. Ostatnia strona otwiera się, zanim pełny plik kiedykolwiek zaistnieje

Weryfikacja zlinearizowanego PDF w Delphi

Najtańsze sprawdzenie to zapytać samego PDFium. TPdf.InspectAvailability puszcza API dostępności wobec strumienia bez ładowania dokumentu, a jego pole Linearization raportuje plnLinearized, plnNotLinearized albo plnUnknown

function IsFastWebView(const FileName: string): Boolean;
var
  Stream: TFileStream;
  Info: TPdfAvailability;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Info := TPdf.InspectAvailability(Stream);
    Result := Info.Linearization = plnLinearized;
  finally
    Stream.Free;
  end;
end;

To potwierdza, że PDFium rozpoznaje układ; nie dowodzi, że podpowiedzi są poprawne. Do prawdziwego testu end-to-end serwuj plik przez transport ograniczony do zakresów i otwórz późną stronę przez TPdfProgressiveDocument, tak samo jak robi to przewodnik po strumieniowaniu dużych PDF-ów na żądanie z PDFium VCL. Jeśli strona się renderuje po pobraniu tylko małego ułamka pliku, tablice podpowiedzi robią swoją robotę. Dodanie drugiego, niezależnego sprawdzacza linearizacji do potoku wydania też się opłaca, bo przeglądarki różnią się pobłażliwością wobec źle postawionego /T albo lekko krzywego offsetu

Dwa limity do zapamiętania. Materializator obiektów wciąż odrzuca tablice od 2 GiB wzwyż, więc ekstremalnie duże dokumenty potrzebują na razie innej strategii. I linearizacja opłaca się tylko, gdy serwer honoruje żądania HTTP Range; za proxy obcinającym Accept-Ranges zlinearizowany plik pobiera się dokładnie jak każdy inny

FAQ

Czy TPdf.SaveAsLinearized trzyma sygnatury cyfrowe ważnymi?

Nie. TPdf.SaveAsLinearized przenumerowuje i przepisuje każdy obiekt, więc istniejące sygnatury tracą ważność. Najpierw zlinearizuj, potem nałóż sygnaturę

Czy PDFium Component zlinearizuje zaszyfrowany PDF?

Tylko, gdy przekażesz saRemoveSecurity. Klucze szyfrowania poziomu obiektów zależą od numerów obiektów, które writer zmienia, więc wyjście jest zapisywane bez szyfrowania

Które wersje PDF wspiera zlinearizowane wyjście?

TPdf.SaveAsLinearized przyjmuje pvUnknown, żeby trzymać wersję źródła, albo dowolny cel od PDF 1.2 wzwyż, i odrzuca saIncremental, bo doklejona rewizja złamałaby układ

Wyjście Fast Web View, ładowanie progresywne i API dostępności są częściami PDFium Component dla Delphi i C++Builder, który opakowuje PDFium dla projektów Delphi, C++Builder i Lazarus