Teknisk artikel

Inkrementelle PDF-opdateringer i Delphi: AppendToStream

Inkrementelle PDF-opdateringer lader en Delphi-applikation ændre et dokument ved kun at tilføje de ændrede objekter og lade hver eneste oprindelige byte stå urørt. losLab PDF Library implementerer dette gennem AppendToStream, som kun skriver den inkrementelle sektion, der er defineret i ISO 32000-1 §7.5.6, så en redigering af ét bogmærke i en fil på 2 GB koster kilobytes af output i stedet for en fuld omskrivning. Den samme mekanisme er grunden til, at signerede dokumenter kan opdateres uden at ugyldiggøre deres signaturer

Den smerte, dette løser, er konkret. En fuld lagring omskriver hele filen: hvert objekt serialiseres igen, hver krydsreference-offset genberegnes, og outputtet har ingen relation på byteniveau til inputtet. For en faktura på 40 KB er det fint. For et scannet arkiv på 2 GB, hvor du kun rettede en tastefejl i dokumenttitlen, er det absurd at omskrive to gigabyte for at ændre tyve bytes — og hvis filen bar en digital signatur, har omskrivningen netop ødelagt den

Hvorfor bryder lagring af en PDF dens digitale signatur?

En digital PDF-signatur signerer ikke dokumentets logiske indhold; den signerer byteområder i den fysiske fil. Posten /ByteRange i signatur-dictionaryet registrerer præcis, hvilke spænd af filen det kryptografiske digest dækker. Enhver lagringsoperation, der serialiserer de bytes igen — selv én, der producerer et semantisk identisk dokument — ændrer digestet, og enhver validator vil rapportere signaturen som brudt. Det er med vilje: signaturen attesterer de bytes, underskriveren så, ikke en eller anden abstrakt dokumentmodel

Inkrementelle opdateringer er den nødudgang, PDF-specifikationen stiller til rådighed. Fordi en inkrementel lagring tilføjer nye data efter den oprindelige %%EOF og aldrig rører de signerede byteområder, bliver den eksisterende signatur ved med at validere mod de bytes, den dækker. Validatorer klassificerer derefter de tilføjede ændringer separat — en anden signatur, en formularudfyldning, en annotation — og afgør, 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 signeringspipelines, dækker ledsageartiklen om PAdES-signering og -validering i Delphi i detaljer, hvordan signatur-byteområder og inkrementelle sektioner spiller sammen

PDF Library for Delphi: Byteområde-diagram, der viser, hvorfor en fuld PDF-lagring ugyldiggør digitale signaturer, mens inkrementelle AppendToStream-opdateringer bevarer dem
Fordi digestet dækker faste byteområder, blander en fuld lagring dem og bryder valideringen, mens en inkrementel opdatering tilføjer uden for den signerede region, og hver kædet signatur overlever

Sådan fungerer inkrementelle opdateringer under ISO 32000-1 §7.5.6

ISO 32000-1 §7.5.6 definerer modellen i tre regler. For det første efterlades det oprindelige filindhold fuldstændig intakt — ikke én byte flytter sig. For det andet tilføjes ændrede og nyoprettede objekter efter den sidste %%EOF, hver med det samme objektnummer, som den 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 for den forrige krydsreferencesektion og danner en kæde, som en læser gennemgår fra nyeste til ældste for at opløse hvert objekt til dets seneste definition

To nyttige egenskaber følger af denne struktur. Opdateringer er billige i forhold til det, der blev ændret, ikke til dokumentets størrelse — tilføjelsesomkostningen er størrelsen af de ændrede objekter plus et lille xref/trailer-overhead. Og filen bliver sin egen versionshistorik: hver tidligere revision er stadig fysisk til stede, så en revisor kan afkorte filen ved enhver tidligere %%EOF og genskabe præcis det dokument, der eksisterede på det tidspunkt. For compliance-arbejdsgange, der skal bevise, hvordan et dokument så ud før hver ændring, er dette indbyggede revisionsspor ofte det afgørende argument for inkrementel lagring

