Aby zmniejszyć rozmiar pliku PDF w Delphi, losLab PDF Library udostępnia trzy interfejsy API, które eliminują trzy największe źródła nadmiarowości: SubsetEmbeddedFonts przepisuje każdy wbudowany program fontu TrueType, ograniczając go tylko do glifów rzeczywiście renderowanych w dokumencie, DownsampleImages zmniejsza rozdzielczość obrazów rastrowych przekraczających docelowe DPI, a NormalizeLZWStreams zastępuje przestarzałą kompresję LZWDecode przez FlateDecode. Każda z tych metod zwraca liczbę zmienionych obiektów, więc zero oznacza, że przebieg nie wykonał żadnych operacji, a nie że zakończył się cichym niepowodzenia
Dlaczego mój scalony plik PDF jest większy niż pliki źródłowe?
Scalony lub wygenerowany programowo plik PDF jest zazwyczaj zbyt duży z jednego z trzech powodów: w pełni osadzonych fontów, obrazów próbkowanych znacznie powyżej ich rozdzielczości wyświetlania oraz strumieni wciąż skompresowanych przestarzałym filtrem LZW. ISO 32000-1 §9.9 pozwala producentowi osadzić kompletny program fontu i większość programów tak właśnie robi, ponieważ jest to bezpieczne ustawienie domyślne. Kompletny plik FontFile2 dla czcionki Arial zajmuje setki kilobajty; osadź go w kilkunastu plikach źródłowych, scal je i otrzymasz kilkanaście kopii obrysów glifów dla znaków, których nikt nie wpisał. Samo scalanie nie tworzy tych śmieci, a jedynie gromadzi je w jednym pliku, w którym całkowity rozmiar staje się w końcu widoczny
Drugim winowajcą są obrazy. Skan o szerokości 4800 pikseli umieszczony w ramce zajmującej ćwierć strony przesyła około 40 razy więcej danych pikseli, niż jest w stanie wykorzystać potok drukowania 300 DPI. Trzeci powód jest cichszy: strumienie filtrowane za pomocą LZWDecode. Norma ISO 32000-1 §7.4.4 określa zarówno LZWDecode, jak i FlateDecode, zauważając przy tym, że Flate zazwyczaj kompresuje co najmniej tak samo dobrze; w praktyce dane wyjściowe Flate są konsekwentnie mniejsze przy tych samych danych, a LZW przetrwało głównie w plikach, które w swojej historii przeszły przez narzędzia z lat 90. Dalsza część tego artykułu omawia trzy przebiegi losLab PDF Library, które rozwiązują każdy z tych problemów, a następnie łączy je w jeden potok
Wydzielanie podzbiorów fontów za pomocą SubsetEmbeddedFonts
SubsetEmbeddedFonts zmniejsza każdy wbudowany font TrueType w załadowanym dokumencie do znaków, które są faktycznie używane, i nie wymaga żadnych argumentów, ponieważ pobiera listę zachowywanych znaków bezpośrednio ze strumieni zawartości. Wewnętrznie ten przebieg analizuje strumień zawartości każdej strony za pomocą GetTextRuns, zbiera kody znaków powiązane z każdym zasobem fontu, buduje listę zachowywanych znaków i przekazuje oryginalny program fontu do silnika Windows FontSub (CreateFontPackage) w celu utworzenia podzbioru. Przepisany program zastępuje w miejscu strumień FontFile2, a nazwa BaseFont zyskuje znacznik LOSABC+ — konwencję sześciu wielkich liter i znaku plus, którą norma ISO 32000-1 §9.6.4 definiuje dla fontów z wydzielonym podzbiorem. Ten prefiks sprawia również, że wywołanie jest idempotentne: uruchom ten przebieg dwukrotnie, a już przetworzone fonty zostaną rozpoznane i pominięte, co czyni bezpiecznym wpięcie tej metody do zadania wsadowego, które może ponownie przetwarzać pliki
var
Lib: TPDFlib;
Fonts: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
begin
Fonts := Lib.SubsetEmbeddedFonts;
// Fonts = number of FontFile2 programs rewritten;
// 0 means nothing embedded, or everything already subsetted
Lib.SaveToFile('merged-report-subset.pdf');
end;
finally
Lib.Free;
end;
end;
Warto znać dwa szczegóły implementacji, ponieważ wyjaśniają one granice tego API. Po pierwsze, przebieg ten dotyczy FontFile2, więc obejmuje wbudowane programy TrueType; fonty osadzone jako Type 1 lub czyste CFF pozostają nienaruszone, aby uniknąć ryzyka uszkodzenia. Po drugie, opiera się on na FontSub, co sprawia, że SubsetEmbeddedFonts działa wyłącznie w systemie Windows. Subtelniejsza kwestia dotycząca implementacji: to, czy dany font kwalifikuje się do przetworzenia, jest rozstrzygane poprzez faktyczne rozwiązanie łańcucha referencji FontDescriptor → FontFile2, a nie przez ufanie heurystyce flagi osadzenia, ponieważ fonty w załadowanym dokumencie nigdy nie przeszły przez księgowość po stronie tworzenia, która ustawia takie flagi. Jeśli rozwiązany strumień istnieje, font staje się kandydatem; jeśli nie, jest pomijany bez zgłaszania błędu
Uczciwy kompromis: font z wydzielonym podzbiorem zawiera tylko te glify, które były obecne w momencie wydzielania. Jeśli inne narzędzie lub Twój własny kod doda później tekst tym samym fontem, każdy znak spoza podzbioru nie będzie miał obrysu i wyrenderuje się jako brakujący glif. Wydzielaj podzbiory jako ostatni krok zmieniający zawartość, nigdy przed etapem edycji. Ta sama ostrożność obowiązuje, jeśli planujesz później wyciągnąć font do ponownego użycia; artykuł o wyciąganiu tekstu, obrazów i fontów za pomocą PDFlibPas opisuje, co wyekstrahowany program podzbioru może, a czego nie może Ci dać
Jak DownsampleImages decyduje, które obrazy zmniejszyć?
DownsampleImages(MaxDPI, Quality, Filter) przepróbkowuje tylko te obrazy, które można z pewnością uznać za nadpróbkowane, stosując celowo ostrożną ocenę DPI. Obiekt PDF image XObject przechowuje wymiary pikseli, ale nie ma wiarygodnych informacji o fizycznej rozdzielczości, a jakikolwiek znacznik DPI z obrazu źródłowego rzadko przetrwa cykl wczytaj-edytuj-zapisz. Przebieg ten szacuje zatem SrcDPI = PixelWidth / 8.5, pytając w efekcie: gdyby ten obraz obejmował całą szerokość strony Letter, jaka byłaby jego rozdzielczość? Modyfikowane są tylko te obrazy, których szacunek przekracza MaxDPI. To przesunięcie jest zamierzone: obraz umieszczony jako mały na stronie ma rzeczywiste DPI wyższe niż szacowane, więc przebieg raczej nie uruchomi się na niektórych obrazach, zamiast degradować zasoby o jakości druku, których nie może zmierzyć
Wartość Quality od 1 do 100 określa jakość ponownego kodowania JPEG, podczas gdy 0 zachowuje wyjście jako bezstratny Flate w stylu PNG; Filter wybiera jądro przepróbkowania: 0 dla box average (średnia skrzynkowa) i 1 dla bilinear (dwuliniowy). W przypadku zeskanowanych dokumentów biurowych sensownym punktem wyjścia jest DownsampleImages(150, 75, 1); w przypadku wszystkiego, co może być ponownie drukowane, należy podnieść MaxDPI do 300 lub całkowicie pominąć ten przebieg. Zmniejszanie rozdzielczości jest jedynym stratnym krokiem z tych trzech, więc powinno być ukryte za ustawieniem, które użytkownicy mogą wyłączyć
Konwertowanie przestarzałych strumieni LZW za pomocą NormalizeLZWStreams
NormalizeLZWStreams to darmowa wygrana: bezstratnie dekompresuje każdy strumień LZWDecode i kompresuje go ponownie za pomocą FlateDecode na miejscu, zwracając liczbę przekonwertowanych strumieni. Obsługuje zarówno pojedynczy wpis /Filter /LZWDecode, jak i LZW pojawiające się w tablicy łańcucha filtrów, gdzie zastępowane jest tylko ogniwo LZW, a reszta łańcucha pozostaje zachowana. Parametry predyktora (Predictor, Columns, Colors, BitsPerComponent) are odczytywane z DecodeParms strumienia i przekazywane do dekompresora, dzięki czemu dane obrazu zakodowane z predyktorem przechodzą proces bezproblemowo. Ponieważ oba filtry są kodekami bitowo dokładnymi, zdekodowane bajty są identyczne przed i po; zmienia się tylko kompresja kontenera, co czyni ten przebieg bezpiecznym do uruchomienia bezwarunkowo na każdym pliku
W dokumencie bez strumieni LZW wywołanie po prostu zwraca 0 i niczego nie dotyka, co jest wyraźnie sprawdzane przez zestaw testów regresyjnych biblioteki: świeżo utworzony plik tylko z Flate musi zgłosić zero konwersji. Ta gwarancja braku działania ma znaczenie, gdy przebieg znajduje się w potoku przetwarzającym tysiące zróżnicowanych plików, niektórych z roku 2024, a innych z roku 1998
Kompletny potok optymalizacji rozmiaru w Delphi
Trzy przebiegi łączą się w jedną funkcję wczytaj-optymalizuj-zapisz, a kolejność ma mniejsze znaczenie, niż można by się spodziewać, ponieważ działają one na rozłącznych typach obiektów: fontach, obiektach graficznych XObject i filtrach strumieni. Uruchomienie najpierw wydzielania podzbiorów jest nadal najbardziej uporządkowanym wyborem, ponieważ jest to przebieg z ograniczeniem kolejności edycji
function OptimizePDF(const Src, Dst: string): Boolean;
var
Lib: TPDFlib;
Fonts, Images, Streams: Integer;
begin
Result := False;
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Src, '') <> 1 then
Exit;
Fonts := Lib.SubsetEmbeddedFonts; // TrueType FontFile2 -> subset
Images := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
Streams := Lib.NormalizeLZWStreams; // LZWDecode -> FlateDecode
Result := Lib.SaveToFile(Dst) = 1;
// Log Fonts/Images/Streams: three zeros mean the file was already lean
finally
Lib.Free;
end;
end;
Zweryfikuj potok w taki sam sposób, w jaki biblioteka weryfikuje samą siebie: wykonując pełny proces kołowy (round-trip). Testy regresyjne w wersji 3.130 tworzą dokument, zapisują go, ładują ponownie, uruchamiają optymalizację, zapisują ponownie, a następnie potwierdzają trzy rzeczy: plik wyjściowy jest mniejszy, zwrócone liczby odpowiadają oczekiwaniom, a ponowne załadowanie zoptymalizowanego pliku nadal pozwala na jego przetworzenie i wyrenderowanie. Odtworzenie tej pętli utwórz-optymalizuj-przeładuj na próbce własnych plików produkcyjnych i porównanie wyodrębnionego tekstu przed i po to jednogodzinna inwestycja, która pozwala wychwycić błędy integracji na długo przed tym, jak klient otworzy uszkodzoną fakturę
// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
Assert(Lib.GetPageCount > 0);
finally
Lib.Free;
end;
Gdzie potok pasuje do przepływu pracy scalania? Po scaleniu, a nie w jego trakcie. Najpierw scalenie, a następnie optymalizacja pojedynczego wyniku oznacza, że każdy wbudowany font jest dzielony na podzbiory tylko raz dla sumy wszystkich użytych znaków, a nie dla każdego pliku źródłowego z osobna. Jeśli przepustowość scalania jest wąskim gardłem, PDFlibPas oferuje szybką ścieżkę na poziomie bajtów, która unika pełnego parsowania obiektów, opisaną w artykule o szybkim scalaniu PDF z przesunięciem referencji bajtowych; w przypadku danych wejściowych zbyt dużych, by zmieścić je w pamięci, artykuł o bezpośrednim dostępie do łączenia i dzielenia dużych plików PDF opisuje ścieżkę strumieniową. Obie te metody naturalnie łączą się z końcowym przebiegiem optymalizacji scalonego wyniku
Czego nie zrobią te trzy przebiegi
Trio optymalizacyjne losLab PDF Library celowo wyklucza wszystko, co zmienia semantykę dokumentu. SubsetEmbeddedFonts nie scala zduplikowanych fontów w scalonych źródłach w jeden program, ale zmniejsza każdy niezależnie; deduplikacja to inna, bardziej ryzykowna transformacja. DownsampleImages pominie obraz, którego ostrożnie oszacowane DPI pozostaje poniżej progu, nawet jeśli człowiek mógłby stwierdzić, że jest on zbyt duży dla swojej ramki. Żaden z przebiegów nie dotyka również struktury dokumentu, więc plik powiększony o tysiące osieroconych obiektów wymaga zapisu w stylu przepisywania, a nie tych przebiegów na poziomie strumienia. W tych granicach połączenie wydzielania podzbiorów fontów, zmniejszania rozdzielczości obrazów oraz normalizacji LZW do Flate usuwa trzy klasyczne źródła nadmiarowości PDF za pomocą jednego przewidywalnego wywołania API dla każdego z nich. Te trzy funkcje są dostarczane jako część standardowej biblioteki losLab PDF Library dla Delphi, C# i VB.NET, wraz z interfejsami API scalania, ekstrakcji i renderowania omówionymi powyżej