Technisch artikel

PDF incrementele updates in Delphi: AppendToStream-handleiding

Met incrementele PDF-updates kan een Delphi-applicatie een document wijzigen door alleen de gewijzigde objecten toe te voegen, waardoor elke originele byte onaangetast blijft. losLab PDF Library implementeert dit via AppendToStream, die alleen de incrementele sectie schrijft die is gedefinieerd door ISO 32000-1 §7.5.6, zodat een bewerking van één bladwijzer aan een bestand van 2 GB kilobytes aan uitvoer kost in plaats van een volledige herschrijving. Hetzelfde mechanisme is de reden waarom ondertekende documenten kunnen worden bijgewerkt zonder dat hun handtekeningen ongeldig worden

Het probleem dat hiermee wordt opgelost is concreet. Een volledige opslag herschrijft het hele bestand: elk object wordt opnieuw geserialiseerd, elke kruisverwijzingsverschuiving wordt opnieuw berekend en de uitvoer heeft op byte-niveau geen relatie met de invoer. Voor een factuur van 40 KB is dat prima. Voor een gescand archief van 2 GB waarin u alleen een typfout in de documenttitel hebt gecorrigeerd, is het herschrijven van twee gigabyte om twintig bytes te wijzigen absurd — en als het bestand een digitale handtekening bevatte, heeft de herschrijving deze zojuist vernietigd

Waarom verbreekt het opslaan van een PDF de digitale handtekening?

Een digitale PDF-handtekening ondertekent niet de logische inhoud van het document; it ondertekent byte-bereiken van het fysieke bestand. De vermelding /ByteRange in het handtekeningwoordenboek legt exact vast welke delen van het bestand de cryptografische hash dekt. Elke opslagbewerking die die bytes opnieuw serialiseert — zelfs een bewerking die een semantisch identiek document oplevert — verandert de hash, en elke validator zal de handtekening als verbroken rapporteren. Dit is inherent aan het ontwerp: de handtekening getuigt van de bytes die de ondertekenaar heeft gezien, niet van een abstract documentmodel

Incrementele updates zijn de ontsnappingsroute die de PDF-specificatie biedt. Omdat een incrementele opslag nieuwe gegevens toevoegt na de originele %%EOF en nooit de ondertekende byte-bereiken raakt, blijft de bestaande handtekening geldig ten opzichte van de bytes die deze dekt. Validators classificeren de toegevoegde wijzigingen vervolgens afzonderlijk — een tweede handtekening, een ingevuld formulier, een annotatie — en beslissen of dit toegestane wijzigingen zijn. Elke workflow met meerdere handtekeningen is hier afhankelijk van: elke ondertekenaar voegt een incrementele sectie toe bovenop de vorige. Als u pijplijnen voor ondertekening bouwt, behandelt het bijbehorende artikel over PAdES-ondertekening en -validatie in Delphi in detail hoe byte-bereiken van handtekeningen en incrementele secties op elkaar inwerken

Hoe incrementele updates werken onder ISO 32000-1 §7.5.6

ISO 32000-1 §7.5.6 definieert het model in drie regels. Ten eerste blijft de originele bestandsinhoud volledig intact — er verschuift geen enkele byte. Ten tweede worden gewijzigde en nieuw gemaakte objecten toegevoegd na de laatste %%EOF, elk met hetzelfde objectnummer dat het voorheen had (gewijzigde objecten krijgen simpelweg een nieuwere definitie die de oude overschaduwt). Ten derde worden er een nieuwe kruisverwijzingssectie (xref) en trailer toegevoegd; de vermelding /Prev in de trailer verwijst terug naar de byte-offset van de vorige kruisverwijzingssectie, waardoor een keten wordt gevormd die een lezer van nieuw naar oud doorloopt om elk object naar zijn meest recente definitie te herleiden

