Інкрементні оновлення PDF дозволяють додатку Delphi змінювати документ, додаючи лише змінені об'єкти, залишаючи кожен оригінальний байт недоторканим. losLab PDF Library реалізує це за допомогою AppendToStream, який записує лише інкрементну секцію, визначену стандартом ISO 32000-1 §7.5.6, тому редагування однієї закладки у файлі розміром 2 ГБ коштує кілобайти вихідних даних замість повного перезапису. Цей самий механізм дозволяє оновлювати підписані документи без анулювання їхніх підписів
Біль, яку це вирішує, є цілком конкретною. Повне збереження перезаписує весь файл: кожен об'єкт серіалізується заново, кожен зсув перехресних посилань обчислюється повторно, і вихідний файл не має зв'язку на рівні байтів із вхідним. Для рахунка-фактури розміром 40 КБ це цілком нормально. Але для відсканованого архіву розміром 2 ГБ, де ви лише виправили друкарську помилку в назві документа, перезапис двох гігабайтів для зміни двадцяти байтів є абсурдним — і якщо файл містив цифровий підпис, цей перезапис щойно його знищив
Чому збереження PDF порушує його цифровий підпис?
Цифровий підпис PDF не підписує логічний вміст документа; він підписує діапазони байтів фізичного файлу. Запис /ByteRange у словнику підпису фіксує, які саме проміжки файлу охоплює криптографічний дайджест. Будь-яка операція збереження, яка заново серіалізує ці байти — навіть та, що створює семантично ідентичний документ — змінює дайджест, і кожен валідатор повідомить, що підпис пошкоджено. Це зроблено навмисно: підпис засвідчує ті байти, які бачив підписант, а не якусь абстрактну модель документа
Інкрементні оновлення — це рятівний люк, який надає специфікація PDF. Оскільки інкрементне збереження додає нові дані після оригінального %%EOF і ніколи не торкається підписаних діапазонів байтів, існуючий підпис продовжує перевірятися на відповідність байтам, які він охоплює. Потім валідатори класифікують додані зміни окремо — другий підпис, заповнення форми, анотація — і вирішують, чи є вони дозволеними змінами. Будь-який робочий процес із кількома підписами залежить від цього: кожен підписант додає інкрементну секцію поверх попередньої. Якщо ви будуєте конвеєри підписання, супутня стаття про підписання та валідацію PAdES у Delphi детально описує взаємодію діапазонів байтів підпису та інкрементних секцій
Як працюють інкрементні оновлення відповідно до ISO 32000-1 §7.5.6
ISO 32000-1 §7.5.6 визначає модель у вигляді трьох правил. По-перше, оригінальний вміст файлу залишається повністю недоторканим — жоден байт не переміщується. По-друге, змінені та щойно створені об'єкти додаються після останнього %%EOF, кожен з тим самим номером об'єкта, який він мав раніше (змінені об'єкти просто отримують новіше визначення, яке затінює старе). По-третє, додається нова секція перехресних посилань і трейлер; запис /Prev трейлера вказує на зсув байтів попередньої секції перехресних посилань, утворюючи ланцюжок, який програма читання проходить від найновішого до найстарішого для вирішення кожного об'єкта до його найсвіжішого визначення
З цієї структури випливають дві корисні властивості. Оновлення є дешевими пропорційно до того, що змінилося, а не до розміру документа — вартість додавання є розміром змінених об'єктів плюс невеликі витрати на xref/трейлер. І файл стає власною історією версій: кожна попередня ревізія все ще фізично присутня, тому аудитор може обрізати файл на будь-якому ранньому %%EOF та відновити саме той документ, який існував на той момент. Для робочих процесів відповідності, які повинні довести, як виглядав документ перед кожною зміною, цей вбудований аудиторський слід часто є вирішальним аргументом для інкрементних збережень
Запис інкрементного оновлення за допомогою AppendToStream
losLab PDF Library відкриває інкрементний вивід через AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, який повертає 1 у разі успіху та 0 у разі помилки. Параметр AppendMode вибирає, що саме потрапить у цільовий потік. Режим 0 записує повний файл: оригінальні байти джерела копіюються в потік першими, потім додається інкрементна секція. Режим 1 записує лише саму інкрементну секцію — дельту — і повністю пропускає байти джерела. Режим 2 спочатку записує наданий викликачем префікс, зареєстрований за допомогою SetAppendInputFromString, а потім додає секцію оновлення поверх нього
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Small edit: the kind of change that should not
// trigger a rewrite of the whole file
Doc.SetInformation(3, 'Amended 2026-07-04'); // key 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: write only the incremental section.
// Original bytes + Delta = a complete, valid PDF.
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
Режим 1 є найцікавішим для проектування систем. Оскільки дельта є самодостатньою, ви можете передавати її незалежно від оригіналу: зберігати ревізії як окремі двійкові об'єкти в сховищі об'єктів, реплікувати лише дельти на віддалений майданчик або відновлювати будь-яку ревізію шляхом об'єднання базового файлу з його ланцюжком інкрементів. Правило відновлення полягає в простому об'єднанні байтів — спочатку оригінальний файл, потім кожна дельта по порядку — тому що це саме те макетування, яке §7.5.6 приписує для інкрементно оновленого файлу
Як бібліотека обчислює зсуви xref без копіювання оригінального файлу?
Записи перехресних посилань всередині інкрементної секції повинні містити абсолютні зсуви байтів — позиції, виміряні від початку повного файлу, а не від початку дельти. Це створює загадку для режиму 1: записувач ніколи не виводить оригінальні байти, проте кожен записаний ним зсув має вдавати, що вони там є. losLab PDF Library вирішує це за допомогою внутрішнього адаптера потоку, TPDFAppendSectionStream, який представляє віртуальний простір координат для серіалізатора. Адаптер створюється з байтовою довжиною оригінального файлу як базовим зсувом, повідомляє про свою позицію та розмір як цю базу плюс усе, що було додано до цього часу, і передає лише щойно записані байти в цільовий потік викликача
Наслідком є те, що режим 1 ніколи не матеріалізує копію вихідного документа — ні на диску, ні в пам'яті. Наївна реалізація (записати повний файл у тимчасовий буфер, а потім відрізати хвіст) призвела б до створення тимчасової копії всього оригінального PDF, що для гігабайтних входів є саме тими витратами, для уникнення яких існують інкрементні оновлення. Ця техніка віртуалізації зсувів є близьким родичем зсуву байтових посилань, який використовується в інших місцях бібліотеки; стаття про швидке об'єднання PDF зі зсувом байтових посилань показує ту саму ідею, застосовану до об'єднання документів, а посібник із об'єднання та розділення великих PDF з прямим доступом до файлів охоплює навколишню архітектуру введення-виведення для файлів, які не вміщуються комфортно в оперативній пам'яті
Потокове повне збереження за допомогою SaveToStream
Інкрементний вивід — це половина потокової історії; інша половина — це те, що відбувається під час повного збереження. SaveToStream у losLab PDF Library запускає серіалізатор документа безпосередньо проти цільового потоку, замість того щоб спочатку рендерити весь документ у проміжний AnsiString, а потім записувати цей буфер за один виклик. Старий підхід працював, але це означало, що кожне повне збереження тимчасово утримувало другу повну копію виводу в пам'яті — нешкідливо при 10 МБ, болісно при 500 МБ і було жорсткою стіною для мультигігабайтних виводів у 32-бітних процесах. Пряма серіалізація змушує пікову пам'ять відстежувати структури об'єктів документа замість його серіалізованої довжини
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... edits that justify a full rewrite ...
Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
try
if Doc.SaveToStream(Output) = 0 then
Writeln('Save failed, error ', Doc.LastErrorCode);
finally
Output.Free;
end;
finally
Doc.Free;
end;
end;
Урок щодо режиму спільного доступу: коли AppendToFile повернув 0
Про одну регресію в цій області варто розповісти, оскільки шаблон помилки є загальним. AppendToFile(FileName) додає інкрементне оновлення безпосередньо до існуючого PDF на диску — природний виклик для робочого процесу з аудиторським слідом на місці: завантажити файл, зробити зміну, додати до того самого шляху. У версії v3.71.2 саме ця послідовність почала повертати 0. Першопричина крилася в завантажувачі, а не в записувачі: для підтримки читання великих документів за запитом, LoadFromFile тримає дескриптор вихідного файлу відкритим протягом усього життя об'єкта документа, і цей дескриптор був відкритий з fmShareDenyWrite. Коли потім AppendToFile спробував відкрити той самий файл для запису, власний режим спільного доступу завантажувача відмовив у цьому, і API дав збій ще до запису байта
Виправлення послабило режим спільного доступу завантажувача до fmShareDenyNone, що є безпечним саме завдяки тому, чим є інкрементне додавання: воно додає байти строго після кінця файлу і ніколи не перезаписує область, яку обслуговує довготривалий дескриптор читача. Загальний урок для тих, хто обгортає цю бібліотеку — або будує подібні потокові завантажувачі — полягає в тому, що ледачі читачі, які утримують дескриптор, та записувачі того самого файлу перебувають у напрузі, і вибраний вами режим спільного доступу під час відкриття є контрактом API, а не деталлю реалізації. Якщо AppendToFile коли-небудь повертає 0 у вашому коді, спочатку перевірте, чи не утримує інший процес у вашій системі цільовий файл із обмежувальним режимом спільного доступу
Справжня ціна: коли інкрементні оновлення є неправильним інструментом
Інкрементні оновлення розмінюють розмір файлу на ефективність запису, і цей обмін не завжди є вигідним. Кожна ревізія додає свої змінені об'єкти, тоді як застарілі визначення залишаються у файлі, тому документ, відредагований сотні разів, накопичує мертві об'єкти та довгий ланцюжок /Prev, який повинна проходити кожна програма читання. Гірше того, «видалений» вміст не зникає: текст, вилучений у п'ятій ревізії, все ще фізично присутній у байтах четвертої ревізії, і його може відновити будь-хто, хто обріже файл. Тому редагування, санітарна обробка або будь-яке видалення конфіденційного вмісту вимагає повного перезапису — інкрементне збереження відредагованого вмісту є витоком даних з додатковими кроками
Повне збереження також є правильним вибором, коли метою є стиснення (видалення накопичених інкрементів та невикористовуваних об'єктів), при зміні загальнодокументних властивостей, таких як шифрування — повторне шифрування торкається кожного рядка та потоку, тому в зміні не залишається нічого «інкрементного» — або при створенні чистого кінцевого результату, коли історія редагування не повинна передаватися разом із файлом. Розумне правило: використовуйте AppendToStream або AppendToFile, поки документ живе і змінюється, особливо коли він містить підписи; використовуйте повний перезапис SaveToStream на межах життєвого циклу, коли документ залишає вашу систему або його історію необхідно згладити
Інкрементні оновлення, дельта-вивід з віртуальним зсувом та серіалізація безпосередньо в потік є частиною стандартної losLab PDF Library для Delphi, C# та VB.NET; сторінка продукту перелічує весь інтерфейс API для збереження та додавання разом із функціями підписання та роботи з великими файлами, обговорюваними вище