Artykuł techniczny

Strumienie obiektów i aktualizacje przyrostowe PDF w Delphi

PDF 1.5 wprowadził dwie struktury składowania, których wcześniejszy format pliku nie potrafił wyrazić: strumień obiektów i strumień odsyłaczy. Strumień obiektów to jeden kontener skompresowany metodą Flate, oznaczony /Type /ObjStm, który mieści wiele małych obiektów pośrednich upakowanych jeden za drugim, zamiast rozsypywać je po ciele pliku. Strumień odsyłaczy to tablica wyszukiwania pliku przepisana jako skompresowane dane binarne z polami o zmiennej szerokości, w miejsce tablicy ASCII o stałej szerokości, która zamykała każdy PDF aż do wersji 1.4. Podróżują razem. Gdy obiekty zostaną zwinięte do strumienia, stara tablica tekstowa nie potrafi już ich zaadresować, więc binarny xref musi jej towarzyszyć

Zestaw to z klasycznym układem, a koszt, który to usuwa, staje się łatwy do dostrzeżenia. W pliku PDF 1.4 każdy obiekt pośredni siedzi nieskompresowany za własnym nagłówkiem obj, a tablica na końcu wydaje dokładnie 20 bajtów ASCII na wpis, bez prawa do kompresji. Dokument z 200 000 obiektów niesie mniej więcej 4 MB danych odsyłaczy, zanim narysowany zostanie choćby jeden glif, a na tym piętrzą się wszystkie nieskompresowane ciała słowników. PDF 1.5 atakuje obie liczby naraz: słowniki zwijają się do kontenerów Flate, a tablica o rozmiarze 4 MB kurczy się do kilkuset kilobajtów danych binarnych. ISO 32000-1 definiuje obie struktury w §7.5.7 i §7.5.8

Zestawienie układów plików w HotPDF porównujące nieskompresowane obiekty PDF 1.4 i tablice xref w ASCII ze skompresowanymi strumieniami obiektów PDF 1.5 i binarnym strumieniem xref
Zwinięte słowniki i binarny strumień xref zbijają megabajty narzutu strukturalnego, podczas gdy treść stron i dane obrazów zachowują kompresję, którą już miały — najwięcej zyskują pliki gęste od struktury

Gdzie faktycznie ląduje oszczędność

Strumienie obiektów dotykają wyłącznie obiektów niebędących strumieniami, więc kompresują strukturę, a nie piksele. Treść stron była kompresowana metodą Flate już przed 1.5, a dane obrazów niosą własne kodeki, i dlatego broszura gęsta od obrazów ledwie drgnie. Pliki, które się zapadają, to te gęste od struktury: AcroForms z tysiącami słowników pól, głębokie drzewa konspektu, elementy struktury oznaczonego PDF. Te obiekty są maleńkie, liczne i niemal identyczne wzajemnie, a właśnie tę powtarzalność wykorzystuje Flate, gdy siedzą w jednym buforze, a nie rozrzucone po ciele pliku z wciśniętymi między nie nagłówkami

Łatwo nie docenić, jak dużo starego pliku stanowi narzut. Archiwum formularzy, które wchłonęło lata edycji, potrafi wydać znacznie ponad połowę swoich bajtów na nagłówki słowników, wypełnienie xref i rewizje, na które żaden czytnik nigdy nie spojrzy. Dwie opisane tu funkcje odzyskują pierwsze dwa z tych kosztów. Trzeci, nagromadzone rewizje, ustępuje dopiero przy zagęszczaniu, gdy plik nie musi już pamiętać własnej historii

W HotPDF włączasz obie przez parę właściwości, a to, jak od siebie zależą, znaczy więcej niż kolejność, w jakiej je zapiszesz:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.UseXRefStream := True;      // binarny xref, warunek konieczny dla ObjStm
    Pdf.UseObjectStreams := True;   // pakuj obiekty do /Type /ObjStm
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
    Pdf.EndDoc;                     // emituje kontenery XRefStm + ObjStm
  finally
    Pdf.Free;
  end;
end;