Twee nuttige eigenschappen vloeien voort uit deze structuur. Updates zijn goedkoop in verhouding tot wat er is gewijzigd, niet tot de bestandsgrootte — de kosten voor het toevoegen zijn de grootte van de gewijzigde objecten plus een kleine overhead voor de xref/trailer. En het bestand wordt zijn eigen versiegeschiedenis: elke eerdere revisie is nog fysiek aanwezig, dus een auditor kan het bestand afkappen bij elke eerdere %%EOF en exact het document herstellen dat op dat moment bestond. Voor compliance-workflows die moeten bewijzen hoe een document er voor elke wijziging uitzag, is dit ingebouwde audittrail vaak het doorslaggevende argument voor incrementele opslag

Een incrementele update schrijven met AppendToStream

losLab PDF Library stelt incrementele uitvoer beschikbaar via AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, die 1 retourneert bij succes en 0 bij mislukking. De parameter AppendMode selecteert wat er in de doelstream terechtkomt. Modus 0 schrijft een compleet bestand: de originele bronbytes worden eerst naar de stream gekopieerd, waarna de incrementele sectie wordt toegevoegd. Modus 1 schrijft alleen de incrementele sectie zelf — de delta — en slaat de bronbytes volledig over. Modus 2 schrijft eerst een door de aanroeper geleverde prefix die is geregistreerd via SetAppendInputFromString, en voegt vervolgens de updatesectie daarbovenop toe

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;

Modus 1 is de meest interessante voor systeemontwerp. Omdat de delta op zichzelf staat, kunt u deze onafhankelijk van het origineel verzenden: bewaar revisies als afzonderlijke blobs in objectopslag, repliceer alleen delta's naar een externe locatie, of reconstrueer elke revisie door het basisbestand samen te voegen met zijn keten van toevoegingen. De reconstructieregel is eenvoudige byteconcatenatie — eerst het originele bestand, daarna elke delta op volgorde — omdat dat precies de lay-out is die §7.5.6 voorschrijft voor een incrementeel bijgewerkt bestand

Hoe berekent de bibliotheek xref-offsets zonder het originele bestand te kopiëren?

De kruisverwijzingsitems in een incrementele sectie moeten absolute byte-offsets bevatten — posities gemeten vanaf het begin van het complete bestand, niet vanaf het begin van de delta. Dat vormt een puzzel voor modus 1: de schrijver verzendt de originele bytes nooit, maar elke offset die hij registreert moet doen alsof ze er wel zijn. losLab PDF Library lost dit op met een interne streamadapter, TPDFAppendSectionStream, die een virtuele coördinatenruimte aan de serialisator presenteert. De adapter wordt gemaakt met de byte-lengte van het originele bestand als basisoffset, rapporteert zijn positie en grootte als die basis plus alles wat tot nu toe is toegevoegd, en stuurt alleen de nieuw geschreven bytes door naar de doelstream van de aanroeper

Het gevolg is dat modus 1 nooit een kopie van het brondocument materialiseert — niet op schijf en niet in het geheugen. De naïeve implementatie (het volledige bestand naar een tijdelijke buffer schrijven en vervolgens de staart eraf snijden) zou een tijdelijke kopie van de hele originele PDF met zich meebrengen, wat voor invoer op gigabyteschaal precies de kosten zijn die incrementele updates proberen te vermijden. Deze offset-virtualisatietechniek is nauw verwant aan het verschuiven van byte-referenties dat elders in de bibliotheek wordt gebruikt; het artikel over snelle PDF-samenvoeging met verschuiving van byte-referenties laat zien dat hetzelfde idee wordt toegepast op het combineren van documenten, en de handleiding voor het samenvoegen en splitsen van grote PDF's met directe bestandstoegang behandelt de omliggende I/O-architectuur voor bestanden die niet comfortabel in het RAM-geheugen passen

Volledige opslag streamen met SaveToStream

SaveToStream in losLab PDF Library stuurt de documentserialisator rechtstreeks aan tegen de doelstream, in plaats van eerst het hele document te renderen in een tijdelijke AnsiString en die buffer vervolgens in één aanroep uit te schrijven. De oudere aanpak werkte, maar betekende dat elke volledige opslag tijdelijk een tweede complete kopie van de uitvoer in het geheugen hield — onschadelijk bij 10 MB, pijnlijk bij 500 MB en een harde barrière voor uitvoer van meerdere gigabytes op 32-bit processen. Directe serialisatie zorgt ervoor dat het piekgeheugengebruik de objectstructuren van het document volgt in plaats van de geserialiseerde lengte

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;

