Инкременталните актуализации на PDF позволяват на Delphi приложение да променя документ, като добавя само променените обекти, оставяйки всеки оригинален байт недокоснат; losLab PDF Library имплементира това чрез AppendToStream, който записва само инкременталния раздел, дефиниран от ISO 32000-1 §7.5.6, така че редактирането на една отметка в 2 GB файл струва килобайти изходна информация вместо пълно пренаписване; Същият механизъм е причината, поради която подписаните документи могат да се актуализират, без това да анулира техните подписи
Болката, която това решава, е конкретна; Пълното запазване пренаписва целия файл: всеки обект се сериализира отново, всяко отместване на кръстосана препратка се изчислява отново и изходният резултат няма връзка на ниво байтове с входния; За фактура от 40 KB това е добре; За сканиран архив от 2 GB, където сте коригирали само печатна грешка в заглавието на документа, пренаписването на два гигабайта за промяна на двадесет байта е абсурдно — и ако файлът е носил цифров подпис, пренаписването току-що го е унищожило
Защо запазването на 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;
// Малка редакция: видът промяна, която не трябва
// да предизвиква пренаписване на целия файл
Doc.SetInformation(3, 'Amended 2026-07-04'); // ключ 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: записва само инкременталния раздел.
// Оригинални байтове + Delta = пълен, валиден 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 файлове с директен достъп до файлове обхваща съпътстващата I/O архитектура за файлове, които не се побират удобно в RAM
Поточно пълно запазване със SaveToStream
Инкременталният изход е половината от историята с потоците; другата половина е това, което се случва при пълно запазване; SaveToStream в losLab PDF Library управлява сериализатора на документи директно срещу целевия поток, вместо първо да изобразява целия документ в междинен AnsiString и след това да записва този буфер с едно извикване; По-старият подход работеше, но означаваше, че всяко пълно запазване временно държеше второ пълно копие на изхода в паметта — безобидно при 10 MB, болезнено при 500 MB и твърда стена за многогигабайтови изходи при 32-битови процеси; Директната сериализация кара пиковата памет да проследява обектовите структури на документа, вместо неговата сериализирана дължина
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... редакции, които оправдават пълно пренаписване ...
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# and VB.NET; страницата на продукта изброята пълния набор от API за запазване и добавяне, заедно с функциите за подписване и големи файлове, обсъдени по-горе