Tehnički članak

Inkrementalna ažuriranja PDF-a u Delphiju: Vodič za AppendToStream

Inkrementalna ažuriranja PDF-a omogućavaju aplikaciji u Delphiju da modifikuje dokument dodavanjem samo izmenjenih objekata, ostavljajući svaki izvorni bajt netaknutim. losLab PDF Library to implementira putem AppendToStream, koji zapisuje samo inkrementalni odeljak definisan standardom ISO 32000-1 §7.5.6, pa uređivanje jedne oznake (bookmark) u datoteci od 2 GB košta samo nekoliko kilobajta izlaza umesto potpunog prepisivanja. Isti je mehanizam razlog zašto se potpisani dokumenti mogu ažurirati bez poništavanja njihovih potpisa

Problem koji ovo rešava je konkretan. Potpuno čuvanje ponovo prepisuje celu datoteku: svaki objekat se ponovo serijalizuje, svaki pomak unakrsne reference (cross-reference offset) se ponovo izračunava, a izlaz nema nikakve veze sa ulazom na nivou bajtova. Za fakturu od 40 KB to je u redu. Ali za skeniranu arhivu od 2 GB u kojoj ste samo ispravili grešku u kucanju u naslovu dokumenta, prepisivanje dva gigabajta radi promene dvadeset bajtova je apsurdno — a ako je datoteka imala digitalni potpis, prepisivanje ga je upravo uništilo

Zašto čuvanje PDF-a narušava njegov digitalni potpis?

Digitalni potpis PDF-a ne potpisuje logički sadržaj dokumenta; on potpisuje raspone bajtova fizičke datoteke. Unos /ByteRange u rečniku potpisa beleži tačno koje raspone datoteke pokriva kriptografski sažetak. Svaka operacija čuvanja koja ponovo serijalizuje te bajtove — čak i ona koja proizvodi semantički identičan dokument — menja sažetak, i svaki validator će prijaviti potpis kao nevažeći. To je tako dizajnirano: potpis svedoči o bajtovima koje je potpisnik video, a ne o nekom apstraktnom modelu dokumenta

Inkrementalna ažuriranja su sigurnosni ventil koji pruža PDF specifikacija. Budući da inkrementalno čuvanje dodaje nove podatke nakon izvornog %%EOF i nikada ne dira potpisane raspone bajtova, postojeći potpis ostaje važeći za bajtove koje pokriva. Validatori zatim zasebno klasifikuju dodate promene — drugi potpis, ispunjavanje obrasca, anotaciju — i odlučuju da li su to dopuštene izmene. Svaki tok rada sa više potpisa zavisi od toga: svaki potpisnik dodaje inkrementalni odeljak na vrh prethodnog. Ako gradite pipeline-ove za potpisivanje, popratni članak o PAdES potpisivanju i verifikaciji u Delphiju detaljno pokriva kako rasponi bajtova potpisa i inkrementalni odeljci međusobno deluju

Kako funkcionišu inkrementalna ažuriranja prema ISO 32000-1 §7.5.6

ISO 32000-1 §7.5.6 definiše model u tri pravila. Prvo, izvorni sadržaj datoteke ostaje potpuno netaknut — niti jedan bajt se ne pomera. Drugo, promenjeni i novostvoreni objekti dodaju se nakon poslednjeg %%EOF, svaki sa istim brojem objekta koji je imao pre (promenjeni objekti jednostavno dobijaju novu definiciju koja zasenjuje staru). Treće, dodaju se novi odeljak unakrsnih referenci i trailer; unos /Prev trailera ukazuje nazad na pomak u bajtovima prethodnog odeljka unakrsnih referenci, tvoreći lanac kojim čitalac prolazi od najnovijeg prema najstarijem kako bi razrešio svaki objekat u njegovu najnoviju definiciju

