Odborný článok

Inkrementálne aktualizácie PDF v Delphi: AppendToStream

Inkrementálne aktualizácie PDF umožňujú aplikácii v Delphi upraviť dokument tak, že pripojí len zmenené objekty a každý pôvodný bajt nechá nedotknutý. Knižnica losLab PDF Library to realizuje cez AppendToStream, ktorá zapíše iba inkrementálnu sekciu definovanú v norme ISO 32000-1 §7.5.6, takže úprava jednej záložky v súbore s veľkosťou 2 GB stojí kilobajty výstupu namiesto úplného prepisu. Ten istý mechanizmus je dôvodom, prečo sa podpísané dokumenty dajú aktualizovať bez znehodnotenia ich podpisov

Problém, ktorý to rieši, je veľmi konkrétny. Plné uloženie prepíše celý súbor: každý objekt sa nanovo serializuje, každý offset krížových odkazov sa prepočíta a výstup nemá s vstupom žiadny vzťah na úrovni bajtov. Pri faktúre s veľkosťou 40 KB to nevadí. Pri naskenovanom archíve s veľkosťou 2 GB, v ktorom ste opravili len preklep v názve dokumentu, je prepisovanie dvoch gigabajtov kvôli zmene dvadsiatich bajtov absurdné — a ak súbor niesol digitálny podpis, prepis ho práve zničil

Prečo uloženie PDF poškodí jeho digitálny podpis?

Digitálny podpis v PDF nepodpisuje logický obsah dokumentu; podpisuje rozsahy bajtov fyzického súboru. Položka /ByteRange v slovníku podpisu presne zaznamenáva, ktoré úseky súboru kryptografický odtlačok pokrýva. Akákoľvek operácia uloženia, ktorá tieto bajty nanovo serializuje — aj taká, ktorá vyprodukuje sémanticky identický dokument — zmení odtlačok a každý validátor nahlási podpis ako porušený. Je to zámer: podpis potvrdzuje bajty, ktoré podpisujúci videl, nie akýsi abstraktný model dokumentu

Inkrementálne aktualizácie sú únikový východ, ktorý špecifikácia PDF poskytuje. Keďže inkrementálne uloženie pripája nové dáta až za pôvodné %%EOF a nikdy sa nedotkne podpísaných rozsahov bajtov, existujúci podpis naďalej platí voči bajtom, ktoré pokrýva. Validátory potom pripojené zmeny klasifikujú osobitne — druhý podpis, vyplnenie formulára, anotáciu — a rozhodnú, či ide o povolené úpravy. Každý pracovný postup s viacerými podpismi na tomto stojí: každý podpisujúci pridá inkrementálnu sekciu nad tú predchádzajúcu. Ak budujete podpisové pipeline, sprievodný článok o podpisovaní a validácii PAdES v Delphi podrobne rozoberá, ako spolu rozsahy bajtov podpisu a inkrementálne sekcie fungujú

PDF Library for Delphi: Diagram rozsahov bajtov ukazujúci, prečo plné uloženie PDF znehodnotí digitálne podpisy, zatiaľ čo inkrementálne aktualizácie cez AppendToStream ich zachovajú
Keďže odtlačok pokrýva pevné rozsahy bajtov, plné uloženie ich rozhádže a rozbije validáciu, kým inkrementálna aktualizácia pripája za podpísanú oblasť a každý reťazený podpis prežije

Ako fungujú inkrementálne aktualizácie podľa ISO 32000-1 §7.5.6

Norma ISO 32000-1 §7.5.6 definuje tento model tromi pravidlami. Po prvé, pôvodný obsah súboru zostáva úplne nedotknutý — nepohne sa ani jeden bajt. Po druhé, zmenené a novo vytvorené objekty sa pripájajú za posledné %%EOF, každý s rovnakým číslom objektu, aké mal predtým (zmenené objekty jednoducho dostanú novšiu definíciu, ktorá tú starú zatieni). Po tretie, pripojí sa nová sekcia krížových odkazov a nový trailer; položka /Prev v traileri ukazuje späť na bajtový offset predchádzajúcej sekcie krížových odkazov a tvorí reťaz, ktorou čítačka prechádza od najnovšej po najstaršiu, aby každý objekt vyhodnotila na jeho najaktuálnejšiu definíciu

Z tejto štruktúry vyplývajú dve užitočné vlastnosti. Aktualizácie sú lacné v pomere k tomu, čo sa zmenilo, nie k veľkosti dokumentu — náklady pripojenia predstavujú veľkosť zmenených objektov plus malá réžia xref a traileru. A súbor sa stáva vlastnou históriou verzií: každá predchádzajúca revízia je stále fyzicky prítomná, takže audítor môže súbor orezať na ktoromkoľvek staršom %%EOF a získať presne ten dokument, ktorý v danom bode existoval. Pri pracovných postupoch pre súlad s predpismi, ktoré musia preukázať, ako dokument vyzeral pred každým dodatkom, býva táto zabudovaná audítorská stopa rozhodujúcim argumentom pre inkrementálne ukladanie