UseObjectStreams wymaga UseXRefStream ustawionego na True. Do skompresowanego obiektu dociera się przez wpis xref typu 2, który zapisuje numer strumienia obiektów plus indeks, a klasyczny 20-bajtowy wiersz tekstowy nie ma gdzie przechować tej pary. Dlatego samo UseObjectStreams nie robi nic widocznego; działającą konfiguracją są obie flagi ustawione przed BeginDoc. Ustaw je po BeginDoc, a HotPDF już zobowiązał się do starszego układu

Dlaczego obie są domyślnie wyłączone

HotPDF zostawia obie właściwości jako False od razu po wyjęciu z pudełka, a powód ujawnia się w integracjach ze starym kodem dalej w łańcuchu. Czytnik rozumiejący wyłącznie PDF 1.4 nie ogłasza, że nie radzi sobie ze skompresowanymi obiektami. Napotyka strumień xref, nie znajduje żadnego ze słów kluczowych zwiastuna, których oczekuje, i zgłasza uszkodzoną tablicę odsyłaczy albo po prostu odmawia otwarcia pliku. Jeśli twoje wyjście płynie do starzejącej się bramki faksowej, drukarki sprzętowej z wbudowanym interpreterem albo parsera, który ktoś napisał pod specyfikację 1.4 dekadę temu, trzymaj obie flagi wyłączone dla tego kanału i pogódź się z większym plikiem. Dla składowania archiwalnego i dostarczania przez sieć, gdzie każda główna przeglądarka czyta PDF 1.5 od dwudziestu lat, ich włączenie to kompresja niemal za darmo

Jest jeszcze efekt drugiego rzędu, o którym warto powiedzieć zespołowi wsparcia. Gdy słowniki zostaną upakowane w strumienie obiektów, porównywanie dwóch wygenerowanych plików bajt po bajcie przestaje cokolwiek znaczyć, ponieważ zmiana jednego pola potrafi ponownie skompresować cały kontener i przetasować wszystko po nim. Porównuj takie pliki po treści obiektów, a nie porównaniem binarnym

Aktualizacje przyrostowe i przesunięcia bajtowe, które chronią

Podpis cyfrowy obejmuje jawny /ByteRange: dwa przedziały pliku fizycznego, podane jako bezwzględne przesunięcia bajtowe, z których wzięto skrót CMS. Przepisz plik, choćby na coś wyglądającego identycznie na ekranie, a wszystkie te przesunięcia się przesuną. Skrót przestaje pasować, a podpis czyta się jako zepsuty. To dokładnie ten problem, który ISO 32000-1 §7.5.6 rozwiązuje aktualizacjami przyrostowymi. Nowe i zmienione obiekty są dopisywane za istniejącym %%EOF, a potem zapisywana jest świeża sekcja odsyłaczy, której wpis /Prev wskazuje wstecz na poprzednią. Oryginalne bajty nigdy nie zostają naruszone, więc podpisana rewizja pozostaje weryfikowalna, a Acrobat może przedstawić każdą podpisaną rewizję osobno w panelu podpisów

HotPDF udostępnia to przez własny punkt wejścia:

Trzy rewizje HotPDF dopisywane jedna za drugą, powiązane wpisami Prev w xref, przy nadal walidujących się skrótach oryginalnego ByteRange
Dopisywane rewizje wiążą się wstecz przez wpisy Prev i nigdy nie dotykają bajtów objętych skrótem podpisu, więc każda wcześniejsza podpisana rewizja nadal się waliduje, a plik rośnie wyłącznie w prawo
Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf');  // dopisuje wyłącznie różnicę

Dwie rzeczy podkładają ludziom nogę. BeginIncrementalUpdate musi dostać nazwę oryginalnego pliku, ponieważ dopisywana sekcja xref zapisuje przesunięcia mające sens wyłącznie względem dokładnie tych oryginalnych bajtów; wskaż mu kopię przemianowaną albo zapisaną na nowo, a przesunięcia opisują plik, który już nie istnieje. A zapis jest z konstrukcji tylko dopisujący, więc wyjście jest zawsze większe od wejścia. Ten przyrost nie jest marnotrawstwem do wystrojenia. To ta sama właściwość, która pozostawia wcześniejsze podpisane rewizje nietknięte

