PDF prieaugio atnaujinimai leidžia Delphi programai keisti dokumentą pridedant tik pakeistus objektus, o visus pradinius baitus paliekant nepakeistus. „losLab PDF Library“ tai įgyvendina per AppendToStream, kuris įrašo tik prieaugio skyrių, apibrėžtą ISO 32000-1 §7.5.6, todėl vienos žymos redagavimas 2 GB faile reikalauja kilobaitų išvesties vietoj pilno perrašymo. Tas pats mechanizmas leidžia atnaujinti pasirašytus dokumentus nepažeidžiant jų parašų galiojimo
Problema, kurią tai išsprendžia, yra konkreti. Pilnas išsaugojimas perrašo visą failą: kiekvienas objektas yra serijozuojamas iš naujo, kiekviena kryžminių nuorodų poslinkio reikšmė perskaičiuojama, o išvestis neturi jokio baitų lygio ryšio su įvestimi. 40 KB sąskaitai faktūrai tai yra gerai. Tačiau 2 GB skenuotam archyvui, kuriame tik ištaisėte spausdinimo klaidą dokumento pavadinime, perrašyti du gigabaitus, kad pakeistumėte dvidešimt baitų, yra absurdiška – o jei faile buvo skaitmeninis parašas, perrašymas jį tiesiog sugadino
Kodėl PDF failo išsaugojimas sugadina jo skaitmeninį parašą?
PDF skaitmeninis parašas nepasirašo dokumento loginio turinio; jis pasirašo fizinio failo baitų diapazonus. Įrašas /ByteRange parašo žodyne tiksliai fiksuoja, kuriuos failo diapazonus apima kriptografinė santrauka. Bet kokia išsaugojimo operacija, kuri iš naujo serijozuoja tuos baitus – net ir ta, kuri sukuria semantiškai identišką dokumentą – pakeičia santrauką, ir kiekvienas validatorius praneš, kad parašas yra sugadintas. Taip yra suprojektuota: parašas liudija baitus, kuriuos matė pasirašantis asmuo, o ne kažkokį abstraktų dokumento modelį
Prieaugio atnaujinimai yra PDF specifikacijos numatyta išeitis. Kadangi prieaugio išsaugojimas prideda naujus duomenis po pradinio %%EOF ir niekada neliečia pasirašytų baitų diapazonų, esamas parašas išlieka galiojantis baitams, kuriuos jis apima. Tada validatoriai atskirai klasifikuoja pridėtus pakeitimus – antrą parašą, formos užpildymą, anotaciją – ir nusprendžia, ar tai yra leidžiami pakeitimai. Kiekviena kelių parašų darbo eiga priklauso nuo to: kiekvienas pasirašantis asmuo prideda prieaugio skyrių ant ankstesnio viršaus. Jei kuriate pasirašymo srautus, susijęs straipsnis apie PAdES pasirašymą ir patvirtinimą Delphi aplinkoje išsamiai aprašo, kaip sąveikauja parašo baitų diapazonai ir prieaugio skyriai
Kaip veikia prieaugio atnaujinimai pagal ISO 32000-1 §7.5.6
ISO 32000-1 §7.5.6 apibrėžia modelį pagal tris taisykles. Pirma, pradinis failo turinys paliekamas visiškai nepaliestas – nė vienas baitas nejuda. Antra, pakeisti ir naujai sukurti objektai pridedami po paskutinio %%EOF, kiekvienas su tuo pačiu objekto numeriu, kurį turėjo anksčiau (pakeisti objektai tiesiog gauna naujesnį apibrėžimą, kuris užgožia senąjį). Trečia, pridedamas naujas kryžminių nuorodų skyrius ir priekaba (trailer); priekabos įrašas /Prev nukreipia atgal į ankstesnio kryžminių nuorodų skyriaus baitų poslinkį, suformuodamas grandinę, kuria skaitytojas eina nuo naujausio iki seniausio, kad surastų kiekvieno objekto naujausią apibrėžimą
Iš šios struktūros kyla dvi naudingos savybės. Atnaujinimai yra pigūs proporcingai tam, kas pasikeitė, o ne dokumento dydžiui – pridėjimo kaina yra pakeistų objektų dydis plius nedidelis kryžminių nuorodų / priekabos (xref/trailer) pridėtinis krūvis. Be to, failas tampa savo paties versijų istorija: kiekviena ankstesnė versija vis dar fiziškai yra faile, todėl auditorius gali sutrumpinti failą ties bet kuriuo ankstesniu %%EOF ir atkurti tiksliai tą dokumentą, kuris egzistavo tuo metu. Atitikties užtikrinimo procesuose, kur reikia įrodyti, jak dokumentas atrodė prieš kiekvieną pakeitimą, šis integruotas audito pėdsakas dažnai yra lemiamas argumentas pasirinkti prieaugio išsaugojimą
Prieaugio atnaujinimo įrašymas naudojant „AppendToStream“
„losLab PDF Library“ atveria prieaugio išvestį per AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, kuris grąžina 1 sėkmės atveju ir 0 nesėkmės atveju. Parametras AppendMode pasirenka, kas patenka į tikslinį srautą. Režimas 0 įrašo pilną failą: pradiniai šaltinio baitai pirmiausia nukopijuojami į srautą, o tada pridedamas prieaugio skyrius. Režimas 1 įrašo tik patį prieaugio skyrių – skirtumą (delta) – ir visiškai praleidžia šaltinio baitus. Režimas 2 pirmiausia įrašo kviečiančiojo pateiktą prefiksą, užregistruotą per SetAppendInputFromString, o tada ant jo prideda atnaujinimo skyrių
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žimas 1 yra įdomus sistemos kūrimui. Kadangi skirtumas (delta) yra savarankiškas, galite jį gabenti nepriklausomai nuo originalo: saugoti versijas kaip atskirus objektus saugykloje, replikuoti tik skirtumus į nuotolinę svetainę arba atkurti bet kurią versiją sujungiant bazinį failą su jo prieaugio grandine. Atkūrimo taisyklė yra paprastas baitų sujungimas – pirmiausia originalus failas, tada kiekvienas skirtumas iš eilės – nes būtent tokį išdėstymą §7.5.6 nurodo atnaujintam failui
Kaip biblioteka apskaičiuoja kryžminių nuorodų poslinkius nekopijuodama pradinio failo?
Kryžminių nuorodų įrašai prieaugio skyriuje turi turėti absoliučiuosius baitų poslinkius – pozicijas, matuojamas nuo viso failo pradžios, o ne nuo skirtumo (delta) pradžios. Tai sukelia problemą 1 režimui: rašytuvas niekada neišveda pradinių baitų, tačiau kiekvienas jo užfiksuotas poslinkis turi apsimesti, kad jie ten yra. „losLab PDF Library“ tai išsprendžia naudodama vidinį srauto adapterį TPDFAppendSectionStream, kuris pateikia virtualią koordinačių erdvę serijozavimo įrankiui. Adapteris sukuriamas kaip bazinį poslinkį naudojant pradinio failo baitų ilgį, praneša apie savo poziciją ir dydį kaip apie šią bazę plius tai, kas buvo pridėta iki šiol, ir persiunčia tik naujai įrašytus baitus į kviečiančiojo tikslinį srautą
To pasekmė yra ta, kad režimas 1 niekada nesukuria šaltinio dokumento kopijos – nei diske, nei atmintyje. Paprastas įgyvendinimas (įrašyti visą failą į laikiną buferį, o tada nupjauti uodegą) reikalautų laikinos viso pradinio PDF kopijos, o gigabaitų dydžio įvestims tai yra būtent ta kaina, kurios išvengti padeda prieaugio atnaujinimai. Šis poslinkio virtualizavimo būdas yra glaudžiai susijęs su baitų nuorodų poslinkiu (byte-reference shifting), naudojamu kitur bibliotekoje; straipsnyje apie greitą PDF sujungimą su baitų nuorodų poslinkiu rodoma ta pati idėja, pritaikyta dokumentų apjungimui, o vadove apie didelio PDF sujungimą ir skaidymą su tiesiogine failų prieiga aprašoma aplinkinė įvesties/išvesties (I/O) architektūra failams, kurie netelpa RAM atmintyje
Pilnas išsaugojimas srautu naudojant „SaveToStream“
Prieaugio išvestis yra tik pusė istorijos apie srautus; kita pusė yra tai, kas nutinka atliekant pilną išsaugojimą. „losLab PDF Library“ funkcija SaveToStream nukreipia dokumento serijozavimo įrankį tiesiai į tikslinį srautą, užuot pirmiausia atvaizdavusi visą dokumentą į tarpinę AnsiString eilutę ir tada išvedusi tą buferį vienu iškvietimu. Senesnis metodas veikė, tačiau tai reiškė, kad kiekvienas pilnas išsaugojimas laikinai išlaikydavo antrą pilną išvesties kopiją atmintyje – tai nekenksminga esant 10 MB, tačiau skausminga esant 500 MB ir yra neįveikiama riba kelių gigabaitų išvestims 32 bitų procesuose. Tiesioginis serijozavimas leidžia didžiausiai atminties apkrovai sekti dokumento objektų struktūras, o ne jo serijozuotą ilgį
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... edits that justify a full rewrite ...
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;
Dalijimosi režimo (share-mode) pamoka: kai „AppendToFile“ grąžino 0
Vieną šios srities regresiją verta prisiminti, nes jos nesėkmės šablonas yra bendro pobūdžio. AppendToFile(FileName) prideda prieaugio atnaujinimą tiesiai prie esamo PDF diske – natūralus iškvietimas vietoje atliekamo audito pėdsako darbo eigai: įkelti failą, atlikti pakeitimą, pridėti prie to paties kelio. Versijoje v3.71.2 ši seka pradėjo grąžinti 0. Pagrindinė priežastis buvo įkėliklyje, o ne rašytuve: siekiant palaikyti didelių dokumentų skaitymą pagal poreikį, LoadFromFile išlaiko šaltinio failo rankenėlę (handle) atvirą visą dokumento objekto gyvavimo laikotarpį, o ši rankenėlė buvo atidaryta naudojant fmShareDenyWrite. Kai AppendToFile bandė vėl atidaryti tą patį failą rašymui, paties įkėliklio dalijimosi režimas tai atmetė, ir API sugriuvo neįrašiusi nei vieno baito
Taisymas sušvelnino įkėliklio dalijimosi režimą iki fmShareDenyNone, o tai yra saugu būtent dėl to, kas yra prieaugio pridėjimas: jis prideda baitus griežtai po failo pabaigos ir niekada neperrašo srities, kurią aptarnauja ilgalaikė skaitytuvo rankenėlė. Bendra pamoka visiems, naudojantiems šią biblioteką – arba kuriantiems panašius srautinius įkėliklius – yra ta, kad tingūs, rankenėles laikantys skaitytuvai ir to paties failo rašytuvai konfliktuoja, o dalijimosi režimas, kurį pasirenkate atidarymo metu, yra API sutartis, o ne įgyvendinimo detalė. Jei AppendToFile jūsų kode kada nors grąžina 0, pirmiausia patikrinkite, ar kas nors kitas jūsų procese vis dar nelaiko tikslinio failo su ribojančiu dalijimosi režimu
Tikrosios išlaidos: kada prieaugio atnaujinimai yra netinkamas įrankis
Prieaugio atnaujinimai iškeičia failo dydį į rašymo efektyvumą, ir šie mainai ne visada yra naudingi. Kiekviena revizija prideda savo pakeistus objektus, o pakeisti apibrėžimai lieka faile, zodžiu, dokumentas, redaguotas šimtus kartų, sukaupia nenaudojamus objektus ir ilgą /Prev grandinę, kuria turi praeiti kiekvienas skaitytuvas. Dar blogiau, „ištrintas“ turinys niekur nedingsta: tekstas, pašalintas penktojoje versijoje, vis dar fiziškai yra ketvirtosios versijos baituose, kuriuos gali atkurti bet kas, sutrumpinęs failą. Todėl redagavimui (redaction), išvalymui ar bet kokiam jautraus turinio šalinimui reikalingas pilnas perrašymas – prieaugio išsaugojimas redagavimo atveju yra tiesiog duomenų nutekėjimas su papildomais žingsniais
Pilnas išsaugojimas taip pat yra teisingas pasirinkimas, kai tikslas yra sutankinimas (sukauptų prieaugių ir nenaudojamų objektų pašalinimas), kai keičiamos viso dokumento savybės, pavyzdžiui, šifravimas – pakartotinis šifravimas paliečia kiekvieną eilutę ir srautą, todėl pakeitime nelieka nieko „prieauginio“ – arba kai kuriamas galutinis švarus dokumentas, kurio redagavimo istorija neturėtų keliauti kartu su failu. Protinga taisyklė: naudokite AppendToStream arba AppendToFile, kol dokumentas yra aktyvus ir keičiamas, ypač jei jame yra parašų; naudokite pilną perrašymą su SaveToStream gyvavimo ciklo ribose, kai dokumentas palieka jūsų sistemą arba jo istorija turi būti pašalinta
Prieaugio atnaujinimai, virtualaus poslinkio skirtumų (delta) išvestis ir tiesioginis serijozavimas į srautą yra standartinės „losLab PDF Library“, skirtos Delphi, C# ir VB.NET, dalis; produkto puslapyje pateikiamas visas išsaugojimo ir pridėjimo API sąrašas kartu su pasirašymo ir didelių failų funkcijomis, aptartomis aukščiau