Articol tehnic

Actualizări incrementale PDF în Delphi: Ghid pentru AppendToStream

Actualizările incrementale PDF permit unei aplicații Delphi să modifice un document prin adăugarea doar a obiectelor modificate, lăsând neatins fiecare octet original. losLab PDF Library implementează acest lucru prin AppendToStream, care scrie doar secțiunea incrementală definită de ISO 32000-1 §7.5.6, astfel încât o modificare a unui singur semn de carte într-un fișier de 2 GB costă kiloocteți de date de ieșire în loc de o rescriere completă. Același mecanism este motivul pentru care documentele semnate pot fi actualizate fără a le invalida semnăturile

Problema pe care o rezolvă acest lucru este una concretă. O salvare completă rescrie întregul fișier: fiecare obiect este reserializat, fiecare decalaj de referință încrucișată este recalculat, iar rezultatul nu are nicio legătură la nivel de octet cu intrarea. Pentru o factură de 40 KB acest lucru este în regulă. Pentru o arhivă scanată de 2 GB în care ați corectat doar o greșeală de tipar în titlul documentului, rescrierea a doi gigaocteți pentru a modifica douăzeci de octeți este absurdă — iar dacă fișierul conținea o semnătură digitală, rescrierea tocmai a distrus-o

De ce salvarea unui PDF distruge semnătura sa digitală?

O semnătură digitală PDF nu semnează conținutul logic al documentului; ea semnează intervale de octeți din fișierul fizic. Intrarea /ByteRange din dicționarul de semnături înregistrează exact ce porțiuni ale fișierului sunt acoperite de rezumatul criptografic (digest). Orice operațiune de salvare care reserializează acei octeți — chiar și una care produce un document identic din punct de vedere semantic — modifică rezumatul, iar fiecare validator va raporta semnătura ca fiind deteriorată. Acest lucru este prevăzut prin proiectare: semnătura atestă octeții pe care i-a văzut semnatarul, nu un model abstract de document

Actualizările incrementale sunt calea de scăpare pe care o oferă specificația PDF. Deoarece o salvare incrementală adaugă date noi după marcajul %%EOF original și nu atinge niciodată intervalele de octeți semnate, semnătura existentă continuă să fie validată pe baza octeților pe care îi acoperă. Validatoarele clasifică apoi modificările adăugate separat — o a doua semnătură, completarea unui formular, o adnotare — și decid dacă acestea sunt modificări permise. Fiecare flux de lucru cu mai multe semnături depinde de acest lucru: fiecare semnatar adaugă o secțiune incrementală peste cea anterioară. Dacă construiți linii de procesare a semnăturilor, articolul asociat despre semnarea și validarea PAdES în Delphi explică în detaliu modul în care interacționează intervalele de octeți ale semnăturii și secțiunile incrementale

Cum funcționează actualizările incrementale conform ISO 32000-1 §7.5.6

ISO 32000-1 §7.5.6 definește modelul prin trei reguli. În primul rând, conținutul original al fișierului este lăsat complet intact — niciun octet nu este mutat. În al doilea rând, obiectele modificate și cele nou create sunt adăugate după ultimul %%EOF, fiecare având același număr de obiect ca înainte (obiectele modificate primesc pur și simplu o definiție mai nouă care o eclipsează pe cea veche). În al treilea rând, se adaugă o nouă secțiune de referințe încrucișate și un trailer; intrarea /Prev a trailer-ului trimite înapoi la decalajul de octeți al secțiunii anterioare de referințe încrucișate, formând o cale pe care un cititor o parcurge de la cea mai nouă la cea mai veche pentru a rezolva fiecare obiect la cea mai recentă definiție a sa

Două proprietăți utile rezultă din această structură. Actualizările sunt ieftine în raport cu ceea ce s-a schimbat, nu cu dimensiunea documentului — costul adăugării este dimensiunea obiectelor modificate plus o mică valoare suplimentară pentru xref/trailer. De asemenea, fișierul devine propriul său istoric de versiuni: fiecare revizuire anterioară este încă prezentă fizic, astfel încât un auditor poate trunchia fișierul la orice %%EOF anterior și poate recupera exact documentul care exista în acel moment. Pentru fluxurile de lucru de conformitate care trebuie să dovedească cum arăta un document înainte de fiecare modificare, această pistă de audit integrată este adesea argumentul decisiv pentru salvările incrementale

