Inkrementelle PDF-oppdateringer lar et Delphi-program endre et dokument ved å legge til bare de objektene som er endret, uten å røre en eneste av de opprinnelige bytene. losLab PDF Library implementerer dette gjennom AppendToStream, som skriver kun den inkrementelle seksjonen som ISO 32000-1 §7.5.6 definerer, slik at en redigering av ett bokmerke i en 2 GB stor fil koster kilobyte med utdata i stedet for en full omskriving. Det er den samme mekanismen som gjør at signerte dokumenter kan oppdateres uten at signaturene blir ugyldige
Problemet dette løser er konkret. En full lagring skriver om hele filen: hvert objekt serialiseres på nytt, hver kryssreferanseforskyvning beregnes på nytt, og utdataene har ingen sammenheng med inndataene på bytenivå. For en faktura på 40 KB går det helt fint. For et skannet arkiv på 2 GB der du bare rettet en skrivefeil i dokumenttittelen, er det absurd å skrive om to gigabyte for å endre tjue byte — og hvis filen bar en digital signatur, ødela omskrivingen den nettopp
Hvorfor ødelegger lagring av en PDF den digitale signaturen?
En digital PDF-signatur signerer ikke dokumentets logiske innhold; den signerer byteområder i den fysiske filen. Oppføringen /ByteRange i signaturordboken registrerer nøyaktig hvilke strekk av filen det kryptografiske sammendraget dekker. Enhver lagringsoperasjon som serialiserer disse bytene på nytt — selv en som gir et semantisk identisk dokument — endrer sammendraget, og enhver validator vil rapportere signaturen som brutt. Slik skal det være: signaturen bevitner de bytene signataren så, ikke en abstrakt dokumentmodell
Inkrementelle oppdateringer er nødutgangen PDF-spesifikasjonen tilbyr. Fordi en inkrementell lagring legger nye data etter den opprinnelige %%EOF og aldri rører de signerte byteområdene, fortsetter den eksisterende signaturen å validere mot de bytene den dekker. Validatorer klassifiserer så de tilføyde endringene for seg — en ny signatur, en skjemautfylling, en annotasjon — og avgjør om de er tillatte endringer. Enhver arbeidsflyt med flere signaturer hviler på dette: hver signatar legger en inkrementell seksjon oppå den forrige. Bygger du signeringsløyper, dekker følgeartikkelen om PAdES-signering og validering i Delphi i detalj hvordan signaturens byteområder og inkrementelle seksjoner spiller sammen
Slik fungerer inkrementelle oppdateringer under ISO 32000-1 §7.5.6
ISO 32000-1 §7.5.6 definerer modellen i tre regler. For det første forblir det opprinnelige filinnholdet helt intakt — ikke én byte flyttes. For det andre legges endrede og nyopprettede objekter til etter den siste %%EOF, hvert med samme objektnummer som det hadde før (endrede objekter får rett og slett en nyere definisjon som skygger for den gamle). For det tredje legges en ny kryssreferanseseksjon og en ny trailer til; trailerens /Prev-oppføring peker tilbake til byteforskyvningen til den forrige kryssreferanseseksjonen, og danner en kjede som en leser går gjennom fra nyest til eldst for å løse hvert objekt opp til sin nyeste definisjon
To nyttige egenskaper faller ut av denne strukturen. Oppdateringer koster i forhold til hvor mye som ble endret, ikke i forhold til dokumentets størrelse — tilleggskostnaden er størrelsen på de endrede objektene pluss litt overhead til xref og trailer. Og filen blir sin egen versjonshistorikk: hver tidligere revisjon er fortsatt fysisk til stede, så en revisor kan kutte filen ved en hvilken som helst tidligere %%EOF og få tilbake nøyaktig det dokumentet som fantes på det tidspunktet. For arbeidsflyter med etterlevelseskrav som må kunne bevise hvordan et dokument så ut før hver endring, er dette innebygde revisjonssporet ofte det avgjørende argumentet for inkrementelle lagringer
Skrive en inkrementell oppdatering med AppendToStream
losLab PDF Library eksponerer inkrementell utskriving gjennom AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, som returnerer 1 ved suksess og 0 ved feil. Parameteren AppendMode velger hva som havner i målstrømmen. Modus 0 skriver en komplett fil: de opprinnelige kildebytene kopieres til strømmen først, og deretter legges den inkrementelle seksjonen til. Modus 1 skriver bare selve den inkrementelle seksjonen — deltaet — og hopper helt over kildebytene. Modus 2 skriver først et prefiks fra kalleren, registrert via SetAppendInputFromString, og legger så oppdateringsseksjonen oppå det
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Liten redigering: den typen endring som ikke bør
// utløse en omskriving av hele filen
Doc.SetInformation(3, 'Amended 2026-07-04'); // nøkkel 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: skriv bare den inkrementelle seksjonen.
// Opprinnelige byte + Delta = en komplett, gyldig PDF.
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
Modus 1 er den interessante for systemdesign. Siden deltaet er selvstendig, kan du sende det uavhengig av originalen: lagre revisjoner som separate blobber i objektlagring, replikere bare deltaene til et fjernsted, eller gjenskape en hvilken som helst revisjon ved å sette sammen basisfilen med kjeden av inkrementer. Regelen for gjenskaping er ren sammenslåing av byte — originalfilen først, deretter hvert delta i rekkefølge — fordi det er nøyaktig den strukturen §7.5.6 foreskriver for en inkrementelt oppdatert fil
Hvordan beregner biblioteket xref-forskyvninger uten å kopiere originalfilen?
Kryssreferanseoppføringene inne i en inkrementell seksjon må inneholde absolutte byteforskyvninger — posisjoner målt fra starten av den komplette filen, ikke fra starten av deltaet. Det skaper et puslespill for modus 1: skriveren sender aldri ut de opprinnelige bytene, likevel må hver forskyvning den noterer late som om de er der. losLab PDF Library løser dette med en intern strømadapter, TPDFAppendSectionStream, som presenterer et virtuelt koordinatrom for serialisereren. Adapteren opprettes med originalfilens bytelengde som basisforskyvning, rapporterer posisjon og størrelse som denne basisen pluss det som er lagt til så langt, og videresender bare de nyskrevne bytene til kallerens målstrøm
Konsekvensen er at modus 1 aldri materialiserer en kopi av kildedokumentet — verken på disk eller i minnet. Den naive implementasjonen (skriv hele filen til en midlertidig buffer, og skjær så av halen) ville båret en flyktig kopi av hele original-PDF-en, noe som for inndata i gigabyteklassen er nøyaktig den kostnaden inkrementelle oppdateringer finnes for å unngå. Denne teknikken med virtuelle forskyvninger er nær beslektet med byteforskyvningen av referanser som brukes andre steder i biblioteket; artikkelen om rask PDF-sammenslåing med forskyvning av bytereferanser viser den samme ideen anvendt på å kombinere dokumenter, og veiledningen til sammenslåing og deling av store PDF-filer med direkte filtilgang dekker den omkringliggende I/O-arkitekturen for filer som ikke får plass i RAM
Strømmende fulle lagringer med SaveToStream
Inkrementell utskriving er halve strømmehistorien; den andre halvparten er hva som skjer ved en full lagring. SaveToStream i losLab PDF Library kjører dokumentserialisereren rett mot målstrømmen, i stedet for først å bygge hele dokumentet inn i en mellomliggende AnsiString og deretter skrive den bufferen ut i ett kall. Den eldre framgangsmåten virket, men den betydde at hver full lagring en kort stund holdt en ekstra komplett kopi av utdataene i minnet — harmløst ved 10 MB, smertefullt ved 500 MB, og en vegg for utdata på flere gigabyte i 32-bits prosesser. Direkte serialisering gjør at minnetoppen følger dokumentets objektstrukturer i stedet for lengden på det serialiserte resultatet
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... redigeringer som forsvarer en full omskriving ...
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;
En lærdom om delingsmodus: da AppendToFile returnerte 0
Én regresjon på dette området er verdt å gjenfortelle fordi feilmønsteret lar seg generalisere. AppendToFile(FileName) legger en inkrementell oppdatering rett til en eksisterende PDF på disk — det naturlige kallet for en arbeidsflyt med revisjonsspor på stedet: last inn en fil, gjør en endring, legg til på samme sti. I v3.71.2 begynte nøyaktig den sekvensen å returnere 0. Årsaken lå i innlasteren, ikke i skriveren: for å støtte lesing av store dokumenter ved behov holder LoadFromFile filhåndtaket til kilden åpent så lenge dokumentobjektet lever, og det håndtaket ble åpnet med fmShareDenyWrite. Da AppendToFile deretter forsøkte å åpne den samme filen på nytt for skriving, nektet innlasterens egen delingsmodus den tilgang, og API-et feilet før det fikk skrevet en eneste byte
Rettelsen løsnet innlasterens delingsmodus til fmShareDenyNone, noe som er trygt nettopp på grunn av hva en inkrementell tilføyelse er: den legger byte strengt etter slutten av filen og skriver aldri om det området leserens langlevde håndtak betjener. Den generelle lærdommen for alle som pakker inn dette biblioteket — eller bygger lignende strømmende innlastere — er at late lesere som holder på håndtak og skrivere mot samme fil står i spenn, og at delingsmodusen du velger ved åpning er en API-kontrakt, ikke en implementasjonsdetalj. Hvis AppendToFile noen gang returnerer 0 i koden din, sjekk først om noe annet i prosessen din fortsatt holder målfilen med en restriktiv delingsmodus
De ærlige kostnadene: når inkrementelle oppdateringer er feil verktøy
Inkrementelle oppdateringer bytter filstørrelse mot skriveeffektivitet, og byttet er ikke alltid gunstig. Hver revisjon legger til sine endrede objekter mens de erstattede definisjonene blir liggende i filen, så et dokument som er redigert hundrevis av ganger samler opp døde objekter og en lang /Prev-kjede som enhver leser må gå gjennom. Verre er det at "slettet" innhold ikke er borte: tekst som ble fjernet i revisjon fem, ligger fortsatt fysisk i bytene fra revisjon fire, og kan hentes fram av hvem som helst som kutter filen. Sladding, sanering eller enhver fjerning av sensitivt innhold krever derfor en full omskriving — en inkrementell lagring av en sladding er en datalekkasje med noen ekstra trinn
En full lagring er også det riktige valget når målet er komprimering (å presse ut oppsamlede inkrementer og ubrukte objekter), når dokumentomfattende egenskaper som kryptering endres — ny kryptering rører hver streng og hver strøm, så det er ingenting "inkrementelt" igjen ved endringen — eller når du lager en ren leveranse der redigeringshistorikken ikke skal følge med filen. En fornuftig tommelfingerregel: bruk AppendToStream eller AppendToFile mens et dokument lever og endres, særlig når det bærer signaturer; bruk en full omskriving med SaveToStream ved livssyklusgrenser, når dokumentet forlater systemet ditt eller historikken må flates ut
Inkrementelle oppdateringer, delta-utskriving med virtuelle forskyvninger og serialisering rett til strøm er alle en del av standardutgaven av losLab PDF Library for Delphi, C# og VB.NET; produktsiden lister hele API-flaten for lagring og tilføyelse ved siden av signerings- og storfilfunksjonene som er omtalt over