Inkrementelle PDF-opdateringer gør det muligt for en Delphi-applikation at ændre et dokument ved kun at tilføje de ændrede objekter, hvilket efterlader hver oprindelig byte urørt. losLab PDF Library implementerer dette via AppendToStream, som kun skriver den inkrementelle sektion defineret af ISO 32000-1 §7.5.6, så en redigering af et enkelt bogmærke i en 2 GB stor fil kun koster kilobytes af output i stedet for en komplet genskrivning. Den samme mekanisme er årsagen til, at signerede dokumenter kan opdateres uden at ugyldiggøre deres signaturer
Det problem, dette løser, er yderst konkret. En komplet gem-handling genskriver hele filen: Hvert objekt re-serialiseres, hver krydsreference-offset genberegnes, og outputtet har ingen byte-niveau relation til inputtet. For en faktura på 40 KB er det fint. For et 2 GB scannet arkiv, hvor du kun rettede en slåfejl i dokumenttitlen, er det absurd at genskrive to gigabytes for at ændre tyve bytes — og hvis filen indeholdt en digital signatur, har genskrivningen lige ødelagt den
Hvorfor ødelægger lagring af en PDF dens digitale signatur?
En digital PDF-signatur signerer ikke dokumentets logiske indhold; den signerer byte-områder i den fysiske fil. Posten /ByteRange i signaturordbogen registrerer præcis, hvilke dele af filen det kryptografiske resumé (digest) dækker. Enhver gem-handling, der re-serialiserer disse bytes — selv en, der producerer et semantisk identisk dokument — ændrer resumét, og enhver validator vil rapporte signaturen som brudt. Dette er tilsigtet: Signaturen attesterer de bytes, som underskriveren så, ikke en eller anden abstrakt dokumentmodel
Inkrementelle opdateringer er den nødudgang, som PDF-specifikationen tilbyder. Fordi en inkrementel lagring tilføjer nye data efter den oprindelige %%EOF og aldrig rører de signerede byte-områder, fortsætter den eksisterende signatur med at validere mod de bytes, den dækker. Validatorer klassificerer derefter de tilføjede ændringer separat — en ekstra signatur, en formularudfyldning, en annotering — og beslutter, om de er tilladte ændringer. Enhver arbejdsgang med flere signaturer afhænger af dette: Hver underskriver tilføjer en inkrementel sektion oven på den forrige. Hvis du bygger signeringpipelines, dækker ledsagerartiklen om PAdES-signering og validering i Delphi i detaljer, hvordan signaturs byte-områder og inkrementelle sektioner interagerer
Hvordan inkrementelle opdateringer fungerer under ISO 32000-1 §7.5.6
ISO 32000-1 §7.5.6 definerer modellen i tre regler. For det første forbliver den oprindelige fils indhold helt intakt — ikke én eneste byte flyttes. For det andet tilføjes ændrede og nyoprettede objekter efter den sidste %%EOF, hver med det samme objektnummer, som det havde før (ændrede objekter får blot en nyere definition, der skygger for den gamle). For det tredje tilføjes en ny krydsreferencesektion og trailer; trailerens /Prev-post peger tilbage på byte-offsettet af den forrige krydsreferencesektion, hvilket danner en kæde, som en læser gennemgår fra nyeste til ældste for at finde frem til hvert objekts nyeste definition
To nyttige egenskaber følger af denne struktur. Opdateringer er billige i forhold til, hvad der er ændret, ikke i forhold til dokumentstørrelsen — omkostningen ved tilføjelsen er størrelsen på de modificerede objekter plus et lille xref-/trailer-overhead. Og filen bliver sin egen versionshistorik: Enhver tidligere revision er stadig fysisk til stede, så en auditor kan afskære filen ved enhver tidligere %%EOF og gendanne præcis det dokument, der eksisterede på det tidspunkt. Til compliance-arbejdsgange, der skal bevise, hvordan et dokument så ud før hver ændring, er denne indbyggede revisionshistorik ofte det afgørende argument for inkrementel lagring
Skrivning af en inkrementel opdatering med AppendToStream
losLab PDF Library eksponerer inkrementelt output via AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, som returnerer 1 ved succes og 0 ved fejl. Parameteren AppendMode vælger, hvad der lander i målstrømmen. Tilstand 0 skriver en komplet fil: De oprindelige kilde-bytes kopieres til strømmen først, og derefter tilføjes den inkrementelle sektion. Tilstand 1 skriver kun selve den inkrementelle sektion — deltaet — og springer helt kilde-bytes over. Tilstand 2 skriver først et brugerdefineret præfiks registreret via SetAppendInputFromString og tilføjer derefter opdateringssektionen oven på det
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;
Tilstand 1 er den mest interessante for systemdesign. Fordi deltaet er selvstændigt, kan du distribuere det uafhængigt af det originale: Gemme revisioner som separate blobs i objektlagring, replikere kun deltas til en ekstern lokation, eller rekonstruere enhver revision by at sammensætte basisfilen med dens kæde af inkrementer. Rekonstruktionsreglen er simpel byte-sammensætning — den oprindelige fil først, derefter hvert delta i rækkefølge — fordi det er netop det layout, §7.5.6 foreskriver for en inkrementelt opdateret fil
Hvordan beregner biblioteket xref-offsets uden at kopiere den oprindelige fil?
Hvordan beregner biblioteket xref-offsets uden at kopiere den oprindelige fil?
Krydsreference-posterne i en inkrementel sektion skal indeholde absolutte byte-offsets — positioner målt fra starten af den komplette fil, ikke fra starten af deltaet. Det skaber et problem for tilstand 1: Skriveren udsender aldrig de oprindelige bytes, men alligevel skal hver offset, den registrerer, lade som om, de er der. losLab PDF Library løser dette med en intern strømadapter, TPDFAppendSectionStream, der præsenterer et virtuelt koordinatrum for serialiseringen. Adapteren oprettes med den oprindelige fils byte-længde som sit basisoffset, rapporterer sin position og størrelse som denne basis plus hvad der hidtil er blevet tilføjet, og videresender kun de nyskrevne bytes til brugerens målstrøm
Konsekvensen er, at tilstand 1 never danner en kopi af kildedokumentet — hverken på disk eller i hukommelsen. Den naive implementering (skriv hele filen til en midlertidig buffer og skær derefter enden af) ville indebære en midlertidig kopi af hele den oprindelige PDF, hvilket for gigabyte-skala inputs er præcis den omkostning, inkrementelle opdateringer eksisterer for at undgå. Denne offset-virtualiseringsteknik er nært beslægtet med den byte-referenceskiftning, der bruges andre steder i biblioteket; artiklen om hurtig PDF-sammenføjning med byte-referenceskiftning viser den samme idé anvendt på at kombinere dokumenter, og guiden til sammenføjning og opdeling af store PDF-filer med direkte filadgang dækker den omgivende I/O-arkitektur for filer, der ikke nemt kan være i RAM
Streaming af komplette lagringer med SaveToStream
SaveToStream i losLab PDF Library driver dokument-serialiseringen direkte mod målstrømmen frem for først at rendere hele dokumentet til en midlertidig AnsiString og derefter skrive denne buffer ud i ét kald. Den ældre tilgang fungerede, men den betød, at enhver komplet lagring midlertidigt optog endnu en komplet kopi af outputtet i hukommelsen — harmløst ved 10 MB, smertefuldt ved 500 MB, og en hård grænse for multi-gigabyte-output på 32-bit processer. Direkte serialisering får spidshukommelsesforbruget til at følge dokumentets objektstrukturer i stedet for dets serialiserede længde
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;
En lektie i delingstilstand: Da AppendToFile returnerede 0
En regression på dette område er værd at genfortælle, fordi fejlmønsteret kan generaliseres. AppendToFile(FileName) tilføjer en inkrementel opdatering direkte til en eksisterende PDF på disken — det naturlige kald til en revisionshistorik-arbejdsgang på stedet: Indlæs en fil, foretag en ændring, tilføj til samme sti. I v3.71.2 begyndte netop denne sekvens at returnere 0. Årsagen lå i loaderen, ikke i skriveren: For at understøtte on-demand læsning af store dokumenter holder LoadFromFile kildefilens handle åbent i hele dokumentobjektets levetid, og dette handle blev åbnet med fmShareDenyWrite. Da AppendToFile derefter forsøgte at genåbne den samme fil til skrivning, afviste loaderens egen delingstilstand det, og API'en fejlede, før der blev skrevet en enkelt byte
Rettelsen løsnede loaderens delingstilstand til fmShareDenyNone, hvilket er sikkert netop på grund af, hvad en inkrementel tilføjelse er: Den tilføjer udelukkende bytes efter slutningen af filen og genskriver aldrig det område, som læserens langvarige handle betjener. Den generelle lektie for enhver, der wrapper dette bibliotek — eller bygger lignende streaming-loadere — er, at dovne (lazy), handle-holdende læsere og skrivere af samme fil er i konflikt, og den delingstilstand, du vælger ved åbningstidspunktet, er en API-kontrakt, ikke en implementeringsdetalje. Hvis AppendToFile nogensinde returnerer 0 i din kode, skal du først kontrollere, om noget andet i din proces still holds målfilen med en restriktiv delingstilstand
De reelle omkostninger: Når inkrementelle opdateringer er det forkerte værktøj
Inkrementelle opdateringer bytter filstørrelse for skriveeffektivitet, og denne byttehandel er ikke altid fordelagtig. Hver revision tilføjer sine ændrede objekter, mens de forældede definitioner forbliver i filen, så et dokument, der er redigeret hundredvis af gange, akkumulerer døde objekter og en lang /Prev-kæde, som enhver læser skal gennemgå. Hvad værre er, så er "slettet" indhold ikke forsvundet: Tekst, der blev fjernet i revision fem, er stadig fysisk til stede i bytes for revision fire, og kan gendannes af enhver, der afskærer filen. Redigering (redaction), sanering eller enhver fjernelse af følsomt indhold kræver derfor en komplet genskrivning — en inkrementel gem-handling af en redigering er blot et datalæk med ekstra trin
En komplet lagring er også det rigtige valg, når målet er komprimering (at presse akkumulerede inkrementer og ubrugte objekter ud), når man ændrer egenskaber for hele dokumentet som f.eks. kryptering — genkryptering rører ved enhver streng og strøm, så der er intet "inkrementelt" tilbage ved ændringen — eller når man producerer et rent leveranceobjekt, hvor redigeringshistorikken ikke bør følge med filen. En rimelig regel: Brug AppendToStream eller AppendToFile, mens et dokument er aktivt og under forandring, især når det indeholder signaturer; brug en komplet SaveToStream-genskrivning ved grænserne for livscyklussen, når dokumentet forlader dit system, eller dets historik skal flades ud
Inkrementelle opdateringer, virtuel-offset delta output og direkte-til-strøm serialisering er alle en del af standard losLab PDF Library til Delphi, C# og VB.NET; produktsiden viser hele lagrings- og tilføjelses-API-overfladen sammen med signerings- og store filfunktioner diskuteret ovenfor