Scrierea unei actualizări incrementale cu AppendToStream

losLab PDF Library expune rezultatul incremental prin AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, care returnează 1 în caz de succes și 0 în caz de eșec. Parametrul AppendMode selectează ceea ce ajunge în fluxul țintă. Modul 0 scrie un fișier complet: octeții sursă originali sunt copiați mai întâi în flux, apoi este adăugată secțiunea incrementală. Modul 1 scrie doar secțiunea incrementală propriu-zisă — delta — și omite complet octeții sursă. Modul 2 scrie mai întâi un prefix furnizat de apelant înregistrat prin SetAppendInputFromString, apoi adaugă secțiunea de actualizare peste acesta

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;

Modul 1 este cel interesant pentru proiectarea sistemelor. Deoarece delta este autonomă, o puteți livra independent de documentul original: puteți stoca revizuirile ca blob-uri separate în stocarea de obiecte, puteți replica doar diferențele către un site la distanță sau puteți reconstrui orice revizuire prin concatenarea fișierului de bază cu lanțul său de incrementări. Regula de reconstrucție este simpla concatenare a octeților — mai întâi fișierul original, apoi fiecare delta în ordine — deoarece aceasta este exact structura pe care §7.5.6 o prescrie pentru un fișier actualizat incremental

Cum calculează biblioteca decalajele xref fără a copia fișierul original?

Intrările de referințe încrucișate dintr-o secțiune incrementală trebuie să conțină decalaje absolute de octeți — poziții măsurate de la începutul fișierului complet, nu de la începutul delta-ului. Acest lucru creează o problemă pentru modul 1: scriitorul nu emite niciodată octeții originali, dar fiecare decalaj pe care îl înregistrează trebuie să pretindă că aceștia se află acolo. losLab PDF Library rezolvă acest lucru cu un adaptor de flux intern, TPDFAppendSectionStream, care prezintă un spațiu virtual de coordonate serializatorului. Adaptorul este creat având lungimea în octeți a fișierului original ca decalaj de bază, își raportează poziția și dimensiunea ca fiind acea bază plus tot ceea ce a fost adăugat până în prezent și redirecționează doar octeții nou scriși către fluxul țintă al apelantului

Consecința este că modul 1 nu materializează niciodată o copie a documentului sursă — nici pe disc, nici în memorie. Implementarea naivă (scrierea fișierului complet într-un buffer temporar, apoi tăierea cozii) ar presupune o copie tranzitorie a întregului PDF original, ceea ce, pentru intrări de ordinul gigaocteților, este tocmai costul pe care actualizările incrementale încearcă să îl evite. Această tehnică de virtualizare a decalajelor este strâns înrudită cu deplasarea referințelor de octeți utilizată în alte părți ale bibliotecii; articolul despre îmbinarea rapidă a PDF-urilor cu deplasarea referințelor de octeți arată aceeași idee aplicată la combinarea documentelor, iar ghidul pentru îmbinarea și divizarea PDF-urilor mari cu acces direct la fișiere acoperă arhitectura I/O aferentă pentru fișierele care nu încap confortabil în memoria RAM

Redarea fluxului salvărilor complete cu SaveToStream

Rezultatul incremental reprezintă doar jumătate din povestea redării în flux; cealaltă jumătate este ceea ce se întâmplă la o salvare completă. SaveToStream din losLab PDF Library direcționează serializatorul de documente direct către fluxul țintă, în loc să redea mai întâi întregul document într-un AnsiString intermediar și apoi să scrie acel buffer într-un singur apel. Abordarea anterioară funcționa, dar însemna că fiecare salvare completă păstra temporar o a doua copie completă a datelor de ieșire în memorie — inofensiv la 10 MB, problematic la 500 MB și o limită de netrecut pentru datele de ieșire de ordinul gigaocteților pe procese pe 32 de biți. Serializarea directă face ca memoria de vârf să urmărească structurile de obiecte ale documentului, în loc de lungimea sa serializată

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;