Skrivning af en inkrementel opdatering med AppendToStream

losLab PDF Library eksponerer inkrementelt output gennem 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. Mode 0 skriver en komplet fil: de oprindelige kildebytes kopieres først til strømmen, og derefter tilføjes den inkrementelle sektion. Mode 1 skriver kun selve den inkrementelle sektion — deltaet — og springer kildebytes helt over. Mode 2 skriver først et præfiks leveret af kalderen og registreret via SetAppendInputFromString og tilføjer derefter opdateringssektionen oven på det

Sammenligning af AppendToStream-modes 0, 1 og 2, der skriver kildebytes, kalderens præfiks og den inkrementelle sektion til en Delphi-strøm
AppendMode vælger, hvad strømmen modtager, fra en komplet kopi over et præfiks leveret af kalderen plus delta, hvor Mode 1 udsender den portable rene inkrementelle sektion
var
  Doc: TPDFlib;
  Delta: TMemoryStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('contract.pdf', '') <= 0 then
      Exit;

    // Lille redigering: den slags ændring, der ikke bør
    // udløse en omskrivning af hele filen
    Doc.SetInformation(3, 'Amended 2026-07-04');  // nøgle 3 = /Subject

    Delta := TMemoryStream.Create;
    try
      // AppendMode = 1: skriv kun den inkrementelle sektion.
      // Oprindelige bytes + Delta = en komplet, gyldig PDF.
      if Doc.AppendToStream(1, Delta) = 1 then
        Delta.SaveToFile('contract.delta.bin');
    finally
      Delta.Free;
    end;
  finally
    Doc.Free;
  end;
end;

Mode 1 er den interessante for systemdesign. Fordi deltaet er selvstændigt, kan du sende det uafhængigt af originalen: gem revisioner som separate blobs i objektlager, replikér kun deltaer til et fjernt site, eller rekonstruér enhver revision ved at sammenkæde basisfilen med dens kæde af inkrementer. Rekonstruktionsreglen er ren bytesammenkædning — oprindelig fil først, derefter hvert delta i rækkefølge — fordi det er præcis det layout, §7.5.6 foreskriver for en inkrementelt opdateret fil

Hvordan beregner biblioteket xref-offsets uden at kopiere den oprindelige fil?

Krydsreferenceposterne inde 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 puslespil for mode 1: writeren udsender aldrig de oprindelige bytes, og alligevel skal hvert 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 serializeren. Adapteren oprettes med den oprindelige fils bytelængde som basis-offset, rapporterer sin position og størrelse som den basis plus alt, hvad der hidtil er tilføjet, og videresender kun de nyskrevne bytes til kalderens målstrøm

Konsekvensen er, at mode 1 aldrig materialiserer en kopi af kildedokumentet — hverken på disk eller i hukommelsen. Den naive implementering (skriv den fulde fil til en midlertidig buffer, og skær derefter halen af) ville bære en forbigående kopi af hele den oprindelige PDF, hvilket for input i gigabyte-skala er præcis den omkostning, inkrementelle opdateringer findes for at undgå. Denne offset-virtualiseringsteknik er en nær slægtning af den bytereference-forskydning, der bruges andre steder i biblioteket; artiklen om hurtig PDF-sammenfletning med bytereference-forskydning viser den samme idé anvendt på kombination af dokumenter, og guiden til sammenfletning og opdeling af store PDF-filer med direkte filadgang dækker den omgivende I/O-arkitektur for filer, der ikke passer komfortabelt i RAM

Streaming af fulde lagringer med SaveToStream

Inkrementelt output er halvdelen af streaming-historien; den anden halvdel er, hvad der sker ved en fuld lagring. SaveToStream i losLab PDF Library driver dokumentserializeren direkte mod målstrømmen i stedet for først at rendere hele dokumentet til en mellemliggende AnsiString og derefter skrive den buffer ud i ét kald. Den ældre tilgang virkede, men den betød, at hver fuld lagring forbigående holdt en anden komplet kopi af outputtet i hukommelsen — harmløst ved 10 MB, smertefuldt ved 500 MB og en hård mur for output på flere gigabyte i 32-bit-processer. Direkte serialisering får det maksimale hukommelsesforbrug 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;

    // ... redigeringer, der retfærdiggør en fuld omskrivning ...

    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 om share-mode: da AppendToFile returnerede 0

