Műszaki cikk

Inkrementális PDF-frissítések Delphi-ben: AppendToStream útmutató

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