O lecție despre modul de partajare: când AppendToFile a returnat 0

O regresie din acest domeniu merită povestită deoarece modelul de eșec se generalizează. AppendToFile(FileName) adaugă o actualizare incrementală direct la un PDF existent pe disc — apelul firesc pentru un flux de lucru cu pistă de audit pe loc: se încarcă un fișier, se face o modificare, se adaugă la aceeași cale. În versiunea 3.71.2, această secvență exactă a început să returneze 0. Cauza principală s-a aflat în încărcător, nu în scriitor: pentru a asigura citirea la cerere a documentelor mari, LoadFromFile menține identificatorul fișierului sursă deschis pe durata de viață a obiectului document, iar acel identificator a fost deschis cu fmShareDenyWrite. Când AppendToFile a încercat apoi să redeschidă același fișier pentru scriere, propriul mod de partajare al încărcătorului a refuzat acest lucru, iar API-ul a eșuat înainte de a scrie un octet

Remedierea a relaxat modul de partajare al încărcătorului la fmShareDenyNone, ceea ce este sigur tocmai datorită naturii unei adăugări incrementale: aceasta adaugă octeți strict după sfârșitul fișierului și nu rescrie niciodată regiunea deservită de identificatorul cu durată lungă de viață al cititorului. Lecția generală pentru oricine utilizează această bibliotecă — sau construiește încărcătoare de flux similare — este că cititoarele leneșe care mențin identificatorul deschis și scriitoarele care utilizează același fișier se află în opoziție, iar modul de partajare pe care îl alegeți la deschidere este un contract API, nu un detaliu de implementare. Dacă AppendToFile returnează vreodată 0 în codul dvs., verificați mai întâi dacă altceva din procesul dvs. mai deține fișierul țintă într-un mod de partajare restrictiv

Costurile reale: când actualizările incrementale sunt instrumentul greșit

Actualizările incrementale oferă eficiență la scriere în detrimentul dimensiunii fișierului, iar acest schimb nu este întotdeauna favorabil. Fiecare revizuire își adaugă obiectele modificate în timp ce definițiile înlocuite rămân în fișier, astfel încât un document editat de sute de ori acumulează obiecte inutile și un lanț lung /Prev pe care fiecare cititor trebuie să îl parcurgă. Mai rău, conținutul „șters” nu dispare: textul eliminat în revizuirea cinci este încă prezent fizic în octeții revizuirii patru, putând fi recuperat de oricine trunchiază fișierul. Redactarea, igienizarea sau orice eliminare de conținut sensibil necesită, prin urmare, o rescriere completă — o salvare incrementală a unei redactări este o scurgere de date mascată

O salvare completă este de asemenea decizia corectă atunci când scopul este compactarea (eliminarea incrementărilor acumulate și a obiectelor neutilizate), când se modifică proprietăți la nivel de document, cum ar fi criptarea — criptarea repetată atinge fiecare șir și flux, astfel încât nu mai rămâne nimic „incremental” în acea modificare — sau când se produce un material final curat, unde istoricul de editare nu ar trebui să însoțească fișierul. O regulă rezonabilă: utilizați AppendToStream sau AppendToFile în timp ce un document este activ și se modifică, în special odată ce conține semnături; utilizați o rescriere completă SaveToStream la limitele ciclului de viață, atunci când documentul părăsește sistemul dvs. sau când istoricul acestuia trebuie aplatizat

Ambele jumătăți, protecția la nivel de scriere și calea de încărcare-reparare-salvare, fac parte din losLab PDF Library pentru Delphi, C# și VB.NET; pagina produsului prezintă întreaga structură a API-ului de salvare și adăugare, alături de caracteristicile de semnare și gestionare a fișierelor mari discutate mai sus