Én regression på dette område er værd at genfortælle, fordi fejlmønstret kan generaliseres. AppendToFile(FileName) tilføjer en inkrementel opdatering direkte til en eksisterende PDF på disken — det naturlige kald for en arbejdsgang med revisionsspor på stedet: indlæs en fil, foretag en ændring, tilføj til den samme sti. I v3.71.2 begyndte præcis den sekvens at returnere 0. Rodårsagen lå i loaderen, ikke i writeren: for at understøtte on-demand-læsning af store dokumenter holder LoadFromFile kildefilens handle åbent i hele dokumentobjektets levetid, og det handle var åbnet med fmShareDenyWrite. Da AppendToFile derefter forsøgte at genåbne den samme fil til skrivning, nægtede loaderens egen share-mode det, og API'en fejlede, før den havde skrevet en eneste byte

Rettelsen lempede loaderens share-mode til fmShareDenyNone, hvilket er sikkert netop på grund af, hvad en inkrementel tilføjelse er: den tilføjer bytes strengt efter filens slutning og omskriver aldrig den region, læserens langlivede handle betjener. Den generelle lektie for enhver, der wrapper dette bibliotek — eller bygger lignende streaming-loadere — er, at dovne, handle-holdende læsere og skrivere til samme fil er i spænding, og at den share-mode, du vælger ved åbning, er en API-kontrakt, ikke en implementeringsdetalje. Hvis AppendToFile nogensinde returnerer 0 i din kode, så undersøg først, om noget andet i din proces stadig holder målfilen med en restriktiv share-mode

De ærlige omkostninger: når inkrementelle opdateringer er det forkerte værktøj

Inkrementelle opdateringer bytter filstørrelse for skriveeffektivitet, og byttet er ikke altid gunstigt. Hver revision tilføjer sine ændrede objekter, mens de overflødiggjorte definitioner bliver i filen, så et dokument, der er redigeret hundredvis af gange, ophober døde objekter og en lang /Prev-kæde, som hver læser skal gennemgå. Værre endnu er "slettet" indhold ikke væk: tekst fjernet i revision fem er stadig fysisk til stede i bytes fra revision fire og kan genskabes af enhver, der afkorter filen. Redigering, sanering eller enhver fjernelse af følsomt indhold kræver derfor en fuld omskrivning — en inkrementel lagring af en redigering er et datalæk med ekstra trin

En fuld lagring er også det rigtige valg, når målet er komprimering (at presse ophobede inkrementer og ubrugte objekter ud), når dokumentomfattende egenskaber som kryptering ændres — genkryptering rører hver streng og stream, så der er intet "inkrementelt" tilbage ved ændringen — eller når der produceres en ren leverance, hvor redigeringshistorikken ikke bør rejse med filen. En fornuftig regel: brug AppendToStream eller AppendToFile, mens et dokument er levende og under forandring, især når det bærer signaturer; brug en fuld SaveToStream-omskrivning ved livscyklusgrænser, når dokumentet forlader dit system, eller dets historik skal udjævnes

PDF Library for Delphi beslutningsguide til valg mellem inkrementelle AppendToStream-lagringer og fulde SaveToStream-omskrivninger gennem et dokuments livscyklus
Tilføj, mens et dokument er levende og signeret, og skift til en fuld omskrivning, hver gang bytes skal forsvinde, historik skal udjævnes, eller kryptering ændres

Inkrementelle opdateringer, delta-output med virtuelle offsets og direkte serialisering til strøm er alle en del af det standardmæssige losLab PDF Library til Delphi, C# og VB.NET; produktsiden viser den fulde API-overflade for lagring og tilføjelse sammen med de signerings- og storfilsfunktioner, der er omtalt ovenfor