A PDF inkrementális frissítései lehetővé teszik, hogy egy Delphi alkalmazás úgy módosítson egy dokumentumot, hogy csak a megváltozott objektumokat fűzi hozzá, és minden eredeti bájtot érintetlenül hagy. A losLab PDF Library ezt az AppendToStream hívással valósítja meg, amely csak az ISO 32000-1 §7.5.6 által definiált inkrementális szakaszt írja ki, tehát egy 2 GB méretű fájl egyetlen könyvjelzőnyi szerkesztése teljes újraírás helyett kilobájtnyi kimenetbe kerül. Ugyanez a mechanizmus az oka annak, hogy az aláírt dokumentumok az aláírásaik érvénytelenítése nélkül frissíthetők
A fájdalom, amelyet ez megold, kézzelfogható. Egy teljes mentés újraírja az egész fájlt: minden objektum újraszerializálódik, minden kereszthivatkozási eltolás újraszámolódik, és a kimenetnek semmilyen bájtszintű kapcsolata nincs a bemenettel. Egy 40 KB méretű számlánál ez rendben van. Egy 2 GB méretű beszkennelt archívumnál, ahol csupán egy elgépelést javított a dokumentum címében, két gigabájt újraírása húsz bájt megváltoztatásáért képtelenség — és ha a fájl digitális aláírást hordozott, az újraírás épp most tette tönkre
Miért töri el egy PDF mentése a digitális aláírását?
Egy PDF digitális aláírása nem a dokumentum logikai tartalmát írja alá, hanem a fizikai fájl bájttartományait. Az aláírásszótár /ByteRange bejegyzése pontosan rögzíti, a fájl mely szakaszait fedi le a kriptográfiai kivonat. Minden olyan mentési művelet, amely újraszerializálja ezeket a bájtokat — még az is, amely szemantikailag azonos dokumentumot állít elő —, megváltoztatja a kivonatot, és minden validátor törtnek jelenti az aláírást. Ez így volt eltervezve: az aláírás azokat a bájtokat tanúsítja, amelyeket az aláíró látott, nem valamiféle elvont dokumentummodellt
Az inkrementális frissítések a menekülőút, amelyet a PDF specifikációja kínál. Mivel egy inkrementális mentés az eredeti %%EOF után fűzi hozzá az új adatot, és soha nem nyúl az aláírt bájttartományokhoz, a meglévő aláírás továbbra is érvényes az általa lefedett bájtokra. A validátorok ezután külön sorolják be a hozzáfűzött változásokat — egy második aláírást, egy űrlapkitöltést, egy jegyzetet —, és eldöntik, hogy engedélyezett módosításokról van-e szó. Minden többaláírásos munkafolyamat ezen múlik: minden aláíró egy inkrementális szakaszt tesz az előző tetejére. Ha aláírási futószalagot épít, a PAdES aláírásról és érvényesítésről Delphiben szóló társcikk részletesen tárgyalja, hogyan hatnak egymásra az aláírások bájttartományai és az inkrementális szakaszok
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állyal definiálja a modellt. Először: az eredeti fájl tartalma teljesen érintetlen marad — egyetlen bájt sem mozdul. Másodszor: a megváltozott és az újonnan létrehozott objektumok az utolsó %%EOF után kerülnek hozzáfűzésre, mindegyik ugyanazzal az objektumszámmal, amellyel korábban rendelkezett (a megváltozott objektumok egyszerűen újabb definíciót kapnak, amely árnyékolja a régit). Harmadszor: egy új kereszthivatkozási szakasz és trailer fűződik hozzá; a trailer /Prev bejegyzése az előző kereszthivatkozási szakasz bájteltolására mutat vissza, és így olyan láncot alkot, amelyet az olvasó a legújabbtól a legrégebbi felé bejárva oldja fel minden objektum legfrissebb definícióját
Ebből a szerkezetből két hasznos tulajdonság következik. A frissítések ára a változás mértékével arányos, nem a dokumentum méretével — a hozzáfűzés költsége a módosított objektumok mérete plusz egy csekély xref- és trailer-ráfordítás. A fájl emellett a saját verziótörténetévé válik: minden korábbi revízió fizikailag jelen van, tehát egy auditor bármelyik korábbi %%EOF jelnél levághatja a fájlt, és pontosan azt a dokumentumot kapja vissza, amely abban a pillanatban létezett. Azoknál a megfelelőségi munkafolyamatoknál, amelyeknek bizonyítaniuk kell, hogyan nézett ki egy dokumentum minden módosítás előtt, ez a beépített auditnyom gyakran a döntő érv az inkrementális mentés mellett
Inkrementális frissítés írása az AppendToStream hívással
A losLab PDF Library az AppendToStream(AppendMode: Integer; OutStream: TStream): Integer hívással teszi elérhetővé az inkrementális kimenetet, amely siker esetén 1-et, hiba esetén 0-t ad vissza. Az AppendMode paraméter választja ki, mi kerül a célfolyamba. A 0-s mód teljes fájlt ír: előbb az eredeti forrásbájtokat másolja a folyamba, majd hozzáfűzi az inkrementális szakaszt. Az 1-es mód csak magát az inkrementális szakaszt írja — a különbözetet —, és a forrásbájtokat teljesen kihagyja. A 2-es mód előbb egy hívó által megadott előtagot ír ki, amelyet a SetAppendInputFromString hívással regisztráltak, 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;
// Apró szerkesztés: az a fajta változás, amely nem
// indokolja az egész fájl újraírását
Doc.SetInformation(3, 'Amended 2026-07-04'); // a 3-as kulcs = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: csak az inkrementális szakasz kiírása.
// 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;
Rendszertervezési szempontból az 1-es mód az érdekes. Mivel a különbözet önmagában áll, az eredetitől függetlenül szállítható: a revíziókat külön blobként tárolhatja objektumtárolóban, csak a különbözeteket replikálhatja egy távoli helyre, vagy bármelyik revíziót visszaállíthatja az alapfájl és az őt követő növekmények láncának összefűzésével. A visszaállítás szabálya sima bájtösszefűzés — előbb az eredeti fájl, majd sorban minden különbözet —, mert pontosan ezt az elrendezést írja elő a §7.5.6 egy inkrementálisan frissített fájlra
Hogyan számol a könyvtár xref-eltolásokat az eredeti fájl másolása nélkül?
Az inkrementális szakaszon belüli kereszthivatkozási bejegyzéseknek abszolút bájteltolásokat kell tartalmazniuk — a teljes fájl elejétől mért pozíciókat, nem a különbözet elejétől mérteket. Ez fejtörőt okoz az 1-es módnál: az író soha nem bocsátja ki az eredeti bájtokat, mégis minden rögzített eltolásának úgy kell tennie, mintha ott lennének. A losLab PDF Library ezt egy belső folyamadapterrel, a TPDFAppendSectionStream osztállyal oldja meg, amely virtuális koordinátateret mutat a szerializálónak. Az adapter az eredeti fájl bájthosszával mint alapeltolással jön létre, a pozícióját és a méretét ennek az alapnak és az eddig hozzáfűzötteknek az összegeként jelenti, és csak az újonnan írt bájtokat továbbítja a hívó célfolyamába
Ennek az a következménye, hogy az 1-es mód soha nem hozza létre a forrásdokumentum másolatát — sem lemezen, sem memóriában. A naiv megvalósítás (a teljes fájl kiírása ideiglenes pufferbe, majd a végének leszelése) az eredeti PDF egészének átmeneti másolatát hordozná, ami gigabájtos nagyságrendű bemeneteknél pontosan az a költség, amelynek elkerüléséért az inkrementális frissítések léteznek. Ez az eltolás-virtualizációs technika közeli rokona a könyvtárban máshol használt bájthivatkozás-eltolásnak; a gyors PDF-egyesítésről bájthivatkozás-eltolással szóló cikk ugyanezt a gondolatot mutatja be dokumentumok összefűzésére, a nagy PDF-ek egyesítéséről és szétbontásáról közvetlen fájlhozzáféréssel szóló útmutató pedig a körülötte lévő I/O-architektúrát tárgyalja azokra a fájlokra, amelyek nem férnek kényelmesen a memóriába
Teljes mentések folyamba a SaveToStream hívással
Az inkrementális kimenet a folyamalapú történet fele; a másik fele az, mi történik egy teljes mentéskor. A losLab PDF Library SaveToStream hívása közvetlenül a célfolyamba hajtja a dokumentumszerializálót ahelyett, hogy előbb az egész dokumentumot egy köztes AnsiString értékbe rajzolná, majd azt a puffert egyetlen hívással kiírná. A régebbi megközelítés működött, de azzal járt, hogy minden teljes mentés átmenetileg a kimenet második teljes másolatát tartotta a memóriában — 10 MB méretnél ártalmatlan, 500 MB méretnél fájdalmas, több gigabájtos kimenetnél pedig kemény fal 32 bites folyamatokon. A közvetlen szerializálás mellett a memóriacsúcs a dokumentum objektumszerkezeteit követi, nem a szerializált hosszát
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... teljes újraírást indokló 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ódról szóló tanulság: amikor az AppendToFile 0-t adott vissza
Egy itteni regressziót érdemes elmesélni, mert a hibaminta általánosítható. Az AppendToFile(FileName) közvetlenül egy lemezen lévő PDF-hez fűz inkrementális frissítést — ez a természetes hívás egy helyben zajló auditnyom-munkafolyamathoz: fájl betöltése, változtatás, hozzáfűzés ugyanahhoz az útvonalhoz. A v3.71.2 verzióban pontosan ez a sorozat kezdett 0-t visszaadni. A kiváltó ok a betöltőben ült, nem az íróban: a nagy dokumentumok igény szerinti olvasásának támogatásához a LoadFromFile a dokumentumobjektum teljes élettartamára nyitva tartja a forrásfájl leíróját, és ezt a leírót fmShareDenyWrite móddal nyitották meg. Amikor az AppendToFile ezután írásra próbálta újranyitni ugyanazt a fájlt, a betöltő saját megosztási módja tagadta meg tőle, és az API még egyetlen bájt kiírása előtt elbukott
A javítás a betöltő megosztási módját fmShareDenyNone értékre lazította, ami épp azért biztonságos, ami egy inkrementális hozzáfűzés: 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ú életű leírója kiszolgál. Az általános tanulság mindenkinek, aki ezt a könyvtárat csomagolja — vagy hasonló folyamalapú betöltőket épít —, az, hogy a lusta, leírót fogó olvasók és az ugyanarra a fájlra írók feszültségben állnak egymással, és a megnyitáskor választott megosztási mód API-szerződés, nem megvalósítási részlet. Ha az AppendToFile valaha 0-t ad vissza a kódjában, először azt nézze meg, hogy a folyamatában nem tartja-e még valami korlátozó megosztási móddal a célfájlt
Az őszinte költségek: mikor rossz eszköz az inkrementális frissítés
Az inkrementális frissítés fájlméretet cserél írási hatékonyságra, és ez a csere 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, tehát egy százszor szerkesztett dokumentum halott objektumokat és hosszú /Prev láncot halmoz fel, amelyet minden olvasónak be kell járnia. Ami rosszabb: a „törölt” tartalom nem tűnt el, az ötödik revízióban eltávolított szöveg fizikailag még mindig ott van a negyedik revízió bájtjaiban, és bárki visszanyerheti, aki levágja a fájlt. A kitakarás, a fertőtlenítés vagy bármilyen érzékeny tartalom eltávolítása ezért teljes újraírást követel — egy kitakarás inkrementális mentése adatszivárgás, csak több lépésben
A teljes mentés akkor is a helyes hívás, amikor a cél a tömörítés (a felhalmozott növekmények és a nem használt objektumok kipréselése), amikor dokumentumszintű tulajdonságok, például a titkosítás változik — az újratitkosítás minden sztringhez és folyamhoz hozzányúl, tehát semmi „inkrementális” nem marad a változásban —, vagy amikor tiszta szállítmányt állít elő, amellyel nem szabad együtt utaznia a szerkesztési történetnek. Észszerű szabály: használja az AppendToStream vagy az AppendToFile hívást, amíg a dokumentum él és változik, különösen ha már aláírásokat hordoz; és használjon teljes SaveToStream újraírást az életciklus határain, amikor a dokumentum elhagyja a rendszerét, vagy a történetét lapítani kell
Az inkrementális frissítések, a virtuális eltolású különbözetkimenet és a közvetlenül folyamba történő szerializálás mind a Delphihez, C#-hoz és VB.NET-hez készült szabványos losLab PDF Library részei; a termékoldal a teljes mentési és hozzáfűzési API-felületet felsorolja a fent tárgyalt aláírási és nagyfájlos funkciók mellett