HotPDF THotPDF.CompressDocument to pojedynczy przełącznik, który każe BeginDoc produkować najmniejszy bezstratny PDF, jaki komponent umie zapisać: FlateDecode na najwyższym poziomie, strumień odwołań krzyżowych ze strumieniami obiektów, subsetowanie fontów i kompaktowe podzbiory fontów, które przenumerowują zachowane glify za jawnym /CIDToGIDMap. EndDoc odkłada potem twoje własne ustawienia na miejsce. Trójstronicowy dokument testowy z Arial i SimSun spadł z 10.2 MB do 20 KB przy identycznym renderowaniu
Co CompressDocument tak naprawdę włącza?
CompressDocument nadpisuje sześć ustawień writera, plus limit strumieni obiektów, dla jednego dokumentu i przywraca wszystkie po fakcie. W BeginDoc, zanim wersja PDF się ustali, HotPDF zapamiętuje twoje wartości i ustawia Compression na cmFlateDecode, CompressionLevel na clMaximum, włącza EnableFontSubsetting i CompactFontSubsetting oraz uruchamia UseXRefStream plus UseObjectStreams (ISO 32000-1 §7.5.7 i §7.5.8). Strumienie obiektów wymagają PDF 1.5, więc starszy Version jest podnoszony do 1.5, gdy nie jest zablokowany. PDF/A-1 zabrania obu struktur, więc dokument PDF/A-1 zachowuje klasyczną tabelę odwołań krzyżowych i dostaje tylko robotę Flate i fontów. Obrazy zostają dokładnie takie, jakie je osadziłeś
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'invoice-2026-1042.pdf';
Pdf.CompressDocument := True; // stosowane przez BeginDoc, cofane przez EndDoc
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Przywrócenie dzieje się w zewnętrznym finally EndDoc, więc wyjątek w połowie raportu nie zostawia długo żyjącego komponentu przyklejonego do maksymalnej kompresji na następne zlecenie. Sama właściwość CompressDocument zostaje True; wracają tylko sześć pożyczonych przez nią ustawień. Wersją obsłużono ostrożniej. HotPDF cofa własne podniesienie do 1.5 tylko wtedy, gdy dokument nadal kończy na 1.5, więc gdy inna funkcja pchnęła plik do 1.6 w trakcie biegu (na przykład osadzony font OpenType), wyższa wersja zostaje, dokładnie tak, jak byłoby bez kompresji
Dlaczego podzbiory fontów są wciąż duże bez kompaktowania?
Klasyczny podzbiór TrueType wyrzuca kontury, których nigdy nie rysujesz, ale zostawia każdy identyfikator glifu tam, gdzie stał, i to numerowanie trzyma go ciężkim. Strumień treści pokazuje CID-y równe oryginalnym GID-om, więc podzbiór musi utrzymać offset loca i wpis hmtx dla każdego slotu aż po najwyższy zachowany glif, pusty czy nie. Dla fontu łacińskiego ten narzut to szum. Dla fontu CJK takiego jak SimSun, którego ideogramy siedzą głęboko w bardzo dużej tabeli glifów, dwa chińskie znaki ciągną za sobą tablice zwymiarowane na cały font. O tym, które glify przeżyją, decydują reguły domknięcia podzbioru fontów dla kształtowanych glifów; kompaktowanie dotyczy tego, ile ocaleni kosztują
CompactFontSubsetting przenumerowuje zachowane glify na gęsty zakres zaczynający się od zera i zapisuje strumień /CIDToGIDMap na CIDFont, który ISO 32000-1 §9.7.4.2 definiuje jako tabelę dwubajtowych GID-ów indeksowaną CID-ami. Ta tabela to cały trik. Strumienie treści, tablica szerokości /W i CMap ToUnicode zachowują wszystkie oryginalne CID-y, więc nic już napisanego nie musi się zmienić; do mapy przenosi się tylko poszukiwanie od CID do glifu. W teście, który wymusił tę funkcję, SimSun z dwoma znakami spadł z 24.8 KB danych fontu do 3.1 KB
Kompaktowanie ma twarde granice i degraduje się po cichu, zamiast zawodzić. HotPDF buduje kompaktowe podzbiory wyłącznie dla fontów Type 0 TrueType, zarówno tych ustawionych przez SetFont z włączonym subsetowaniem, jak i fontu zarejestrowanego przez RegisterUnicodeTTF. Prosty font TrueType znajduje swoje glify przez cmap wewnątrz programu fontu, co przenumerowanie by złamało, więc zachowuje rzadki podzbiór. Fonty OpenType-CFF też nie mają ścieżki kompaktowej. Nieudana budowa kompaktowa spada z powrotem na rzadki podzbiór zamiast rzucać. Właściwość jest domyślnie wyłączona, więc istniejące wyjście pozostaje identyczne bajt w bajt, podczas gdy pod PDF/A zarejestrowany font Unicode zawsze dostaje kompaktowy podzbiór
Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True; // użyteczne bez CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;
Jak upakowany writer ściska strukturę pliku?
Gdy fonty i strumienie są już małe, słowniki i dane odwołań krzyżowych stają się największym pozostałym kosztem, więc writer strumieni obiektów za CompressDocument przystrzyga też je. Przewodnik po strumieniach obiektów i aktualizacjach przyrostowych omawia sam format kontenera; ścieżka kompresji dodaje na wierzch cztery udoskonalenia:
- Kompaktowa składnia wg ISO 32000-1 §7.2.2: spacja jest pisana tylko między dwoma tokenami, które inaczej skleiłyby się jako zwykłe znaki, więc
/Type /Pagestaje się/Type/Page - Pola strumienia odwołań krzyżowych biorą dowolną szerokość, na jaką pozwala §7.5.8.2, więc plik poniżej 16 MB przechowuje każdy offset w 3 bajtach zamiast 4
- Do każdego strumienia obiektów wchodzi do 250 obiektów zamiast zwyczajowych 100, chyba że ustawisz własny limit przez
ConfigureAdaptiveObjectStreamPacking - Gdy plik nie jest zaszyfrowany, Catalog i słownik Info też są pakowane do strumieni obiektów; zaszyfrowane wyjście trzyma je na poziomie najwyższym
Kompaktowa składnia przyszła z pułapką, którą warto znać, jeśli rozszerzasz writera. Podpisywanie dopełnia podpis po zapisaniu pliku, przeszukując bajty za literalnymi placeholderami /ByteRange ( i /Contents <, a kompaktowa pisownia zamieniłaby je w /ByteRange( i /Contents<, czego szukanie nigdy nie znajdzie. Słowniki podpisów (Type Sig albo DocTimeStamp, FT Sig) i słownik szyfrowania trzymają więc układ ze spacjami. Pokrewny defekt dotyczył buildów przed v2.766.41: każdy zapis strumieniami obiektów, CompressDocument włącznie, zaczynał się od dwóch linii nagłówka %PDF-, więc uaktualnij, jeśli ścisły walidator flaguje twoje wyjście
Czy da się skompresować PDF, który jest już wczytany?
Tak, przez przeciążenie z opcjami CompressLoadedDocument(Options, Info), które wykonuje te same bezstratne kroki na istniejącym pliku. Z THPDFLoadedDocumentCompressionOptions.Default usuwa nieużywane zasoby stron, scala identyczne fonty i formularze, subsetuje osadzone fonty z kompaktowymi podzbiorami, rekompresuje strumienie bez filtra, Flate, LZW, ASCII i RunLength Flate'em, gdy wynik jest mniejszy, i każe następnemu zapisowi użyć strumieni obiektów. HighRatioFlate jest domyślnie wyłączone, a strumienie obiektów są pomijane dla PDF/A-1 i zapisów przyrostowych. Przeciążenie CompressLoadedDocument bez parametrów to starsze, węższe wywołanie, które tylko kompresuje Flate'em nieskompresowane strumienie
var
Doc: THotPDF;
Options: THPDFLoadedDocumentCompressionOptions;
Info: THPDFLoadedDocumentCompressionInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
Doc.LoadFromFile('quarterly-report.pdf');
Options := THPDFLoadedDocumentCompressionOptions.Default;
Doc.CompressLoadedDocument(Options, Info);
if Info.RefusedBySignaturePolicy then
Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
else
begin
Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
[Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
end;
finally
Doc.Free;
end;
end;
Na ścieżce wczytanego dokumentu mają znaczenie dwie granice. Każdy krok przepisuje bajty objęte podpisem, więc dokument z polami podpisów jest odrzucany w całości: wywołanie zwraca 0, ustawia RefusedBySignaturePolicy i niczego nie zmienia, chyba że ustawisz AllowSignatureInvalidation, po czym Info.SignaturesInvalidated powie ci, co oddałeś. Kompaktowanie jest też tu bardziej konserwatywne niż na ścieżce tworzenia. HotPDF kompaktuje wyłącznie programy fontów używane tylko przez fonty CIDFontType2 z Identity /CIDToGIDMap, gdzie CID równa się GID, i pomija programy z istniejącym strumieniem mapy, /CIDSet albo tabelami glifów koloru takimi jak COLR, sbix, CBDT czy SVG, bo kompaktowa przebudowa wyrzuciłaby warstwy koloru. Zauważ też, że Info.BytesSaved sumuje tylko kroki zasobów, fontów i strumieni; zysk ze strumieni obiektów wychodzi przy zapisie pliku
Jakich rezultatów oczekiwać w praktyce?
Zyski podążają za tym, ile pliku to nieskompresowana struktura i przerośnięte dane fontów, a nie za tym, ile ma stron. Trójstronicowa próbka Arial i SimSun skurczyła się z 10.2 MB do 20 KB przy generowaniu z CompressDocument i z 10.2 MB do 19.8 KB, gdy nieskompresowany oryginał został wczytany i przepuszczony przez CompressLoadedDocument, z identycznym renderowaniem w obu przypadkach. PDF, który jest już kompaktowy, ledwo drgnie: w zestawie regresyjnym takie pliki zapisywały się w granicach -0.07% do +0.06% pierwotnego rozmiaru. Pliki pełne zdjęć zyskują mało, bo żadna ze ścieżek nie dotyka danych obrazów
Jeśli generujesz te same raporty CJK co noc, sparuj kompaktowe podzbiory z trwałym cache podzbiorów fontów na dysku, żeby praca subsetowania nie powtarzała się przy każdym biegu, i diffuj skompresowane wyjścia po zawartości obiektów, a nie po bajtach, bo jedna zmieniona komórka rekompresuje cały strumień obiektów. Pełna referencja właściwości i rekordów jest na stronie produktu HotPDF Delphi PDF component