HotPDF Component applica uno stream FDF /Differences da Delphi attraverso THPDFHeadlessDocument.SaveFDFDifferencesToFile: il metodo controlla che il PDF caricato combaci ancora con una revisione base che identificate per lunghezza in byte esatta e SHA-256, accoda i byte della revisione cumulativa esattamente una volta, passa il risultato attraverso permessi di cifratura e ammissione DocMDP/FieldMDP, e lo pubblica atomicamente. Ripetere la stessa submission è un no-op byte per byte
Il flusso di lavoro per cui esiste è poco appariscente e comune. Mandate fuori un contratto o un modulo di ispezione come PDF, qualcuno lo compila e lo firma nel proprio viewer, e ciò che torna non è il PDF ma un file FDF. I valori dei campi viaggiano in /Fields, ma la revisione firmata stessa, i byte che la firma CMS copre davvero, viaggia in /Differences. Sbagliate il merge e o rompete la firma o, peggio, accettate valori di campo che nessuna firma ha mai coperto
Cosa trasporta davvero uno stream FDF /Differences?
Uno stream FDF /Differences trasporta byte grezzi di aggiornamento incrementale per il PDF target, non una storia a livello FDF. ISO 32000-1 §12.7.7.3, Tabella 243 definisce la voce Differences del dizionario FDF come le modifiche incrementali al PDF sottostante fatte da quando è stato aperto, nella stessa forma di un aggiornamento incrementale PDF ai sensi del §7.5.6: oggetti nuovi e sostituiti, una sezione di riferimenti incrociati e un trailer il cui /Prev punta indietro nel file base
Il contenitore FDF stesso è l'opposto. Il §12.7.7.1 vieta gli aggiornamenti incrementali ai file FDF, quindi un FDF ha un solo trailer, solo oggetti di generazione 0 e numeri di oggetto unici. HotPDF Component impone esattamente questo nel proprio parser FDF e non lo rilassa mai per facilitare /Differences. I dati incrementali vivono dentro un solo stream di un contenitore rigorosamente non incrementale, e tenere separati quei due livelli è metà del lavoro
Due proprietà dello stream plasmano ogni decisione di design a valle:
- Lo stream è cumulativo: contiene ogni byte accodato da quando il PDF target è stato aperto, quindi una seconda submission dalla stessa sessione ripete la revisione che avete già committato e aggiunge la nuova dietro di essa
- Le voci esterne
/Fieldse/Annotsdello stesso FDF non hanno garanzia di combaciare con/Differences, quindi i valori di campo che potete leggere facilmente non sono i dati che la firma copre
Quel secondo punto merita franchezza. Se il vostro codice di import legge /V da /Fields e tratta il risultato come firmato perché una firma è arrivata nello stesso file, avete costruito un canale di falsificazione. HotPDF Component tiene i percorsi separati di proposito: l'importatore di annotazioni FDF rifiuta /Differences in toto, e solo il trasporto Differences dedicato lo tocca
Legare l'append a una revisione base esplicita
SaveFDFDifferencesToFile si rifiuta di indovinare a quale revisione appartiene l'FDF. Il chiamante passa BaseBytes, la lunghezza esatta della revisione PDF completa da cui il modulo è stato aperto, e BaseSHA256, il suo SHA-256 esadecimale di 64 cifre. Il metodo ricalcola l'hash di quel prefisso del documento caricato, rifiuta una discrepanza con "The loaded PDF does not match the explicitly selected base", e controlla che il prefisso termini su un vero endpoint di revisione il cui startxref punta ai suoi dati reali di riferimenti incrociati
La regola pratica è registrare entrambi i numeri quando mandate fuori il modulo, non quando l'FDF torna. Un piccolo helper sulle routine SHA256Init / SHA256Update / SHA256Final in HPDFCrypt basta:
uses
SysUtils, Classes, HPDFCrypt;
function PrefixSHA256Hex(const FileName: string; PrefixBytes: Int64): AnsiString;
var
Stream: TFileStream;
Ctx: TSHA256Context;
Digest: TSHA256Digest;
Buffer: array[0..65535] of Byte;
Remaining: Int64;
Count, I: Integer;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if (PrefixBytes <= 0) or (PrefixBytes > Stream.Size) then
raise EArgumentException.Create('Base length is outside the file');
SHA256Init(Ctx);
Remaining := PrefixBytes;
while Remaining > 0 do
begin
Count := SizeOf(Buffer);
if Remaining < Count then
Count := Integer(Remaining);
Stream.ReadBuffer(Buffer, Count);
SHA256Update(Ctx, Buffer, Count);
Dec(Remaining, Count);
end;
SHA256Final(Ctx, Digest);
finally
Stream.Free;
end;
Result := '';
for I := 0 to High(Digest) do
Result := Result + AnsiString(LowerCase(IntToHex(Digest[I], 2)));
end;
// Al momento dell'invio: l'intero file in uscita è la revisione base
// BaseLength := dimensione di 'out\contract-0042.pdf' in byte
// BaseHash := PrefixSHA256Hex('out\contract-0042.pdf', BaseLength);
Il documento caricato non deve essere esattamente la base. Può essere la base più le revisioni che avete già committato da submission precedenti, ed è così che lo stream cumulativo resta gestibile: BaseBytes continua a nominare il prefisso originale, e tutto ciò che viene dopo è trattato come byte di append precedenti
Applicare le Differences con SaveFDFDifferencesToFile
THPDFHeadlessDocument.SaveFDFDifferencesToFile prende il nome del file di output, i byte FDF, la lunghezza e l'hash della base, una selezione opzionale di FDF incorporato e un record di limiti THPDFFDFPackageOptions, e restituisce un THPDFHeadlessFDFDifferencesResult. Dentro, apre l'FDF come THPDFFDFPackage e chiama da sé DecodeDifferences, quindi voi passate il file FDF, non uno stream pre-decodificato. Un Selection vuoto significa l'FDF di primo livello; uno non vuoto entra prima in un FDF figlio incorporato
uses
SysUtils, IOUtils, HPDFHeadlessDocument, HPDFHeadlessCMS, HPDFFDFPackage;
procedure CommitReturnedForm(const CurrentPdf, NextPdf, FdfFile: string;
BaseLength: Int64; const BaseHash: AnsiString);
var
Doc: THPDFHeadlessDocument;
Policy: THPDFHeadlessCMSOptions;
Selection: THPDFFDFChildSelections;
Applied: THPDFHeadlessFDFDifferencesResult;
begin
Doc := THPDFHeadlessDocument.Create;
try
Policy := THPDFHeadlessCMSOptions.Default;
Policy.TrustedCAFile := 'trust\issuing-ca.pem';
Doc.SetSignaturePolicyOptions(Policy);
Doc.LoadFromFile(CurrentPdf);
Selection := nil; // FDF di primo livello, nessun figlio incorporato
Applied := Doc.SaveFDFDifferencesToFile(NextPdf,
TFile.ReadAllBytes(FdfFile), BaseLength, BaseHash,
Selection, THPDFFDFPackageOptions.Default);
Writeln(Format('base %d, prior %d, total %d, added %d, no-op %s',
[Applied.BaseBytes, Applied.PriorAppendBytes, Applied.TotalAppendBytes,
Applied.AddedBytes, BoolToStr(Applied.NoOp, True)]));
finally
Doc.Free;
end;
end;
L'ordine dei controlli conta più della chiamata stessa. Dopo che l'hash della base passa, il metodo confronta i byte di append già committati, tutto ciò che sta tra BaseBytes e la fine del file caricato, con l'inizio del payload decodificato. Una submission più corta di ciò che avete già committato fallisce con "A cumulative submission cannot remove committed append bytes", e una in disaccordo con essa fallisce con "The cumulative Differences conflict with the committed revision". Solo il suffisso rimanente è nuovo, e solo quel suffisso finisce in AddedBytes
Il candidato assemblato viene scritto su un file temporaneo in esclusiva nella directory di destinazione, svuotato su disco, riaperto come sorgente di revisione indipendente, e validato prima di sostituire l'output. Fallimenti di parsing, cancellazione, policy, budget o pubblicazione lasciano intatti sia il documento caricato sia qualunque file di output esistente. Due precondizioni prendono la gente in anticipo: l'output non deve essere un alias della sorgente caricata, e il documento non deve avere modifiche non salvate, perché un append opaco le scarterebbe silenziosamente ("Publish pending document edits before applying Differences")
Perché un apply ripetuto è un no-op byte per byte?
Un apply ripetuto dello stesso FDF cumulativo è un no-op perché ogni byte del suo payload è già nel file. Quando il payload è identico ai byte di append precedenti, SaveFDFDifferencesToFile riporta NoOp = True e AddedBytes = 0, e l'output che pubblica è identico al PDF corrente. È una garanzia più forte di "append-only", ed è ciò che rende sicuri i retry dopo un timeout di rete o un worker andato in crash
// La Rev 1 è già committata in contract-0042-r1.pdf; lo stesso FDF arriva due volte
// (Doc, Again, Fdf, Selection, BaseLength e BaseHash dichiarati come sopra)
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile('store\contract-0042-r1.pdf');
Again := Doc.SaveFDFDifferencesToFile('store\contract-0042-r1-retry.pdf',
Fdf, BaseLength, BaseHash, Selection, THPDFFDFPackageOptions.Default);
// Again.NoOp = True, Again.AddedBytes = 0,
// e il file di retry è byte-identico a contract-0042-r1.pdf
finally
Doc.Free;
end;
Scrivete i test di accettazione di conseguenza. Asserite piena uguaglianza di byte tra l'output del retry e la revisione corrente, non solo che il file non si sia ridotto. Un test che controlla solo il comportamento append-only passerà felicemente un'implementazione che accoda la stessa revisione una seconda volta, producendo un PDF dall'aspetto valido con una sezione di aggiornamento duplicata che un audit DocMDP successivo deve spiegare
Quali controlli di ammissione girano prima che la revisione sia pubblicata?
Prima della pubblicazione il PDF candidato deve passare controlli di identità, firma e permessi, e nessuno di loro si fida dei campi FDF esterni. Il primo elemento del trailer /ID deve sopravvivere invariato, e ogni campo firma già presente nel documento caricato deve ancora esistere con un dizionario di firma identico e la stessa identità del valore di firma. Un payload Differences che elimina, sostituisce o riscrive una firma esistente fallisce con hdSignatureModificationUnsupported
Poi vengono i livelli di autorizzazione. Per un append non vuoto su un documento cifrato, gli oggetti modificati vengono classificati per ruolo (catalogo e pagine, annotazioni, campi AcroForm, appearance stream) e controllati contro i bit di permesso dell'utente autenticato, così una revisione che modifica annotazioni richiede il permesso sulle annotazioni, e così via. Quindi il candidato passa attraverso la stessa applicazione di certificazione e blocco di campo che HotPDF Component usa altrove: una firma di certificazione DocMDP (ISO 32000-1 §12.8.2.2) limita quali tipi di cambiamento le revisioni successive possono fare, e i blocchi FieldMDP (§12.8.2.4) proteggono i campi nominati. L'analisi delle revisioni DocMDP e FieldMDP in HotPDF copre come quei permessi vengono classificati per revisione
La gestione degli errori dovrebbe separare "questo FDF è malformato" da "questa revisione non è consentita", perché il secondo caso di solito richiede un essere umano:
try
Applied := Doc.SaveFDFDifferencesToFile(NextPdf, Fdf, BaseLength, BaseHash,
Selection, THPDFFDFPackageOptions.Default);
except
on E: EHPDFFDFPackageError do
QuarantineFdf(FdfFile, 'No usable Differences stream: ' + E.Message);
on E: EHPDFHeadlessDocumentError do
case E.Code of
hdPermissionDenied, hdSignatureModificationUnsupported:
EscalateForReview(FdfFile, E.Message);
hdLimitExceeded:
QuarantineFdf(FdfFile, 'Over budget: ' + E.Message);
else
QuarantineFdf(FdfFile, E.Message); // base non combaciante, conflitto, endpoint sbagliato
end;
end;
Ciò che l'operazione non fa mai è importante quanto ciò che controlla. SaveFDFDifferencesToFile non recupera mai la file specification /F dell'FDF, quindi il PDF nominato dentro l'FDF non può dirottare il merge verso un altro file su disco o sulla rete, e non esegue mai JavaScript o azioni. Ogni input arriva esplicitamente: la base caricata, i byte FDF e il materiale di trust che avete configurato
Transazioni Windows e il C ABI fdf.differences.apply
Lo stesso trasporto è disponibile senza scrivere direttamente codice headless. THPDFWindowsFormDataTransaction.ApplyFDFDifferences prende i byte FDF, la lunghezza della base, lo SHA-256 della base, la selezione, le opzioni del pacchetto e una password, delega a SaveFDFDifferencesToFile, e aggiunge la contabilità dei byte della transazione, rollback, cancellazione e comportamento di retry sullo stesso handle. Per i chiamanti non Delphi il C ABI espone l'operazione JSON fdf.differences.apply, dove base porta lunghezza e hash:
{ "schemaVersion": 1, "type": "fdf.differences.apply", "format": "fdf",
"base": { "bytes": 673, "sha256": "..." },
"signaturePolicy": { "trustedCAPEM": "..." } }
La risposta JSON riporta baseBytes, priorAppendBytes, totalAppendBytes, addedBytes e noop, gli stessi campi di THPDFHeadlessFDFDifferencesResult. I gate di accettazione pubblicati per questo trasporto girano su Linux FPC 3.3.1, Windows Delphi 32-bit e 64-bit e Windows FPC 32-bit e 64-bit; ogni security target copre 21 casi con 1.244 controlli nativi e 382 comandi di callback
Limiti e trappole dei fixture da conoscere
Il trasporto Differences è volutamente ristretto, e qualcuno dei suoi bordi ha morso suite di test reali:
- La base deve essere una revisione PDF completa. Una lunghezza che atterra a metà file, o il cui
startxrefnon punta ai suoi dati reali di riferimenti incrociati, viene rifiutata prima che un solo byte venga copiato - I fixture di firma richiedono un
/Lockindiretto. Il lettore di permessi dei documenti caricati su Windows rifiuta un campo firma il cui/Lockè un dizionario inline con "Signature field /Lock must be an indirect reference", anche se alcuni percorsi di firma tollerano la forma inline, quindi scrivete il dizionario di blocco come oggetto a sé - Una CA di test fatta con un
openssl req -x509nudo è priva diCA:TRUEekeyCertSign, quindi la validazione della catena fallisce con un certificate verify error che sembra un bug della libreria e non lo è - I valori esterni di
/Fieldsnon vengono uniti da questa chiamata. Se li volete anche nel documento, decidete esplicitamente se sono informativi, e non presentateli mai come firmati - L'FDF stesso resta non incrementale. Un file FDF con più trailer, generazioni diverse da zero o numeri di oggetto duplicati è un FDF malformato, non uno più ricco
Per il lato dei semplici valori di FDF e XFDF, senza revisioni firmate in ballo, il round trip multi-select FDF e XFDF in HotPDF e import ed export di annotazioni XFDF in Delphi sono i punti di partenza migliori
FAQ
Si può passare lo stream Differences decodificato invece del file FDF?
No. SaveFDFDifferencesToFile esegue il parsing dell'FDF che passate come THPDFFDFPackage, applica la selezione opzionale incorporata e chiama DecodeDifferences internamente. Usate THPDFFDFPackage.DecodeDifferences da sola quando volete ispezionare o archiviare i byte grezzi della revisione, ma consegnate il file FDF alla chiamata di apply
Cosa accade quando il client invia un secondo FDF cumulativo più grande?
Caricate il PDF che contiene già la prima revisione committata e mantenete gli stessi BaseBytes e BaseSHA256. Il metodo verifica che il nuovo payload cominci esattamente con i byte già committati, accoda solo il nuovo suffisso e lo riporta in AddedBytes, poi esegue la piena ammissione di permessi e DocMDP/FieldMDP sul risultato
Un apply riuscito significa che i valori dei campi in /Fields sono firmati?
No. ISO 32000-1 non richiede che /Fields, /Annots e /Differences in un solo FDF combacino, e HotPDF Component non li riconcilia. Ciò che è firmato è tutto ciò che le firme CMS nella revisione accodata coprono, quindi leggete i valori firmati dal PDF unito, non dal wrapper FDF
Il trasporto FDF Differences, il motore di policy di firma headless e le transazioni Windows dei dati dei moduli sono tutti inclusi in HotPDF Component per Delphi, C++Builder e Free Pascal