Dva korisna svojstva proizlaze iz ove strukture. Ažuriranja su jeftina u srazmeri sa onim što se promenilo, a ne sa veličinom dokumenta — trošak dodavanja je veličina modifikovanih objekata plus mali trošak za xref/trailer. Takođe, datoteka postaje sopstvena istorija verzija: svaka prethodna revizija je još uvek fizički prisutna, pa revizor može skratiti datoteku na bilo kom ranijem %%EOF i oporaviti tačno onaj dokument koji je postojao u tom trenutku. Za tokove rada usklađenosti koji moraju dokazati kako je dokument izgledao pre svake izmene, ovaj ugrađeni revizijski trag često je odlučujući argument za inkrementalna čuvanja

Zapisivanje inkrementalnog ažuriranja pomoću AppendToStream

losLab PDF Library izlaže inkrementalni izlaz kroz AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, koji vraća 1 u slučaju uspeha i 0 u slučaju neuspeha. Parametar AppendMode odabire šta se smešta u ciljni tok. Režim 0 zapisuje potpunu datoteku: izvorni bajtovi izvora prvo se kopiraju u tok, a zatim se dodaje inkrementalni odeljak. Režim 1 zapisuje samo sam inkrementalni odeljak — deltu — i potpuno preskače izvorne bajtove. Režim 2 prvo zapisuje prefiks koji je isporučio pozivalac i registrovao se putem SetAppendInputFromString, a zatim dodaje odeljak za ažuriranje na njegov vrh

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;

Režim 1 je zanimljiv za dizajn sistema. Budući da je delta samostalna, možete je isporučiti nezavisno od izvorne datoteke: pohraniti revizije kao zasebne objekte u skladištu objekata, replicirati samo delte na udaljeno mesto ili rekonstruisati bilo koju reviziju spajanjem osnovne datoteke sa njenim lancem inkremenata. Pravilo rekonstrukcije je jednostavno spajanje bajtova — prvo izvorna datoteka, a zatim svaka delta redom — jer je to tačno raspored koji §7.5.6 propisuje za inkrementalno ažuriranu datoteku

Kako biblioteka izračunava pomake xref bez kopiranja izvorne datoteke?

Unosi unakrsnih referenci unutar inkrementalnog odeljka moraju sadržati apsolutne pomake u bajtovima — pozicije merene od početka celovite datoteke, a ne od početka delte. To stvara zagonetku za režim 1: pisac nikada ne emituje izvorne bajtove, no svaki pomak koji beleži mora se pretvarati da su oni tamo. losLab PDF Library to rešava internim adapterom toka, TPDFAppendSectionStream, koji serijalizatoru predstavlja virtuelni koordinatni prostor. Adapter se stvara sa dužinom izvorne datoteke u bajtovima kao njenim osnovnim pomakom, prijavljuje svoju poziciju i veličinu kao tu osnovicu plus sve što je do sada dodato, i prosleđuje samo novozapisane bajtove u ciljni tok pozivaca

Posledica je da režim 1 nikada ne materijalizuje kopiju izvornog dokumenta — ni na disku, ni u memoriji. Jednostavna implementacija (pisanje cele datoteke u privremeni bafer, a zatim odsecanje repa) nosila bi privremenu kopiju celog izvornog PDF-a, što je za ulaze na nivou gigabajta upravo trošak koji inkrementalna ažuriranja nastoje izbeći. Ova tehnika virtuelizacije pomaka je bliski rođak pomeranja referenci bajtova koje se koristi drugde u biblioteci; članak o brzom spajanju PDF-a sa pomeranjem referenci bajtova prikazuje istu ideju primenjenu na kombinovanje dokumenata, a vodič za spajanje i deljenje velikih PDF-ova sa direktnim pristupom datoteci pokriva okolnu I/O arhitekturu za datoteke koje ne staju komotno u RAM

Strujanje potpunih čuvanja pomoću SaveToStream

Inkrementalni izlaz je polovina priče o strujanju; druga polovina je ono što se dešava pri potpunom čuvanju. SaveToStream u losLab PDF Library-ju pokreće serijalizator dokumenta direktno prema ciljnom toku, umesto da prvo renderuje ceo dokument u privremeni AnsiString, a zatim ispisuje taj bafer u jednom pozivu. Stariji pristup je funkcionisao, ali je značio da je svako potpuno čuvanje prolazno držalo drugu celovitu kopiju izlaza u memoriji — bezopasno pri 10 MB, bolno pri 500 MB, i čvrsti zid za višetrajne gigabajtne izlaze na 32-bitnim procesima. Direktna serijalizacija čini da vršna memorija prati strukture objekata dokumenta umesto njegove serijalizovane dužine

