Inkrementalne posodobitve PDF omogočajo aplikaciji v Delphiju, da spremeni dokument z dodajanjem le spremenjenih objektov, pri čemer pusti vsak prvotni bajt nedotaknjen. losLab PDF Library to izvaja prek AppendToStream, ki zapiše samo inkrementalni del, ki ga določa ISO 32000-1 §7.5.6, tako da urejanje enega zaznamka v datoteki velikosti 2 GB stane le nekaj kilobajtov izhoda namesto celotnega prepisovanja. Isti mehanizem je razlog, zakaj je mogoče podpisane dokumente posodobiti brez razveljavitve njihovih podpisov
Težava, ki jo to rešuje, je zelo oprijemljiva. Popolno shranjevanje prepiše celotno datoteko: vsaj ena pisava ni vgrajena. Za račun velikosti 40 KB je to v redu. Za skeniran arhiv velikosti 2 GB, kjer ste popravili le tiskarsko napako v naslovu dokumenta, pa je prepisovanje dveh gigabajtov za spremembo dvajsetih bajtov nesmiselno — in če je datoteka vsebovala digitalni podpis, ga je prepis pravkar uničil
Zakaj shranjevanje PDF-ja uniči njegov digitalni podpis?
Digitalni podpis PDF ne podpisuje logične vsebine dokumenta; podpisuje bajtne obsege fizične datoteke. Vnos /ByteRange v slovarju podpisov natančno beleži, katere dele datoteke pokriva kriptografski izvleček. Vsako shranjevanje, ki znova serializira te bajte — celo tisto, ki ustvari semantično enak dokument — spremeni izvleček, validatorji pa bodo podpis prijavili som poškodovan. To je namerno: podpis jamči za bajte, ki jih je podpisnik videl, ne pa za nek abstrakten model dokumenta
Inkrementalne posodobitve so izhod v sili, ki ga ponuja specifikacija PDF. Ker inkrementalno shranjevanje doda nove podatke za prvotnim %%EOF in se nikoli ne dotakne podpisanih bajtnih obsegov, obstoječi podpis ostane veljaven za bajte, ki jih pokriva. Validatorji nato ločeno razvrstijo dodane spremembe — drugi podpis, izpolnjevanje obrazca, opombo — in se odločijo, ali so to dovoljene spremembe. Vsak delovni tok z več podpisi je odvisen od tega: vsaj ena pisava ni vgrajena. Če gradite cevovode za podpisovanje, spremljevalni članek o podpisovanju in validaciji PAdES v Delphiju podrobno opisuje, kako medsebojno delujejo bajtni obsegi podpisov in inkrementalni deli
Kako delujejo inkrementalne posodobitve pod ISO 32000-1 §7.5.6
ISO 32000-1 §7.5.6 opredeljuje model v treh pravilih. Prvič, prvotna vsebina datoteke ostane popolnoma nedotaknjena — niti en bajt se ne premakne. Drugič, spremenjeni in na novo ustvarjeni objekti se dodajo za zadnjim %%EOF, vsak z enako številko objekta, kot jo je imel prej (spremenjeni objekti preprosto dobijo novejšo definicijo, ki zasenči staro). Tretjič, doda se nov del navzkrižnega sklica in napovednik (trailer); vnos trailerja /Prev kaže nazaj na bajtni odmik prejšnjega dela navzkrižnega sklica, kar tvori verigo, po kateri bralnik potuje od najnovejše do najstarejše, da razreši vsak objekt v njegovo najnovejšo definicijo
Iz te strukture izhajata dve uporabni lastnosti. Posodobitve so poceni v sorazmerju s tistim, kar se je spremenilo, in ne z velikostjo dokumenta — strošek dodajanja je velikost spremenjenih objektov ter majhen dodaten strošek za xref/trailer. Datoteka pa postane svoja lastna zgodovina različic: vsaka prejšnja revizija je še vedno fizično prisotna, tako da lahko revizor skrajša datoteko pri katerem koli prejšnjem %%EOF in obnovi natanko tisti dokument, ki je obstajal v tistem trenutku. Za delovne tokove skladnosti, ki morajo dokazati, kako je bil dokument videti pred vsako spremembo, je ta vgrajena revizijska sled pogosto odločilen argument za inkrementalno shranjevanje
Zapisovanje inkrementalne posodobitve z AppendToStream
losLab PDF Library ponuja inkrementalni izhod prek AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, ki vrne 1 ob uspehu in 0 ob neuspehu. Parameter AppendMode izbere, kaj bo zapisano v ciljni tok. Način 0 zapiše celotno datoteko: v tok se najprej kopirajo prvotni bajti vira, nato pa se doda inkrementalni del. Način 1 zapiše samo inkrementalni del sam — delto — in popolnoma preskoči bajte vira. Način 2 najprej zapiše predpono, ki jo zagotovi klicatelj in je registrirana prek SetAppendInputFromString, nato pa na vrh doda del posodobitve
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;
Način 1 je zanimiv za načrtovanje sistema. Ker je delta samozadostna, jo lahko pošljete neodvisno od izvirnika: shranite revizije kot ločene bloke v shrambi objektov, replicirate samo delte na oddaljeno lokacijo ali rekonstruirate katero koli revizijo z veriženjem osnovne datoteke z njeno verigo inkrementov. Pravilo za rekonstrukcijo je preprosto veriženje bajtov — najprej izvirna datoteka, nato vsaka delta po vrstnem redu — saj je to natanko tista postavitev, ki jo §7.5.6 predpisuje za inkrementalno posodobljeno datoteko
Kako knjižnica izračuna odmike xref brez kopiranja izvirne datoteke?
Vnosi navzkrižnih sklicev znotraj inkrementalnega dela morajo vsebovati absolutne bajtne odmike — položaje, izmerjene od začetka celotne datoteke, ne pa od začetka delte. To ustvarja uganko za način 1: zapisovalnik nikoli ne odda prvotnih bajtov, vendar se mora vsak odmik, ki ga zabeleži, pretvarjati, da so tam. losLab PDF Library to rešuje z notranjim adapterjem toka, TPDFAppendSectionStream, ki serializatorju predstavlja virtualni koordinatni prostor. Adapter se ustvari z dolžino bajtov izvirne datoteke kot osnovnim odmikom, sporoča svoj položaj in velikost kot to osnovo plus vse, kar je bilo dodano do zdaj, in preusmeri le na novo zapisane bajte v ciljni tok klicatelja
Posledica tega je, da način 1 nikoli ne ustvari kopije izvornega dokumenta — ne na disku in ne v pomnilniku. Naivna implementacija (zapis celotne datoteke v začasni odložišče in nato rezanje repa) bi začasno vsebovala drugo celotno kopijo celotnega izvirnega PDF-ja, kar je pri gigabajtnih vhodih natanko tisti strošek, ki se mu inkrementalne posodobitve želijo izogniti. Ta tehnika virtualizacije odmika je bližnji sorodnik premika bajtnih sklicev, ki se uporablja drugje v knjižnici; članek o hitrem združevanju PDF-jev s premikom bajtnih sklicev v Delphiju prikazuje enako idejo, uporabljeno za kombiniranje dokumentov, vodnik po združevanju in razdeljevanju velikih PDF-jev z neposrednim dostopom do datotek pa pokriva pripadajočo I/O arhitekturo za datoteke, ki ne ustrezajo velikosti RAM-a
Pretočno shranjevanje celotnih saves z SaveToStream
Inkrementalni izhod je polovica zgodbe o pretakanju; druga polovica je tisto, kar se zgodi pri polnem shranjevanju. SaveToStream v losLab PDF Library poganja serializator dokumenta neposredno proti ciljnemu toku, namesto da bi najprej upodobil celoten dokument v vmesni AnsiString in nato ta odložišče zapisal v enem klicu. Starejši pristop je deloval, vendar je pomenil, da je vsak polni shranjevanje začasno zadržal drugo popolno kopijo izhoda v pomnilniku — neškodljivo pri 10 MB, boleče pri 500 MB in trda stena za večgigabajtne izhode v 32-bitnih procesih. Neposredna serializacija omogoča, da konica pomnilnika sledi strukturam objektov dokumenta namesto njegovi serializirani dolžini
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;
Lekcija o načinu deljenja: ko je AppendToFile vrnil 0
Eno regresijo na tem področju je vredno ponoviti, ker se vzorec neuspeha posplošuje. AppendToFile(FileName) doda inkrementalno posodobitev neposredno v obstoječi PDF na disku — naraven klic za delovni tok in-place revizijske sledi: naložite datoteko, naredite spremembo, dodajte na isto pot. V različici v3.71.2 je to natančno zaporedje začelo vračati 0. Korenski vzrok je bil v nalagalniku, ne v zapisovalniku: za podporo branju velikih dokumentov na zahtevo LoadFromFile ohranja ročico izvorne datoteke odprto ves čas življenjske dobe objekta dokumenta, ta ročica pa je bila odprta z načinom fmShareDenyWrite. Ko je nato AppendToFile poskušal znova odpreti isto datoteko za pisanje, mu je lasten način deljenja nalagalnika to zavrnil in API ni uspel, še preden bi zapisal en sam bajt
Popravek je sprostil način deljenja nalagalnika na fmShareDenyNone, kar je varno prav zaradi tega, kar je inkrementalno dodajanje: bajte dodaja strogo za koncem datoteke in nikoli ne prepisuje območja, ki ga oskrbuje bralčeva dolgoživa ročica. Splošna lekcija za vsakogar, ki ovija to knjižnico — ali gradi podobne pretočne nalagalnike —, je, da so leni bralniki, ki držijo ročico, in zapisovalniki v isto datoteko v napetosti, izbrani način deljenja ob odpiranju pa je pogodba API, ne pa podrobnost implementacije. Če AppendToFile v vaši kodi kdaj vrne 0, najprej preverite, ali nekaj drugega v vašem procesu še vedno drži ciljno datoteko z omejevalnim načinom deljenja
Iskreni stroški: ko so inkrementalne posodobitve napačno orodje
Inkrementalne posodobitve trgujejo z velikostjo datoteke za učinkovitost pisanja, in ta menjava ni vedno ugodna. Vsaka revizija doda svoje spremenjene objekte, medtem ko preživete definicije ostanejo v datoteki, tako da dokument, ki je bil urejen stokrat, kopiči odmrle objekte in dolgo verigo /Prev, po kateri mora stopati vsak bralnik. Še huje, "izbrisana" vsebina ni izginila: besedilo, odstranjeno v peti reviziji, je še vedno fizično prisotno v bajtih četrte revizije in ga lahko obnovi vsakdo, ki skrajša datoteko. Redakcija, sanitizacija ali kakršna koli odstranitev občutljive vsebine zato zahteva popoln prepis — inkrementalno shranjevanje redakcije je le uhajanje podatkov z dodatnimi koraki
Polno shranjevanje je prav tako prava odločitev, ko je cilj stiskanje (odstranjevanje nakopičenih inkrementov in neuporabljenih objektov), ko se spreminjajo lastnosti na ravni celotnega dokumenta, kot je šifriranje — ponovno šifriranje se dotakne vsakega niza in toka, zato pri spremembi ni več nič "inkrementalnega" —, ali pri ustvarjanju čistega končnega izdelka, kjer zgodovina urejanja ne bi smela potovati z datoteko. Razumno pravilo: uporabite AppendToStream ali AppendToFile, ko je dokument živ in se spreminja, še posebej, ko vsebuje podpise; uporabite popoln prepis z SaveToStream ob mejah življenjskega cikla, ko dokument zapusti vaš sistem ali pa je treba njegovo zgodovino sploščiti
Inkrementalne posodobitve, izhod delte z virtualnimi odmiki in serializacija neposredno v tok so del standardne knjižnice losLab PDF Library za Delphi, C# in VB.NET; stran izdelka navaja celotno površino API-ja za shranjevanje in dodajanje, skupaj s funkcijami za podpisovanje in velike datoteke, o katerih smo govorili zgoraj