Een les over de deelmodus: toen AppendToFile 0 retourneerde

Eén regressie op dit gebied is het hervertellen waard omdat het foutpatroon zich generaliseert. AppendToFile(FileName) voegt een incrementele update rechtstreeks toe aan een bestaande PDF op schijf — de natuurlijke aanroep voor een in-place audittrail-workflow: laad een bestand, breng een wijziging aan, voeg toe aan hetzelfde pad. In v3.71.2 begon exact die volgorde 0 te retourneren. De hoofdoorzaak lag in de lader, niet in de schrijver: om het on-demand lezen van grote documenten te ondersteunen, houdt LoadFromFile de bestandsingang van de bron geopend gedurende de levensduur van het documentobject, and die ingang werd geopend met fmShareDenyWrite. Toen AppendToFile vervolgens probeerde hetzelfde bestand opnieuw te openen om te schrijven, weigerde de eigen deelmodus (share mode) van de lader dit, en de API mislukte voordat er een byte was geschreven

De oplossing versoepelde de deelmodus van de lader naar fmShareDenyNone, wat veilig is juist vanwege wat een incrementele toevoeging is: het voegt bytes strikt toe na het einde van het bestand en herschrijft nooit de regio die de langdurige ingang van de lezer bedient. De algemene les voor iedereen die deze bibliotheek inkapselt — of soortgelijke streaming-laders bouwt — is dat luie, ingangshoudende lezers en schrijvers naar hetzelfde bestand op gespannen voet met elkaar staan, en dat de deelmodus die u kiest bij het openen een API-contract is, geen implementatiedetail. Als AppendToFile ooit 0 retourneert in uw code, controleer dan eerst of iets anders in uw proces het doelbestand nog steeds vasthoudt met een beperkende deelmodus

De eerlijke kosten: wanneer incrementele updates het verkeerde hulpmiddel zijn

Incrementele updates ruilen bestandsgrootte in voor schrijfefficiëntie, en die ruil is niet altijd gunstig. Elke revisie voegt zijn gewijzigde objecten toe terwijl de vervangen definities in het bestand blijven, zodat een document dat honderden keren is bewerkt dode objecten en een lange /Prev-keten verzamelt die elke lezer moet doorlopen. Erger nog, "verwijderde" inhoud is niet weg: tekst die in revisie vijf is verwijderd, is nog steeds fysiek aanwezig in de bytes van revisie vier, en kan door iedereen worden hersteld die het bestand afkapt. Redactie, sanering of elke verwijdering van gevoelige inhoud vereist daarom een volledige herschrijving — een incrementele opslag van een redactie is een datalek met extra stappen

Een volledige opslag is ook de juiste keuze wanneer het doel compactie is (het verwijderen van geaccumuleerde toevoegingen en ongebruikte objecten), bij het wijzigen van documentbrede eigenschappen zoals versleuteling — opnieuw versleutelen raakt elke string en stream, dus er is niets "incrementeels" meer aan de wijziging — of bij het produceren van een schoon opleverbestand waarvan de bewerkingsgeschiedenis niet met het bestand mee moet reizen. Een redelijke regel: gebruik AppendToStream of AppendToFile zolang een document actief is en verandert, vooral als het handtekeningen bevat; gebruik een volledige SaveToStream-herschrijving bij grenzen van de levenscyclus, wanneer het document uw systeem verlaat of de geschiedenis ervan moet worden afgevlakt

Incrementele updates, delta-uitvoer met virtuele offset en serialisatie rechtstreeks naar de stream maken allemaal deel uit van de standaard losLab PDF Library voor Delphi, C# en VB.NET; de productpagina toont het volledige API-oppervlak voor opslaan en toevoegen, naast de functies voor ondertekening en grote bestanden die hierboven zijn besproken