Inkrementella PDF-uppdateringar gör att en Delphi-applikation kan ändra ett dokument genom att endast lägga till de ändrade objekten, och lämna varje ursprunglig byte orörd. losLab PDF Library implementerar detta genom AppendToStream, som skriver just den inkrementella sektionen definierad av ISO 32000-1 §7.5.6, så att en redigering av ett bokmärke i en fil på 2 GB kostar kilobyte av utdata istället för en fullständig omskrivning. Samma mekanism är anledningen till att signerade dokument kan uppdateras utan att deras signaturer ogiltigförklaras
Smärtan som detta löser är konkret. En fullständig sparning skriver om hela filen: varje objekt serialiseras om, varje korsreferensförskjutning beräknas om och utdata har inget förhållande på bytenivå till indata. För en faktura på 40 KB är det bra. För ett skannat arkiv på 2 GB där du bara åtgärdade ett stavfel i dokumenttiteln är det absurt att skriva om två gigabyte för att ändra tjugo byte — och om filen bar på en digital signatur förstörde omskrivningen den precis
Varför bryter sparning av en PDF dess digitala signatur?
En digital PDF-signatur signerar inte dokumentets logiska innehåll; den signerar byte-intervall för den fysiska filen. Posten /ByteRange i signaturordboken registrerar exakt vilka delar av filen som den kryptografiska signaturen täcker. Varje sparningsåtgärd som serialiserar om dessa byte — även en som producerar ett semantiskt identiskt dokument — ändrar signaturen, och varje validerare kommer att rapportera signaturen som trasig. Detta är avsiktligt: signaturen intygar de byte som undertecknaren såg, inte någon abstrakt dokumentmodell
Inkrementella uppdateringar är den nödutgång som PDF-specifikationen tillhandahåller. Eftersom en inkrementell sparning lägger till nya data efter den ursprungliga %%EOF och aldrig rör de signerade byte-intervallen, fortsätter den befintliga signaturen att valideras mot de byte den täcker. Validerare klassificerar sedan de tillagda ändringarna separat — en andra signatur, en formulärifyllnad, en anteckning — och beslutar om de är tillåtna ändringar. Varje arbetsflöde med flera signaturer beror på detta: varje undertecknare lägger till en inkrementell sektion ovanpå den föregående. Om du bygger signeringspipelines täcker följeslagsartikeln om PAdES-signering och validering i Delphi hur signaturens byte-intervall och inkrementella sektioner interagerar i detalj
Hur inkrementella uppdateringar fungerar under ISO 32000-1 §7.5.6
ISO 32000-1 §7.5.6 definierar modellen i tre regler. För det första lämnas den ursprungliga filens innehåll helt intakt — inte en enda byte flyttas. För det andra läggs ändrade och nyskapade objekt till efter den sista %%EOF, var och en med samma objektnummer som det hade tidigare (ändrade objekt får helt enkelt en nyare definition som skuggar den gamla). För det tredje läggs en ny korsreferenssektion och trailer till; trailerns /Prev-post pekar tillbaka till byte-förskjutningen för den föregående korsreferenssektionen, vilket bildar en kedja som en läsare går igenom från nyaste till äldsta för att matcha varje objekt till dess senaste definition
Två användbara egenskaper följer av denna struktur. Uppdateringar är billiga i förhållande till vad som ändrats, inte till dokumentstorleken — kostnaden för tillägget är storleken på de ändrade objekten plus en liten overhead för xref/trailer. Och filen blir sin egen versionshistorik: varje tidigare revision finns fortfarande fysiskt kvar, så en granskare kan trunkera filen vid vilken tidigare %%EOF som helst och återställa exakt det dokument som fanns vid den tidpunkten. För regelefterlevnadsarbetsflöden som måste bevisa hur ett dokument såg ut före varje ändring är detta inbyggda granskningsspår ofta det avgörande argumentet för inkrementella sparningar
Skriva en inkrementell uppdatering med AppendToStream
losLab PDF Library exponerar inkrementell utmatning genom AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, vilket returnerar 1 vid framgång och 0 vid misslyckande. Parametern AppendMode väljer vad som hamnar i målströmmen. Läge 0 skriver en komplett fil: de ursprungliga källbytena kopieras till strömmen först, sedan läggs den inkrementella sektionen till. Läge 1 skriver endast själva den inkrementella sektionen — deltat — och hoppar över källbytena helt. Läge 2 skriver först ett användardefinierat prefix som registrerats via SetAppendInputFromString och lägger sedan till uppdateringssektionen ovanpå det
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Liten redigering: den typ av ändring som inte borde
// utlösa en omskrivning av hela filen
Doc.SetInformation(3, 'Amended 2026-07-04'); // key 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: skriv endast den inkrementella sektionen.
// Ursprungliga byte + Delta = en komplett, giltig PDF.
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
Läge 1 är det intressanta för systemdesign. Eftersom deltat är självständigt kan du skicka det oberoende av originalet: lagra revisioner som separata blobbar i objektlagring, replikera endast deltan till en fjärrplats, eller rekonstruera vilken revision som helst genom att sammanfoga basfilen med dess kedja av inkrement. Rekonstruktionsregeln är vanlig byte-sammanfogning — originalfilen först, sedan varje delta i ordning — eftersom det är exakt den layout som §7.5.6 föreskriver för en inkrementellt uppdaterad fil
Hur beräknar biblioteket xref-förskjutningar utan att kopiera originalfilen?
Korsreferensposterna inuti en inkrementell sektion måste innehålla absoluta byte-förskjutningar — positioner mätta från början av den kompletta filen, inte från början av deltat. Det skapar ett pussel för läge 1: skrivaren matar aldrig ut originalbytena, men ändå måste varje förskjutning den registrerar låtsas att de finns där. losLab PDF Library löser detta med en intern strömadapter, TPDFAppendSectionStream, som presenterar en virtuell koordinatrymd för serialiseraren. Adaptern skapas med originalfilens byte-längd som sin basförskjutning, rapporterar sin position och storlek som den basen plus vad som har lagts till hittills, och vidarebefordrar endast de nyskrivna bytena till anroparens målström
Konsekvensen är att läge 1 aldrig skapar en kopia av källdokumentet — varken på disk eller i minnet. Den naiva implementeringen (skriv hela filen till en temporär buffert och klipp sedan av slutet) skulle innebära en tillfällig kopia av hela den ursprungliga PDF-filen, vilket för indata på gigabyte-nivå är exakt den kostnad som inkrementella uppdateringar finns till för att undvika. Denna förskjutningsvirtualiseringsteknik är en nära släkting till byte-referensskiftning som används på andra ställen i biblioteket; artikeln om snabb PDF-sammanslagning med byte-referensskiftning visar samma idé tillämpad på att kombinera dokument, och guiden till sammanslagning och delning av stora PDF-filer med direkt filåtkomst täcker den omgivande I/O-arkitekturen för filer som inte får plats bekvämt i RAM
Strömma fullständiga sparningar med SaveToStream
Inkrementell utmatning är halva strömningshistorien; den andra halvan är vad som händer vid en fullständig sparning. SaveToStream i losLab PDF Library driver dokumentserialiseraren direkt mot målströmmen, snarare än att först rendera hela dokumentet till en mellanliggande AnsiString och sedan skriva ut den bufferten i ett enda anrop. Det äldre tillvägagångssättet fungerade, men det innebar att varje fullständig sparning tillfälligt höll en andra komplett kopia av utdata i minnet — ofarligt vid 10 MB, smärtsamt vid 500 MB och en hård vägg för utdata på flera gigabyte på 32-bitars processer. Direkt serialisering gör att minnestopparna spårar dokumentets objektstrukturer istället för dess serialiserade längd
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... redigeringar som motiverar en fullständig 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 läxa om delningsläge: när AppendToFile returnerade 0
En regression inom detta område är värd att berätta igen eftersom felmönstret generaliserar. AppendToFile(FileName) lägger till en inkrementell uppdatering direkt till en befintlig PDF på disk — det naturliga anropet för ett inbyggt granskningskedjearbetsflöde: läs in en fil, gör en ändring, lägg till på samma sökväg. I v3.71.2 började exakt den sekvensen returnera 0. Rotorsaken låg i inläsaren, inte skrivaren: för att stödja läsning av stora dokument på begäran håller LoadFromFile källfilshandtaget öppet under dokumentobjektets livstid, och det handtaget öppnades med fmShareDenyWrite. När AppendToFile sedan försökte öppna samma fil igen för skrivning, nekade inläsarens eget delningsläge det, och API:et misslyckades innan det skrev en enda byte
Korrigeringen lättade på inläsarens delningsläge till fmShareDenyNone, vilket är säkert just på grund av vad ett inkrementellt tillägg är: det lägger till byte strikt efter slutet av filen och skriver aldrig om det område som läsarens långlivade handtag betjänar. Den allmänna läxan för alla som paketerar det här biblioteket — eller bygger liknande strömmande inläsare — är att lata, handtagshållande läsare och skrivare till samma fil befinner sig i spänning, och delningsläget du väljer vid öppningstillfället är ett API-kontrakt, inte en implementeringsdetalj. Om AppendToFile någonsin returnerar 0 i din kod, kontrollera först om något annat i din process still håller målfilen med ett restriktivt delningsläge
De ärliga kostnaderna: när inkrementella uppdateringar är fel verktyg
Inkrementella uppdateringar byter filstorlek mot skrivprestanda, och bytet är inte alltid fördelaktigt. Varje revision lägger till sina ändrade objekt medan de ersatta definitionerna finns kvar i filen, så ett dokument som redigerats hundratals gånger samlar på sig döda objekt och en lång /Prev-kedja som varje läsare måste gå igenom. Värre är att ”borttaget” innehåll inte är borta: text som tagits bort i revision fem finns fortfarande fysiskt kvar i byten för revision fyra, och kan återställas av vem som helst som trunkerar filen. Redigering (redaction), sanering eller borttagning av känsligt innehåll kräver därför en fullständig omskrivning — en inkrementell sparning av en redigering är en dataläcka med extra steg
En fullständig sparning är också rätt beslut när målet är komprimering (att klämma ut ackumulerade inkrement och oanvända objekt), vid ändring av dokumentövergripande egenskaper såsom kryptering — att kryptera om rör varje sträng och ström, så det finns inget ”inkrementellt” kvar med den ändringen — eller vid produktion av en ren leveransvara där redigeringshistoriken inte ska följa med filen. En rimlig regel: använd AppendToStream eller AppendToFile medan ett dokument är aktivt och ändras, särskilt när det bär på signaturer; använd en fullständig SaveToStream-omskrivning vid livscykelgränser, när dokumentet lämnar ditt system eller dess historik måste plattas till
Inkrementella uppdateringar, virtuell-offset deltametod och serialisering direkt till ström är alla en del av standarden losLab PDF Library för Delphi, C# och VB.NET; produktsidan listar hela sparnings- och tilläggs-API-ytan tillsammans med signerings- och storfilsfunktionerna som diskuterats ovan