PDF 1.5 запровадив дві структури зберігання, які раніший формат файлу не мав способу виразити: потік об'єктів (object stream) і потік перехресних посилань (cross-reference stream). Потік об'єктів — це один контейнер, стиснутий Flate, позначений /Type /ObjStm, який утримує багато дрібних непрямих об'єктів, упакованих один за одним, замість того, щоб розсіювати їх по тілу файлу. Потік перехресних посилань — це таблиця пошуку файлу, переписана як стиснутий бінарний формат зі змінною шириною полів, замість фіксованої за шириною ASCII-таблиці, якою завершувався кожен PDF аж до версії 1.4. Вони йдуть у парі. Щойно об'єкти згорнуто в потік, стара текстова таблиця більше не може їх адресувати, тож бінарний xref мусить іти разом із ним
Порівняйте це з класичним компонуванням — і побачити, яку вартість це усуває, легко. У файлі PDF 1.4 кожен непрямий об'єкт лежить нестисненим за власним заголовком obj, а таблиця в кінці витрачає рівно 20 байтів ASCII на кожен запис, і стискання там заборонене. Документ із 200 000 об'єктів несе приблизно 4 МБ даних перехресних посилань ще до того, як намальовано хоча б один гліф, а поверх них нашаровано всі нестиснені тіла словників. PDF 1.5 б'є по обох числах одразу: словники згортаються в контейнери Flate, а 4-мегабайтна таблиця стискається до кількох сотень кілобайтів бінарних даних. ISO 32000-1 визначає обидві структури в §7.5.7 і §7.5.8
Де насправді осідає економія
Потоки об'єктів торкаються лише об'єктів, що не є потоками, тож вони стискають структуру, а не пікселі. Вміст сторінки вже був стиснутий Flate ще до версії 1.5, а дані зображень несуть власні кодеки, тому насичена зображеннями брошура майже не змінюється в розмірі. Файли, що суттєво стискаються, — це файли, багаті на структуру: AcroForm із тисячами словників полів, глибокі дерева закладок, структурні елементи тегованого PDF. Ці об'єкти малі, численні й майже ідентичні один одному, і саме на цій повторюваності виграє Flate, коли вони опиняються в одному буфері замість того, щоб розсіюватися по тілу файлу з заголовками, вклиненими між ними
Легко недооцінити, наскільки старий файл складається з накладних витрат. Архів форм, що ввібрав роки правок, може витрачати значно більше половини своїх байтів на заголовки словників, доповнення xref і ревізії, на які жоден читач ніколи не гляне. Дві функції, розглянуті тут, повертають перші дві з цих категорій. Третя, накопичені ревізії, піддається лише ущільненню — коли файлу більше не потрібно пам'ятати власну історію
У HotPDF обидві функції вмикаються через пару властивостей, і те, як вони залежать одна від одної, важливіше за порядок, у якому ви їх записуєте:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog-2026.pdf';
Pdf.UseXRefStream := True; // бінарний xref, передумова для ObjStm
Pdf.UseObjectStreams := True; // упакувати об'єкти в /Type /ObjStm
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
Pdf.EndDoc; // видає контейнери XRefStm + ObjStm
finally
Pdf.Free;
end;
end;
UseObjectStreams потребує, щоб UseXRefStream було встановлено в True. До стисненого об'єкта дістаються через запис xref типу 2, який фіксує номер потоку об'єктів плюс індекс, а класичний 20-байтовий текстовий рядок не має місця, щоб зберегти цю пару. Тож UseObjectStreams сам собою нічого видимого не дає; робочою конфігурацією є обидва прапорці, встановлені до BeginDoc. Встановіть їх після BeginDoc — і HotPDF уже зафіксувався на старішому компонуванні
Чому обидва за замовчуванням вимкнені
HotPDF залишає обидві властивості False з коробки, і причина цього виявляється в інтеграціях зі старим кодом нижче за конвеєром. Читач, що розуміє лише PDF 1.4, не оголошує, що не може впоратися зі стисненими об'єктами. Він натрапляє на потік xref, не знаходить жодного з очікуваних ключових слів трейлера й повідомляє про пошкоджену таблицю перехресних посилань або просто відмовляється відкривати файл. Якщо ваш вивід тече в застарілий факс-шлюз, апаратний принтер із вбудованим інтерпретатором або парсер, який хтось написав за специфікацією 1.4 десять років тому, тримайте обидва прапорці вимкненими для цього каналу й миріться з більшим файлом. Для архівного зберігання й вебдоставки, де кожен масовий переглядач читає PDF 1.5 уже двадцять років, увімкнення цих прапорців — це стискання, яке ви отримуєте майже задарма
Є ефект другого порядку, про який варто розповісти вашій команді підтримки. Щойно словники упаковано в потоки об'єктів, побайтове порівняння двох згенерованих файлів втрачає будь-який сенс, бо зміна одного-єдиного поля може перестиснути весь контейнер Flate й перемішати все, що йде після нього. Порівнюйте такі файли за вмістом об'єктів, а не бінарним порівнянням
Інкрементні оновлення та зсуви байтів, які вони захищають
Цифровий підпис охоплює явний /ByteRange: два відрізки фізичного файлу, задані як абсолютні зсуви байтів, за якими було обчислено дайджест CMS. Перепишіть файл, навіть у щось, що виглядає ідентичним на екрані, — і всі ці зсуви зрушаться. Дайджест перестане збігатися, і підпис читатиметься як пошкоджений. Саме цю проблему й розв'язує ISO 32000-1 §7.5.6 через інкрементні оновлення. Нові й змінені об'єкти дописуються після наявного %%EOF, а потім записується новий розділ перехресних посилань, чий запис /Prev вказує назад на попередній. Початкові байти ніколи не турбуються, тож підписана ревізія лишається перевірюваною, і Acrobat може показати кожну підписану ревізію окремо в панелі підписів
HotPDF відкриває це через власну точку входу:
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'); // дописує лише дельту
Дві речі часто збивають з пантелику. BeginIncrementalUpdate має отримувати саме ім'я оригінального файлу, бо дописаний розділ xref фіксує зсуви, які мають сенс лише щодо цих точних оригінальних байтів; спрямуйте його на перейменовану чи пересзбережену копію — і зсуви описуватимуть файл, якого вже не існує. І збереження за конструкцією лише дописує, тож вивід завжди більший за вхід. Це зростання — не марнотратство, яке слід усувати. Це та сама властивість, що залишає попередні підписані ревізії непошкодженими
Зміна завантаженого файлу відбувається через LoadFromFile
Розробники, які вперше познайомилися з HotPDF через його API генерації, зазвичай натикаються на конкретну стіну. BeginDoc відкриває цілком новий документ, а це неправильний інструмент, коли ви маєте намір змінити той, що вже існує. Редагування наявного файлу натомість проходить через виклики для завантаженого документа:
PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5); // сторінки 1-3 після сторінки 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');
Змішайте ці два підходи — і симптомом стане вихідний файл, що містить лише ваш новий вміст і нічого з оригіналу, бо BeginDoc безтурботно побудував новий документ поряд із тим, що ви вважали ніби редагуєте. Сприймайте LoadFromFile разом із SaveLoadedDocument як одну лексику, а BeginDoc разом із EndDoc — як іншу. Процедура, що звертається до обох щодо того самого файлу, майже завжди помилкова
Коли ущільнювати дописаний файл
Збереження в режимі лише дописування несе поступову вартість. Нічне завдання, що дописує один рядок статусу до того самого PDF, за рік дає 365 ревізій, і кожна ревізія тягне за собою новий розділ xref. Коли ця історія вже віджила своє й жоден підпис у файлі не потребує збереження, можна сплощити все, пересеріалізувавши через шлях завантаженого документа:
Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');
Це повторне збереження — повне переписування. Воно навмисно відкидає попередні ревізії і ламає будь-який підпис, що ще лишався у файлі, тож ставте цю дію за тим самим бар'єром політики, що й будь-який інший руйнівний крок. Одне правило, перевірене на практиці: ущільнюйте, коли кількість ревізій перевищує поріг, або коли дописані накладні витрати переростають певну частку базового файлу, і ніколи не ущільнюйте документ, у якого панель підписів містить хоч щось
Перевірка виводу перед відправленням
Перевірка цієї пари функцій приємно конкретна. Відкрийте результат в Adobe Acrobat і підтвердьте три пункти: властивості документа повідомляють PDF 1.5 чи новішу версію, коли увімкнено потоки об'єктів; панель підписів усе ще підтверджує кожну раніше підписану ревізію після інкрементного оновлення; а кількість сторінок і закладки пережили цикл завантаження, зміни та збереження неушкодженими. Для архівного виводу проженіть файл також через veraPDF, оскільки стиснутий xref — це саме той тип структури, яку суворий валідатор перевіряє пильніше, ніж будь-коли поблажливий переглядач. Якщо ваша робота також стосується дуже великих вхідних даних, методи перевірки з нашого огляду Direct File API для роботи з великими PDF природно поєднуються з інкрементним збереженням, а механіка підписів, що стоїть за діапазонами байтів вище, детально розглянута в статті про цифрові підписи та PAdES у HotPDF
Обидві функції постачаються як частина HotPDF Delphi Component для Delphi та C++Builder, поряд з API генерації, форм, шифрування та підписання, розглянутими в інших статтях цього блогу. Сторінка продукту містить посилання на повний довідник API, якщо ви хочете звірити наведені вище виклики зі своїм власним конвеєром роботи з документами