Zápis inkrementálnej aktualizácie pomocou AppendToStream

Knižnica losLab PDF Library sprístupňuje inkrementálny výstup cez AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, ktorá vracia 1 pri úspechu a 0 pri zlyhaní. Parameter AppendMode volí, čo skončí v cieľovom streame. Režim 0 zapíše kompletný súbor: do streamu sa najprv skopírujú pôvodné zdrojové bajty a potom sa pripojí inkrementálna sekcia. Režim 1 zapíše iba samotnú inkrementálnu sekciu — deltu — a zdrojové bajty úplne preskočí. Režim 2 najprv zapíše prefix dodaný volajúcim a zaregistrovaný cez SetAppendInputFromString a nad neho pripojí sekciu s aktualizáciou

Porovnanie režimov 0, 1 a 2 metódy AppendToStream, ktoré zapisujú zdrojové bajty, prefix volajúceho a inkrementálnu sekciu do streamu v Delphi
AppendMode volí, čo stream dostane, od kompletnej kópie cez prefix od volajúceho plus deltu, pričom režim 1 vydáva prenositeľnú holú inkrementálnu sekciu
var
  Doc: TPDFlib;
  Delta: TMemoryStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('contract.pdf', '') <= 0 then
      Exit;

    // Malá úprava: presne ten druh zmeny, ktorý by nemal
    // spustiť prepis celého súboru
    Doc.SetInformation(3, 'Amended 2026-07-04');  // kľúč 3 = /Subject

    Delta := TMemoryStream.Create;
    try
      // AppendMode = 1: zapíš iba inkrementálnu sekciu.
      // Pôvodné bajty + Delta = kompletné, platné 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 z hľadiska návrhu systému ten zaujímavý. Keďže je delta samostatná, môžete ju distribuovať nezávisle od originálu: ukladať revízie ako oddelené bloby v objektovom úložisku, replikovať na vzdialenú lokalitu iba delty alebo zrekonštruovať ľubovoľnú revíziu spojením základného súboru s reťazou jeho prírastkov. Pravidlo rekonštrukcie je obyčajné zreťazenie bajtov — najprv pôvodný súbor, potom každá delta v poradí — pretože presne také rozloženie predpisuje §7.5.6 pre inkrementálne aktualizovaný súbor

Ako knižnica počíta offsety xref bez kopírovania pôvodného súboru?

Záznamy krížových odkazov vnútri inkrementálnej sekcie musia obsahovať absolútne bajtové offsety — pozície merané od začiatku kompletného súboru, nie od začiatku delty. To vytvára hlavolam pre režim 1: zapisovač pôvodné bajty nikdy nevydá, no každý offset, ktorý zaznamená, musí predstierať, že tam sú. Knižnica losLab PDF Library to rieši interným adaptérom streamu TPDFAppendSectionStream, ktorý serializátoru predkladá virtuálny súradnicový priestor. Adaptér sa vytvára s bajtovou dĺžkou pôvodného súboru ako základným offsetom, svoju pozíciu aj veľkosť hlási ako tento základ plus všetko, čo bolo doteraz pripojené, a do cieľového streamu volajúceho posiela iba novo zapísané bajty

Dôsledkom je, že režim 1 nikdy nevytvorí kópiu zdrojového dokumentu — ani na disku, ani v pamäti. Naivná implementácia (zapísať celý súbor do dočasného bufferu a potom odrezať koniec) by so sebou niesla dočasnú kópiu celého pôvodného PDF, čo pri vstupoch v gigabajtovej mierke predstavuje presne tie náklady, kvôli ktorým inkrementálne aktualizácie existujú. Táto technika virtualizácie offsetov je blízkou príbuznou posúvania bajtových odkazov používaného inde v knižnici; článok o rýchlom zlučovaní PDF s posúvaním bajtových odkazov ukazuje rovnakú myšlienku aplikovanú na spájanie dokumentov a sprievodca po zlučovaní a delení veľkých PDF s priamym prístupom k súborom pokrýva okolitú I/O architektúru pre súbory, ktoré sa do RAM pohodlne nezmestia

Streamované plné uloženie cez SaveToStream

Inkrementálny výstup je len polovica príbehu o streamovaní; druhou polovicou je to, čo sa deje pri plnom uložení. Metóda SaveToStream v knižnici losLab PDF Library riadi serializátor dokumentu priamo proti cieľovému streamu, namiesto toho, aby najprv celý dokument vykreslila do medziľahlého AnsiString a potom tento buffer vypísala jedným volaním. Starší prístup fungoval, ale znamenal, že každé plné uloženie dočasne držalo v pamäti druhú kompletnú kópiu výstupu — neškodné pri 10 MB, bolestivé pri 500 MB a tvrdý strop pri viacgigabajtových výstupoch v 32-bitových procesoch. Priama serializácia spôsobí, že špičková pamäť sleduje objektové štruktúry dokumentu namiesto jeho serializovanej dĺžky