Lekcija o načinu deljenja (share-mode): kada je AppendToFile vratio 0

Jednu regresiju na ovom području vredi prepričati jer se obrazac neuspeha može generalizovati. AppendToFile(FileName) dodaje inkrementalno ažuriranje direktno na postojeći PDF na disku — prirodan poziv za tok rada revizijskog traga na licu mesta: učitajte datoteku, napravite promenu, dodajte na istu putanju. U verziji v3.71.2 taj je tačan redosled počeo da vraća 0. Korenski uzrok bio je u loaderu, a ne u piscu: da bi podržao čitanje velikih dokumenata na zahtev, LoadFromFile drži otvorenu ručicu (handle) izvorne datoteke tokom životnog veka objekta dokumenta, a ta je ručica bila otvorena sa fmShareDenyWrite. Kada je AppendToFile potom pokušao ponovo da otvori istu datoteku za pisanje, loaderov sopstveni način deljenja (share mode) ga je odbio, i API je zakazao pre pisanja ijednog bajta

Popravka je ublažila loaderov način deljenja na fmShareDenyNone, što je sigurno upravo zbog onoga što inkrementalno dodavanje jeste: ono dodaje bajtove strogo nakon kraja datoteke i nikada ne prepisuje regiju koju poslužuje loaderova dugovečna ručica. Opšta lekcija za svakoga ko omotava ovu biblioteku — ili gradi slične stream loadere — jeste da su lenji čitaoci koji drže ručice i pisci koji pišu u istu datoteku u napetosti, a način deljenja koji odaberete u trenutku otvaranja je ugovor API-ja, a ne detalj implementacije. Ako AppendToFile ikada vrati 0 u vašem kodu, prvo proverite drži li još nešto u vašem procesu ciljnu datoteku sa restriktivnim načinom deljenja

Stvarni troškovi: kada su inkrementalna ažuriranja pogrešan alat

Inkrementalna ažuriranja trguju veličinom datoteke za efikasnost pisanja, a ta trgovina nije uvek povoljna. Svaka revizija dodaje svoje izmenjene objekte dok zamenjene definicije ostaju u datoteci, pa dokument koji je uređivan stotinama puta akumulira mrtve objekte i dugačak lanac /Prev kroz koji svaki čitalac mora proći. Što je još gore, "obrisani" sadržaj nije nestao: tekst uklonjen u petoj reviziji i dalje je fizički prisutan u bajtovima četvrte revizije, te ga može oporaviti svako ko skrati datoteku. Redakcija, sanitizacija ili bilo kakvo uklanjanje osetljivog sadržaja stoga zahteva potpuno prepisivanje — inkrementalno čuvanje redakcije je curenje podataka sa dodatnim koracima

Potpuno čuvanje je takođe ispravan izbor kada je cilj zbijanje (squeezing out nakupljenih inkremenata i neiskorišćenih objekata), kada se menjaju svojstva na nivou celog dokumenta kao što je šifrovanje — ponovno šifrovanje dodiruje svaki niz i tok, pa više nema ničeg "inkrementalnog" u toj promeni — ili kada se proizvodi čisti isporučivi dokument gde istorija uređivanja ne bi trebalo da putuje sa datotekom. Razumno pravilo: koristite AppendToStream ili AppendToFile dok je dokument aktivan i menja se, posebno kada nosi potpise; koristite potpuno prepisivanje sa SaveToStream na granicama životnog ciklusa, kada dokument napušta vaš sistem ili njegova istorija mora biti izravnana

Inkrementalna ažuriranja, izlaz delte sa virtuelnim pomakom i serijalizacija direktno u tok deo su standardne losLab PDF Library-ja za Delphi, C# i VB.NET; stranica proizvoda navodi celokupnu API površinu za čuvanje i dodavanje zajedno sa funkcijama potpisivanja i velikih datoteka o kojima se raspravljalo gore