A PDF inkrementális frissítések lehetővé teszik a Delphi alkalmazások számára, hogy a dokumentumot csak a megváltozott objektumok hozzáfűzésével módosítsák, érintetlenül hagyva az összes eredeti bájtot; a losLab PDF Library ezt az AppendToStream segítségével valósítja meg, amely csak az ISO 32000-1 §7.5.6 által meghatározott inkrementális szakaszt írja le, így egy 2 GB-os fájl egyetlen könyvjelzőjének szerkesztése kilobájtnyi kimenetet igényel a teljes újraírás helyett; ugyanez a mechanizmus az oka annak, hogy az aláírt dokumentumok frissíthetők az aláírásuk érvénytelenítése nélkül
A probléma, amelyet ez megold, kézzelfogható; a teljes mentés újraírja az egész fájlt: minden objektum újra sorosításra kerül, minden kereszthivatkozás eltolás újra kiszámításra kerül, és a kimenet nem mutat bájt szintű kapcsolatot a bemenettel; egy 40 KB-os számla esetén ez rendben van; egy 2 GB-os szkennelt archívumnál, ahol csak a dokumentum címében lévő elírást javította ki, két gigabájt újraírása húsz bájt megváltoztatásához abszurd — és ha a fájl digitális aláírást hordozott, az újraírás éppen most semmisítette meg azt
Miért rontja el a PDF mentése a digitális aláírását?
A PDF digitális aláírás nem a dokumentum logikai tartalmát írja alá; a fizikai fájl bájttartományait írja alá; az aláírás-szótár /ByteRange bejegyzése rögzíti pontosan, hogy a fájl mely részeit fedi le a kriptográfiai kivonat; minden olyan mentési művelet, amely újra sorosítja ezeket a bájtokat — még az is, amely szemantikailag azonos dokumentumot eredményez — megváltoztatja a kivonatot, és minden ellenőrző hibásként fogja jelenteni az aláírást; ez szándékos: az aláírás a bájtokat igazolja, amelyeket az aláíró látott, nem pedig egy absztrakt dokumentummodellt
Az inkrementális frissítések a PDF-specifikáció által biztosított kiskaput jelentik; mivel az inkrementális mentés az új adatokat az eredeti %%EOF után fűzi hozzá, és soha nem érinti az aláírt bájttartományokat, a meglévő aláírás érvényes marad a lefedett bájtokra; az ellenőrzők ezután külön osztályozzák a hozzáfűzött módosításokat — egy második aláírást, űrlapkitöltést, megjegyzést — és eldöntik, hogy ezek megengedett módosítások-e; minden több-aláírásos munkafolyamat ettől függ: minden aláíró egy új inkrementális szakaszt ad hozzá az előző tetejére; ha aláírási folyamatokat épít ki, a Delphi-ben történő PAdES aláírásról és ellenőrzésről szóló társcikk részletesen ismerteti az aláírási bájttartományok és az inkrementális szakaszok közötti interakciót
Hogyan működnek az inkrementális frissítések az ISO 32000-1 §7.5.6 szerint?
Hogyan működnek az inkrementális frissítések az ISO 32000-1 §7.5.6 szerint?
Az ISO 32000-1 §7.5.6 három szabályban határozza meg a modellt; először is, az eredeti fájltartalom teljesen érintetlen marad — egyetlen bájt sem mozdul el; másodszor, a megváltozott és újonnan létrehozott objektumok az utolsó %%EOF után kerülnek hozzáfűzésre, mindegyik a korábbi objektumszámával (a megváltozott objektumok egyszerűen egy újabb definíciót kapnak, amely beárnyékolja a régit); harmadszor, egy új kereszthivatkozási szakasz és lezáró (trailer) kerül hozzáfűzésre; a lezáró /Prev bejegyzése az előző kereszthivatkozási szakasz bájteltolására mutat vissza, láncot alkotva, amelyet az olvasó a legújabbtól a legrégebbi felé haladva jár be, hogy minden objektumot a legfrissebb definíciójára oldjon fel
Ebből a struktúrából két hasznos tulajdonság következik; a frissítések a változások arányában olcsók, nem pedig a dokumentum méretétől függően — a hozzáfűzés költsége a módosított objektumok mérete, plusz egy kis xref/trailer többlet; a fájl pedig a saját verziótörténetévé válik: minden korábbi változat fizikailag jelen van továbbra is, így az ellenőr bármelyik korábbi %%EOF helyen levághatja a fájlt, és pontosan visszaállíthatja az abban a pillanatban létező dokumentumot; a megfelelőségi munkafolyamatok esetében, amelyeknek bizonyítaniuk kell, hogyan nézett ki egy dokumentum az egyes módosítások előtt, ez a beépített ellenőrzési nyomvonal gyakran a döntő érv az inkrementális mentések mellett
Inkrementális frissítés írása az AppendToStream segítségével
A losLab PDF Library az inkrementális kimenetet az AppendToStream(AppendMode: Integer; OutStream: TStream): Integer metóduson keresztül teszi elérhetővé, amely sikeres működés esetén 1, sikertelenség esetén 0 értéket ad vissza; az AppendMode paraméter határozza meg, mi kerüljön a cél-adatfolyamba; a 0-s mód teljes fájlt ír: először az eredeti forrásbájtok másolódnak az adatfolyamba, majd hozzáfűződik az inkrementális szakasz; az 1-es mód csak magát az inkrementális szakaszt írja le — a deltát — és teljesen kihagyja a forrásbájtokat; a 2-es mód először a hívó által biztosított, a SetAppendInputFromString segítségével regisztrált előtagot írja ki, majd erre fűzi rá a frissítési szakaszt
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Kis szerkesztés: az a fajta változtatás, aminek nem
// kellene a teljes fájl újraírását kiváltania
Doc.SetInformation(3, 'Amended 2026-07-04'); // kulcs 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: csak az inkrementális szakaszt írja le
// Eredeti bájtok + Delta = teljes, érvényes PDF
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
Az 1-es mód a legérdekesebb a rendszertervezés szempontjából; mivel a delta önálló, az eredetitől függetlenül is szállítható: a revíziókat különálló blobokként tárolhatja az objektumtárban, csak a deltákat replikálhatja a távoli helyre, vagy rekonstruálhat bármely revíziót az alapfájl és a frissítési lánc egymás után fűzésével; a rekonstrukciós szabály egyszerű bájtok szintű összefűzés — először az eredeti fájl, majd minden delta sorrendben — mert pontosan ezt a felépítést írja elő a §7.5.6 az inkrementálisan frissített fájlokra vonatkozóan
Hogyan számítja ki a könyvtár az xref eltolásokat az eredeti fájl másolása nélkül?
Az inkrementális szakaszon belüli kereszthivatkozás-bejegyzéseknek abszolút bájteltolásokat kell tartalmazniuk — a teljes fájl kezdetétől mérve, nem pedig a delta kezdetétől; ez dilemmát teremt az 1-es módnál: az író soha nem bocsátja ki az eredeti bájtokat, mégis minden rögzített eltolásnak úgy kell tennie, mintha ott lennének; a losLab PDF Library ezt egy belső adatfolyam-adapterrel, a TPDFAppendSectionStream-mel oldja meg, amely virtuális koordináta-teret mutat a sorosítónak; az adapter az eredeti fájl bájthosszával jön létre alapeltolásként, a pozícióját és méretét ezen alap és az addig hozzáfűzött tartalom összegeként jelenti, és csak az újonnan írt bájtokat továbbítja a hívó cél-adatfolyamába
A következmény az, hogy az 1-es mód soha nem hozza létre a forrásdokumentum másolatát — sem a lemezen, sem a memóriában; a naiv megvalósítás (a teljes fájl kiírása egy ideiglenes pufferbe, majd a vége levágása) az egész eredeti PDF átmeneti másolatát hordozná, ami gigabájtos bemeneteknél pontosan az a költség, aminek elkerülésére az inkrementális frissítések léteznek; ez az eltolás-virtuálizációs technika a könyvtárban máshol is használt bájthivatkozás-eltolás közeli rokona; a gyors PDF-összefűzés bájthivatkozás-eltolással című cikk ugyanezt az ötletet mutatja be dokumentumok egyesítésére alkalmazva, a nagy PDF-ek közvetlen fájlelérésű összefűzéséről és felosztásáról szóló útmutató pedig a RAM-ba kényelmesen be nem férő fájlok környező I/O architektúráját tárgyalja
Teljes mentések streamelése a SaveToStream segítségével
Az inkrementális kimenet csak a fele a streamelési történetnek; a másik fele az, ami a teljes mentésnél történik; a losLab PDF Library SaveToStream metódusa a dokumentumsorosítót közvetlenül a cél-adatfolyammal szemben hajtja meg, ahelyett, hogy először a teljes dokumentumot egy köztes AnsiString-gé renderelné, majd azt a puffert egyetlen hívással írná ki; a régebbi megközelítés működött, de azt jelentette, hogy minden teljes mentés átmenetileg a kimenet második teljes másolatát tartotta a memóriában — ami 10 MB-nál ártalmatlan, 500 MB-nál fájdalmas, és kemény akadály a több gigabájtos kimeneteknél a 32 bites folyamatokban; a közvetlen sorosításnak köszönhetően a memóriacsúcs a dokumentum objektumstruktúráit követi a sorosított hosszúság helyett
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... a teljes újraírást indokoló szerkesztések ...
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;
Megosztási mód tanulság: amikor az AppendToFile 0 értéket adott vissza
Érdemes elmesélni egy regressziót ezen a területen, mert a hiba mintája általánosítható; az AppendToFile(FileName) egy inkrementális frissítést fűz közvetlenül a lemezen lévő PDF-hez — ami természetes hívás egy helyben történő ellenőrzési lánc munkafolyamatnál: betöltünk egy fájlt, végrehajtunk egy módosítást, majd hozzáfűzzük ugyanahhoz a helyhez; a v3.71.2-es verzióban ez a pontos sorrend 0 értéket kezdett visszaadni; a kiváltó ok a betöltőben volt, nem az íróben: a nagy dokumentumok igény szerinti olvasásának támogatása érdekében a LoadFromFile a forrásfájl fogantyúját nyitva tartja a dokumentumobjektum élettartama alatt, és ezt a fogantyút a fmShareDenyWrite móddal nyitották meg; amikor az AppendToFile megpróbálta újra megnyitni ugyanazt a fájlt írásra, a betöltő saját megosztási módja megtagadta azt, és az API megbukott még egyetlen bájt kiírása előtt
A javítás a betöltő megosztási módját a fmShareDenyNone-ra enyhítette, ami pontosan azért biztonságos, ami az inkrementális hozzáfűzés lényege: szigorúan a fájl vége után ad hozzá bájtokat, és soha nem írja újra azt a tartományt, amelyet az olvasó hosszú élettartamú fogantyúja kiszolgál; az általános tanulság bárki számára, aki ezt a könyvtárat csomagolja be — vagy hasonló streamelő betöltőket épít —, hogy a lusta, fogantyút tartó olvasók és az ugyanazon fájlba írók feszültségben állnak egymással, és a megnyitáskor választott megosztási mód egy API-szerződés, nem pedig megvalósítási részlet; ha az AppendToFile valaha is 0 értéket ad vissza a kódjában, először ellenőrizze, hogy a folyamatában valami más nem tartja-e még mindig a célfájlt korlátozó megosztási móddal
A valós költségek: amikor az inkrementális frissítés rossz eszköz
A PDF inkrementális frissítések a fájlméretet cserélik el az írási hatékonyságért, a csere pedig nem mindig kedvező; minden revízió hozzáfűzi a megváltozott objektumait, miközben a felülírt definíciók a fájlban maradnak, így a több százszor szerkesztett dokumentum felhalmozza a halott objektumokat és egy hosszú /Prev láncot, amelyet minden olvasónak be kell járnia; ami még rosszabb, a „törölt” tartalom nem tűnik el: az ötödik változatban eltávolított szöveg fizikailag még mindig ott van a negyedik változat bájtai között, amelyet bárki visszaállíthat, aki levágja a fájlt; a kitakarás (redaction), a tisztítás (sanitization) vagy a bizalmas tartalom bármilyen eltávolítása ezért teljes újraírást igényel — a kitakarás inkrementális mentése adatvédelmi incidens plusz lépésekkel
A teljes mentés a megfelelő választás akkor is, ha a cél a tömörítés (a felhalmozott frissítések és a nem használt objektumok kiszorítása), amikor a dokumentum szintű tulajdonságokat változtatja meg, mint például a titkosítás — az újratitkosítás minden karakterláncot és adatfolyamot érint, így a változásban nem marad semmi „inkrementális” —, vagy amikor olyan tiszta végterméket állít elő, amelynél a szerkesztési előzményeknek nem szabad a fájllal együtt utazniuk; ésszerű szabály: használja az AppendToStream-et vagy az AppendToFile-t, amíg a dokumentum életben van és változik, különösen, ha már aláírásokat hordoz; használja a teljes SaveToStream újraírást az életciklus-határokon, amikor a dokumentum elhagyja a rendszerét, vagy ha az előzményeit el kell simítani
Az inkrementális frissítések, a virtuális eltolású delta-kimenet és a közvetlenül adatfolyamba történő sorosítás mind részei a Delphi, C# és VB.NET rendszerekhez készült szabványos losLab PDF Library terméknek; a termékoldal felsorolja a teljes mentési és hozzáfűzési API felületet, a fent említett aláírási és nagyfájlos funkciókkal együtt