var
  Doc: TPDFlib;
  Output: TFileStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('archive.pdf', '') <= 0 then
      Exit;

    // ... úpravy, ktoré ospravedlňujú plný prepis ...

    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;

Lekcia o režime zdieľania: keď AppendToFile vracalo 0

Jednu regresiu v tejto oblasti sa oplatí prerozprávať, pretože vzorec zlyhania sa dá zovšeobecniť. Metóda AppendToFile(FileName) pripája inkrementálnu aktualizáciu priamo k existujúcemu PDF na disku — prirodzené volanie pre pracovný postup s audítorskou stopou na mieste: načítať súbor, urobiť zmenu, pripojiť ju na tú istú cestu. Vo verzii v3.71.2 začala presne táto postupnosť vracať 0. Príčina sedela v načítavači, nie v zapisovači: aby knižnica podporila čítanie veľkých dokumentov na požiadanie, LoadFromFile drží handle zdrojového súboru otvorený počas celej životnosti objektu dokumentu a tento handle bol otvorený s fmShareDenyWrite. Keď sa AppendToFile potom pokúsilo znovu otvoriť ten istý súbor na zápis, vlastný režim zdieľania načítavača mu to odmietol a API zlyhalo skôr, než zapísalo jediný bajt

Oprava uvoľnila režim zdieľania načítavača na fmShareDenyNone, čo je bezpečné práve preto, čím inkrementálne pripojenie je: pridáva bajty striktne za koniec súboru a nikdy neprepisuje oblasť, ktorú obsluhuje dlho žijúci handle čítačky. Všeobecná lekcia pre každého, kto túto knižnicu obaľuje — alebo buduje podobné streamované načítavače — znie, že lenivé čítačky držiace handle a zapisovače do toho istého súboru sú v napätí a režim zdieľania, ktorý zvolíte pri otvorení, je kontrakt API, nie detail implementácie. Ak vám AppendToFile niekedy vráti 0, najprv skontrolujte, či niečo iné vo vašom procese stále nedrží cieľový súbor s reštriktívnym režimom zdieľania

Úprimné náklady: kedy sú inkrementálne aktualizácie nesprávnym nástrojom

Inkrementálne aktualizácie vymieňajú veľkosť súboru za efektivitu zápisu a tento obchod nie je vždy výhodný. Každá revízia pripojí svoje zmenené objekty, kým nahradené definície zostávajú v súbore, takže dokument upravovaný stokrát nazbiera mŕtve objekty a dlhú reťaz /Prev, ktorou musí prejsť každá čítačka. Horšie je, že „zmazaný“ obsah nie je preč: text odstránený v piatej revízii je stále fyzicky prítomný v bajtoch štvrtej revízie a obnoví ho ktokoľvek, kto súbor oreže. Redakcia, sanitizácia či akékoľvek odstraňovanie citlivého obsahu si preto vyžadujú plný prepis — inkrementálne uloženie redakcie je únik dát s pár krokmi navyše

Plné uloženie je správnou voľbou aj vtedy, keď je cieľom kompaktácia (vytlačenie nahromadených prírastkov a nepoužívaných objektov), keď meníte vlastnosti platné pre celý dokument, napríklad šifrovanie — opätovné zašifrovanie sa dotkne každého reťazca a streamu, takže na tejto zmene nezostáva nič „inkrementálne“ — alebo keď vyrábate čistý výstup, s ktorým by história úprav cestovať nemala. Rozumné pravidlo: používajte AppendToStream alebo AppendToFile, kým dokument žije a mení sa, najmä keď už nesie podpisy; siahnite po plnom prepise cez SaveToStream na hraniciach životného cyklu, keď dokument opúšťa váš systém alebo keď treba jeho históriu splošťiť

PDF Library for Delphi: sprievodca rozhodovaním medzi inkrementálnym ukladaním cez AppendToStream a plnými prepismi cez SaveToStream počas životného cyklu dokumentu
Pripájajte, kým je dokument živý a podpísaný, a prepnite na plný prepis vždy, keď musia bajty zmiznúť, história sa splošťiť alebo sa mení šifrovanie

Inkrementálne aktualizácie, výstup delty s virtuálnymi offsetmi aj serializácia priamo do streamu sú súčasťou štandardnej knižnice losLab PDF Library pre Delphi, C# a VB.NET; stránka produktu uvádza celú plochu API pre ukladanie a pripájanie spolu s funkciami pre podpisovanie a veľké súbory, o ktorých bola reč vyššie