Tehnični članak

Inkrementalne posodobitve PDF v Delphiju: Vodnik za AppendToStream

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