Modyfikowanie wczytanego pliku idzie przez LoadFromFile

Programiści, którzy poznali HotPDF najpierw przez jego API generowania, wpadają zwykle na pewną ścianę. BeginDoc otwiera zupełnie nowy dokument, co jest złym narzędziem, gdy zamierzasz zmienić dokument już istniejący. Edycja istniejącego pliku biegnie zamiast tego przez wywołania wczytanego dokumentu:

PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5);  // strony 1-3 po stronie 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');

Pomieszaj te dwa światy, a objawem będzie plik wyjściowy zawierający twoją nową treść i nic z oryginału, ponieważ BeginDoc ochoczo zbudował świeży dokument obok tego, który myślałeś, że edytujesz. Czytaj LoadFromFile z SaveLoadedDocument jako jedno słownictwo, a BeginDoc z EndDoc jako drugie. Procedura sięgająca po oba wobec tego samego pliku jest niemal zawsze błędna

Dwa słownictwa zapisu w HotPDF, gdzie BeginDoc tworzy nowy plik, a LoadFromFile wraz z SaveLoadedDocument edytuje ten istniejący
Para do generowania buduje zupełnie nowy dokument, a para wczytanego dokumentu edytuje to, co już jest na dysku — mieszanie ich to powód, dla którego edycje wychodzą czasem bez żadnej z oryginalnych stron

Kiedy zagęścić dopisywany plik

Zapisywanie tylko przez dopisywanie niesie powolny koszt. Nocne zadanie stemplujące jeden wiersz statusu na tym samym pliku PDF produkuje 365 rewizji w ciągu roku, a każda rewizja ciągnie za sobą nową sekcję xref. Gdy ta historia przeżyje swoją użyteczność i żaden podpis w pliku nie musi przetrwać, możesz spłaszczyć całość, serializując ją ponownie ścieżką wczytanego dokumentu:

Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');

Ten ponowny zapis jest pełnym przepisaniem. Celowo wyrzuca wcześniejsze rewizje i psuje każdy podpis wciąż obecny w pliku, więc postaw go za tą samą bramką polityki, którą stosujesz do każdego innego kroku destrukcyjnego. Jedna produkcyjna zasada, która się broni: zagęszczaj, gdy liczba rewizji przekroczy próg albo gdy dopisany narzut urośnie ponad jakiś udział pliku bazowego, i nigdy nie zagęszczaj dokumentu, którego panel podpisów ma cokolwiek w środku

Sprawdzanie wyjścia, zanim ruszy w świat

Weryfikacja tej pary funkcji jest przyjemnie konkretna. Otwórz wynik w Adobe Acrobat i potwierdź trzy punkty: właściwości dokumentu zgłaszają PDF 1.5 albo nowszy, gdy strumienie obiektów są włączone; panel podpisów nadal waliduje każdą wcześniej podpisaną rewizję po aktualizacji przyrostowej; a liczba stron i zakładki przeszły cykl wczytania, modyfikacji i zapisu bez uszczerbku. Dla wyjścia archiwalnego przepchnij plik dodatkowo przez veraPDF, ponieważ skompresowany xref to dokładnie ten rodzaj struktury, któremu ścisły walidator przygląda się uważniej, niż kiedykolwiek przyjrzy się wyrozumiała przeglądarka. Jeśli twoja praca obejmuje też bardzo duże wejścia, metody inspekcji z naszego omówienia Direct File API dla procesów z dużymi plikami PDF naturalnie łączą się z zapisem przyrostowym, a mechanika podpisów stojąca za powyższymi zakresami bajtów jest dogłębnie opisana w artykule o podpisach cyfrowych i PAdES w HotPDF

Obie funkcje są dostarczane jako część komponentu HotPDF dla Delphi oraz C++Builder, obok API generowania, formularzy, szyfrowania i podpisywania omawianych gdzie indziej na tym blogu. Strona produktu linkuje pełne odniesienie do API, jeśli chcesz zestawić powyższe wywołania z własnym potokiem dokumentów