Для зменшення розміру файлу PDF у Delphi, losLab PDF Library надає три API, які борються з трьома найбільшими джерелами роздування: SubsetEmbeddedFonts перезаписує кожну вбудовану програму шрифту TrueType до гліфів, які документ фактично відображає, DownsampleImages знижує роздільну здатність растрових зображень, які перевищують цільове значення DPI, а NormalizeLZWStreams замінює застаріле стиснення LZWDecode на FlateDecode. Кожен метод повертає кількість змінених об'єктів, тому нуль повідомляє про те, що прохід був холостим, а не про приховану помилку
Чому мій об'єднаний PDF-файл більший за вихідні файли?
Об'єднаний або програмно згенерований PDF-файл зазвичай має завеликий розмір з однієї з трьох причин: повністю вбудовані шрифти, зображення із роздільною здатністю, набагато вищою за роздільну здатність їх відображення, та потоки, які досі стиснуті за допомогою застарілого фільтра LZW. Стандарт ISO 32000-1 §9.9 дозволяє генератору вбудовувати повну програму шрифту, і більшість генераторів роблять саме це, оскільки це безпечний варіант за замовчуванням. Повний Arial FontFile2 займає сотні кілобайт; вбудуйте його у дюжину вихідних файлів, об'єднайте їх, і ви отримаєте дюжину копій контурів гліфів для символів, які ніхто не вводив. Саме об'єднання не створює цих зайвих даних, воно лише концентрує їх в одному файлі, де загальний обсяг нарешті стає помітним
Зображення є другим порушником. Відскановане зображення шириною 4800 пікселів, розміщене у рамці на чверть сторінки, містить приблизно у 40 разів більше піксельних даних, ніж може використати конвеєр друку з роздільною здатністю 300 DPI. Третій порушник діє тихіше: потоки, відфільтровані за допомогою LZWDecode. Стандарт ISO 32000-1 §7.4.4 визначає як LZWDecode, так і FlateDecode, і зазначає, що Flate зазвичай стискає щонайменше не гірше; на практиці вихідний потік Flate постійно менший для тих самих даних, а LZW виживає здебільшого у файлах, які в якийсь момент своєї історії пройшли через інструменти епохи 1990-х років. У решті цієї статті розглядаються три проходи losLab PDF Library, які вирішують кожну з цих проблем, а потім об'єднуються в один конвеєр
Субсетування шрифтів за допомогою SubsetEmbeddedFonts
SubsetEmbeddedFonts зменшує кожен вбудований шрифт TrueType у завантаженому документі до символів, які фактично використовуються в документі, і не потребує аргументів, оскільки отримує список необхідних символів безпосередньо з потоків вмісту. Внутрішньо цей прохід обходить потік вмісту кожної сторінки за допомогою GetTextRuns, збирає коди символів, на які посилаються під кожним ресурсом шрифту, будує список збереження та передає оригінальну програму шрифту рушію Windows FontSub (CreateFontPackage) для створення підмножини. Перезаписана програма замінює потік FontFile2 на місці, а назва BaseFont отримує префікс LOSABC+ — конвенцію з шести великих літер та знака плюс, яку стандарт ISO 32000-1 §9.6.4 визначає для субсетованих шрифтів. Цей префікс також робить виклик ідемпотентним: запустіть прохід двічі, і вже субсетовані шрифти будуть розпізнані та пропущені, тому його безпечно додавати до пакетного завдання, яке може повторно обробляти файли
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;
Дві деталі реалізації варто знати, оскільки вони пояснюють межі API. По-перше, прохід орієнтований на FontFile2, тому він охоплює вбудовані програми TrueType; шрифти, вбудовані як Type 1 або чистий CFF, залишаються недоторканими, щоб уникнути ризику. По-друге, він покладається на FontSub, що робить SubsetEmbeddedFonts сумісним лише з Windows. Більш тонкий момент реалізації: те, чи підходить шрифт, вирішується шляхом фактичного розв'язання ланцюжка посилань FontDescriptor → FontFile2, а не довірою до евристики прапорця вбудовування, оскільки шрифти в завантаженому документі ніколи не проходили через облік на стороні створення, який встановлює такі прапорці. Якщо розв'язаний потік існує, шрифт є кандидатом; якщо ні, він пропускається без помилки
Чесний компроміс: субсетований шрифт містить лише ті гліфи, які були присутні на момент субсетування. Якщо наступний інструмент або ваш власний код пізніше додасть текст цим самим шрифтом, будь-який символ поза підмножиною не матиме контуру і відображатиметься як відсутній гліф. Виконуйте субсетування як останній крок зміни вмісту, ніколи перед етапом редагування. Така ж обережність застосовується, якщо ви плануєте пізніше витягнути шрифт для повторного використання; стаття про вилучення тексту, зображень та шрифтів за допомогою PDFlibPas охоплює те, що може і чого не може дати вам витягнута програма підмножини
Як DownsampleImages вирішує, які зображення зменшувати?
DownsampleImages(MaxDPI, Quality, Filter) знижує роздільну здатність лише тих зображень, які він може впевнено назвати надлишковими, використовуючи навмисно консервативну оцінку DPI. Зображення PDF XObject зберігає розміри пікселів, але не має надійного фізичного розширення, і будь-який тег DPI з вихідного зображення рідко виживає після циклу завантаження-редагування-збереження. Тому прохід оцінює роздільну здатність за формулою SrcDPI = PixelWidth / 8.5, фактично запитуючи: якби це зображення охоплювало повну ширину сторінки Letter, якою була б його роздільна здатність? Змінюються лише ті зображення, оцінка яких перевищує MaxDPI. Це упередження є навмисним: зображення, розміщене невеликим на сторінці, має справжній DPI вищий за оціночний, тому прохід швидше не спрацює, ніж погіршить якість ресурсу, призначеного для друку, який він не може виміряти
Параметр Quality від 1 до 100 вибирає якість повторного кодування JPEG, тоді як 0 зберігає вихідні дані як Flate без втрат у стилі PNG; Filter вибирає ядро передискретизації, 0 для середнього значення коробки та 1 для білінійної. Для відсканованих офісних документів DownsampleImages(150, 75, 1) є розумною відправною точкою; для всього, що може бути надруковано повторно, підвищте MaxDPI до 300 або взагалі пропустіть цей прохід. Зниження роздільної здатності — це єдиний крок із втратами з цих трьох, тому він має бути закритий налаштуванням, яке ваші користувачі можуть вимкнути
Перетворення застарілих потоків LZW за допомогою NormalizeLZWStreams
NormalizeLZWStreams — це безкоштовна перемога: він без втрат розпаковує кожен потік LZWDecode і стискає його за допомогою FlateDecode на місці, повертаючи кількість конвертованих потоків. Він обробляє як один запис /Filter /LZWDecode, так і LZW, що з'являється всередині масиву ланцюжка фільтрів, де замінюється лише ланка LZW, а решта ланцюжка зберігається. Параметри предиктора (Predictor, Columns, Colors, BitsPerComponent) зчитуються з DecodeParms потоку та передаються декомпресору, тому закодовані предиктором дані зображення правильно повертаються. Оскільки обидва фільтри є точними побітовими кодеками, декодовані байти ідентичні до і після; змінюється лише стиснення контейнера, тому цей прохід безпечно запускати безумовно для кожного файлу
Для документа без потоків LZW виклик просто повертає 0 і нічого не торкається, що явно перевіряється набором регресійних тестів бібліотеки: щойно створений файл, який містить лише Flate, повинен повідомляти про нуль перетворень. Ця гарантія холостого ходу має значення, коли прохід знаходиться в конвеєрі, який обробляє тисячі різнорідних файлів, деякі з яких створені у 2024 році, а деякі — у 1998 році
Повний конвеєр оптимізації розміру в Delphi
Три проходи об'єднуються в єдину функцію завантаження-оптимізації-збереження, і порядок має менше значення, ніж ви могли б очікувати, оскільки вони працюють з різними типами об'єктів: шрифтами, зображеннями XObject та фільтрами потоків. Запуск субсетування першим все одно є хорошим вибором, оскільки це прохід з обмеженням порядку редагування
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;
Перевірте конвеєр так, як перевіряє себе бібліотека: круговий шлях. Регресійні тести версії v3.130 створюють документ, зберігають його, завантажують знову, запускають оптимізацію, знову зберігають і потім стверджують три речі: вихідний файл менший, повернуті лічильники відповідають очікуванням, а повторне завантаження оптимізованого файлу все одно розбирається та рендериться. Відтворення цього циклу створення-оптимізації-повторного завантаження на зразку ваших власних виробничих файлів та порівняння витягнутого тексту до і після — це двогодинна інвестиція, яка виявляє помилки інтеграції задовго до того, як клієнт відкриє зламаний рахунок-фактуру
// 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;
Де місце конвеєра в робочому процесі об'єднання? Після об'єднання, а не під час нього. Спочатку об'єднання, а потім оптимізація єдиного результату означає, що кожен вбудований шрифт субсетується один раз проти об'єднання всіх використаних символів, а не для кожного вихідного файлу. Якщо пропускна здатність об'єднання є вузьким місцем, PDFlibPas пропонує швидкий шлях на рівні байтів, який уникає повного аналізу об'єктів, опис якого міститься у статті про швидке об'єднання PDF зі зсувом байтових посилань; а для вхідних даних, занадто великих для повного утримання в пам'яті, стаття про прямий доступ до об'єднання та розділення великих PDF охоплює потоковий маршрут. Обидва варіанти природно поєднуються з фінальним проходом оптимізації на об'єднаному виводі
Чого три проходи не зроблять
Тріо оптимізації losLab PDF Library навмисно виключає все, що змінює семантику документа. SubsetEmbeddedFonts не об'єднує дублікати шрифтів із різних джерел в одну програму, він зменшує кожен незалежно; дедуплікація — це інше, більш ризиковане перетворення. DownsampleImages пропустить зображення, консервативна оцінка DPI якого залишається нижчою за порогову величину, навіть якщо людина може визначити, що воно завелике для своєї рамки. І жоден із проходів не торкається структури документа, тому файл, роздутий тисячами втрачених об'єктів, потребує збереження з перезаписом, а не цих проходів на рівні потоків. У цих межах поєднання субсетування шрифтів, зниження роздільної здатності зображень та нормалізації LZW-у-Flate усуває три класичні джерела роздування PDF за допомогою одного передбачуваного виклику API для кожного. Ці три функції постачаються як частина losLab PDF Library для Delphi, C# та VB.NET, разом із API об'єднання, вилучення та рендерингу, обговорюваними вище