HotPDF Component applique un flux /Differences FDF depuis Delphi via THPDFHeadlessDocument.SaveFDFDifferencesToFile : la méthode vérifie que le PDF chargé correspond toujours à une révision de base que vous identifiez par longueur en octets exacte et SHA-256, ajoute les octets de révision cumulés exactement une fois, fait passer le résultat par ladmission de permissions de chiffrement et DocMDP/FieldMDP, et le publie atomiquement. Répéter la même soumission est un no-op octet pour octet
Le workflow pour lequel ça existe nest pas glorieux et il est courant. Vous envoyez un contrat ou un formulaire dinspection en PDF, quelquun le remplit et le signe dans sa propre visionneuse, et ce qui revient nest pas le PDF mais un fichier FDF. Les valeurs de champs voyagent dans /Fields, mais la révision signée elle-même, les octets que la signature CMS couvre réellement, voyage dans /Differences. Se tromper dans la fusion et vous cassez la signature ou, pire, acceptez des valeurs de champs quaucune signature na jamais couvertes
Que porte réellement un flux /Differences FDF ?
Un flux /Differences FDF porte les octets bruts de mise à jour incrémentale pour le PDF cible, pas un historique au niveau FDF. ISO 32000-1 §12.7.7.3, Table 243, définit lentrée Differences du dictionnaire FDF comme les changements incrémentaux apportés au PDF sous-jacent depuis son ouverture, sous la même forme quune mise à jour incrémentale PDF selon §7.5.6 : objets nouveaux et remplacés, une section de table de référence croisée et un trailer dont le /Prev pointe en arrière dans le fichier de base
Le conteneur FDF lui-même est linverse. §12.7.7.1 interdit les mises à jour incrémentales des fichiers FDF, si bien quun FDF a un trailer unique, uniquement des objets de génération 0 et des numéros dobjets uniques. HotPDF Component fait respecter exactement cela dans son analyseur FDF et ne lassouplit jamais pour rendre /Differences plus facile. Les données incrémentales vivent dans un seul flux dun conteneur strictement non incrémental, et garder ces deux couches séparées fait la moitié du travail
Deux propriétés du flux façonnent chaque décision de conception en aval :
- Le flux est cumulatif : il contient chaque octet ajouté depuis louverture du PDF cible, si bien quune seconde soumission de la même session répète la révision que vous avez déjà commitée et ajoute la nouvelle derrière
- Les entrées externes
/Fieldset/Annotsdu même FDF ne sont pas garanties daccorder avec/Differences, si bien que les valeurs de champs que vous pouvez lire facilement ne sont pas les données que couvre la signature
Ce second point mérite de la franchise. Si votre code dimport lit /V dans /Fields et traite le résultat comme signé parce quune signature est arrivée dans le même fichier, vous avez bâti un canal de falsification. HotPDF Component garde les chemins séparés à dessein : limporteur dannotations FDF rejette /Differences carrément, et seul le transport Differences dédié y touche
Lier lappendice à une révision de base explicite
SaveFDFDifferencesToFile refuse de deviner à quelle révision le FDF appartient. Lappelant passe BaseBytes, la longueur exacte de la révision PDF complète depuis laquelle le formulaire a été ouvert, et BaseSHA256, son SHA-256 hexadécimal de 64 chiffres. La méthode rehache ce préfixe du document chargé, rejette une inadéquation avec "The loaded PDF does not match the explicitly selected base", et vérifie que le préfixe se termine à un vrai point de révision dont le startxref pointe sur ses données réelles de référence croisée
La règle pratique est denregistrer les deux nombres quand vous envoyez le formulaire, pas quand le FDF revient. Un petit helper au-dessus des routines SHA256Init / SHA256Update / SHA256Final de HPDFCrypt suffit :
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;
// À lenvoi : le fichier sortant entier est la révision de base
// BaseLength := taille de 'out\contract-0042.pdf' en octets
// BaseHash := PrefixSHA256Hex('out\contract-0042.pdf', BaseLength);
Le document chargé ne doit pas être exactement la base. Il peut être la base plus des révisions que vous avez déjà commitées depuis des soumissions antérieures, et cest ainsi que le flux cumulatif reste gérable : BaseBytes continue de nommer le préfixe dorigine, et tout ce qui suit est traité comme octets dappendice antérieurs
Appliquer les Differences avec SaveFDFDifferencesToFile
THPDFHeadlessDocument.SaveFDFDifferencesToFile prend le nom du fichier de sortie, les octets FDF, la longueur et le hash de base, une sélection FDF incorporée optionnelle et un enregistrement de limites THPDFFDFPackageOptions, et renvoie un THPDFHeadlessFDFDifferencesResult. À lintérieur, il ouvre le FDF comme un THPDFFDFPackage et appelle lui-même DecodeDifferences, si bien que vous passez le fichier FDF, pas un flux pré-décodé. Un Selection vide signifie le FDF de premier niveau ; un non vide descend dabord dans un FDF enfant incorporé
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 de premier niveau, pas denfant incorporé
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;
Lordre des vérifications compte plus que lappel lui-même. Après que le hash de base passe, la méthode compare les octets dappendice déjà commités, tout ce qui est entre BaseBytes et la fin du fichier chargé, au début du contenu décodé. Une soumission plus courte que ce que vous avez déjà commité échoue avec "A cumulative submission cannot remove committed append bytes", et une qui diverge échoue avec "The cumulative Differences conflict with the committed revision". Seul le suffixe restant est neuf, et seul ce suffixe finit dans AddedBytes
Le candidat assemblé est écrit dans un fichier temporaire exclusif dans le répertoire cible, purgé, rouvert comme source de révision indépendante, et validé avant de remplacer la sortie. Les échecs danalyse, dannulation, de politique, de budget ou de publication laissent le document chargé comme tout fichier de sortie existant intacts. Deux préconditions attrapent les gens tôt : la sortie ne doit pas être un alias de la source chargée, et le document ne doit avoir aucune édition non sauvegardée, parce quun appendice opaque les jetterait en silence ("Publish pending document edits before applying Differences")
Pourquoi une application répétée est-elle un no-op octet pour octet ?
Une application répétée du même FDF cumulatif est un no-op parce que chaque octet de son contenu est déjà dans le fichier. Quand le contenu égale exactement les octets dappendice antérieurs, SaveFDFDifferencesToFile rapporte NoOp = True et AddedBytes = 0, et la sortie quil publie est identique au PDF courant. Cest une garantie plus forte que "ajout seul", et cest ce qui rend sûres les relances après un délai réseau dépassé ou un worker planté
// La révision 1 est déjà commitée dans contract-0042-r1.pdf ; le même FDF arrive deux fois
// (Doc, Again, Fdf, Selection, BaseLength et BaseHash déclarés comme ci-dessus)
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,
// et le fichier de relance est identique octet à octet à contract-0042-r1.pdf
finally
Doc.Free;
end;
Écrivez vos tests dacceptation pour matcher. Affirmez légalité complète des octets entre la sortie de relance et la révision courante, pas seulement que le fichier na pas rétréci. Un test qui ne contrôle que le comportement ajout-seul passera joyeusement une implémentation qui ajoute la même révision une seconde fois, ce qui produit un PDF dapparence valide avec une section de mise à jour dupliquée quun audit DocMDP ultérieur devra expliquer
Quelles vérifications dadmission tournent avant la publication de la révision ?
Avant publication, le PDF candidat doit passer des vérifications didentité, de signature et de permissions, et aucune ne fait confiance aux champs FDF externes. Le premier élément du /ID du trailer doit survivre inchangé, et chaque champ de signature déjà présent dans le document chargé doit encore exister avec un dictionnaire de signature identique et la même identité de valeur de signature. Un contenu Differences qui abandonne, remplace ou réécrit une signature existante échoue avec hdSignatureModificationUnsupported
Viennent ensuite les couches dautorisation. Pour un appendice non vide sur un document chiffré, les objets modifiés sont classés par rôle (catalogue et pages, annotations, champs AcroForm, flux dapparence) et confrontés aux bits de permissions utilisateur authentifiés, si bien quune révision qui édite des annotations a besoin de la permission dannotations, et ainsi de suite. Puis le candidat passe par la même application de certification et de verrous de champs quHotPDF Component emploie ailleurs : une signature de certification DocMDP (ISO 32000-1 §12.8.2.2) limite les types de changement que les révisions ultérieures peuvent faire, et les verrous FieldMDP (§12.8.2.4) protègent les champs nommés. Lanalyse de révisions DocMDP et FieldMDP dans HotPDF couvre comment ces permissions sont classées par révision
La gestion derreurs devrait séparer "ce FDF est mal formé" de "cette révision nest pas autorisée", parce que le second cas demande dordinaire un humain :
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); // inadéquation de base, conflit, endpoint faux
end;
end;
Ce que lopération ne fait jamais compte autant que ce quelle vérifie. SaveFDFDifferencesToFile ne va jamais chercher la spécification de fichier /F du FDF, si bien que le PDF nommé dans le FDF ne peut pas rediriger la fusion vers un autre fichier sur le disque ou le réseau, et il nexécute jamais de JavaScript ni dactions. Chaque entrée arrive explicitement : la base chargée, les octets FDF et le matériau de confiance que vous avez configuré
Transactions Windows et lABI C fdf.differences.apply
Le même transport est disponible sans écrire directement du code headless. THPDFWindowsFormDataTransaction.ApplyFDFDifferences prend les octets FDF, la longueur de base, le SHA-256 de base, la sélection, les options de paquet et un mot de passe, délègue à SaveFDFDifferencesToFile, et ajoute la comptabilité doctets de la transaction, le rollback, lannulation et le comportement de relance sur le même handle. Pour les appelants non Delphi, lABI C expose lopération JSON fdf.differences.apply, où base porte la longueur et le hash :
{ "schemaVersion": 1, "type": "fdf.differences.apply", "format": "fdf",
"base": { "bytes": 673, "sha256": "..." },
"signaturePolicy": { "trustedCAPEM": "..." } }
La réponse JSON rapporte baseBytes, priorAppendBytes, totalAppendBytes, addedBytes et noop, les mêmes champs que THPDFHeadlessFDFDifferencesResult. Les portes dacceptation publiées pour ce transport tournent sur Linux FPC 3.3.1, Windows Delphi 32 bits et 64 bits et Windows FPC 32 bits et 64 bits ; chaque cible de sécurité couvre 21 cas avec 1 244 vérifications natives et 382 commandes de callback
Limites et pièges de fixtures à connaître
Le transport Differences est délibérément étroit, et quelques-uns de ses bords ont mordu de vraies suites de tests :
- La base doit être une révision PDF complète. Une longueur qui atterrit en plein milieu de fichier, ou dont le
startxrefne pointe pas sur ses données réelles de référence croisée, est rejetée avant que le moindre octet soit copié - Les fixtures de signatures ont besoin dun
/Lockindirect. Le lecteur de permissions du document chargé Windows rejette un champ de signature dont le/Lockest un dictionnaire en ligne avec "Signature field /Lock must be an indirect reference", même si certains chemins de signature tolèrent la forme en ligne, donc écrivez le dictionnaire de verrou comme son propre objet - Une AC de test faite avec un simple
openssl req -x509manque deCA:TRUEet dekeyCertSign, si bien que la validation de chaîne échoue avec une erreur de vérification de certificat qui ressemble à un bug de bibliothèque et nen est pas un - Les valeurs externes de
/Fieldsne sont pas fusionnées par cet appel. Si vous les voulez aussi dans le document, décidez explicitement si elles sont informatives, et ne les présentez jamais comme signées - Le FDF lui-même reste non incrémental. Un fichier FDF avec plusieurs trailers, des générations non nulles ou des numéros dobjets dupliqués est un FDF mal formé, pas un plus riche
Pour le côté valeurs simples de FDF et XFDF, sans révisions signées au tableau, le aller-retour FDF et XFDF multi-sélection dans HotPDF et limport et export dannotations XFDF en Delphi sont les meilleurs points de départ
FAQ
Puis-je passer le flux Differences décodé au lieu du fichier FDF ?
Non. SaveFDFDifferencesToFile analyse le FDF que vous passez comme un THPDFFDFPackage, applique la sélection incorporée optionnelle et appelle DecodeDifferences en interne. Employez THPDFFDFPackage.DecodeDifferences seul quand vous voulez inspecter ou archiver les octets bruts de révision, mais confiez le fichier FDF à lappel dapplication
Que se passe-t-il quand le client envoie un second FDF cumulatif plus grand ?
Chargez le PDF qui contient déjà la première révision commitée et gardez les mêmes BaseBytes et BaseSHA256. La méthode vérifie que le nouveau contenu commence par exactement les octets déjà commités, najoute que le nouveau suffixe et le rapporte dans AddedBytes, puis fait passer ladmission complète de permissions et DocMDP/FieldMDP sur le résultat
Une application réussie signifie-t-elle que les valeurs de /Fields sont signées ?
Non. ISO 32000-1 nexige pas que /Fields, /Annots et /Differences dun même FDF saccordent, et HotPDF Component ne les réconcilie pas. Ce qui est signé, cest ce que couvrent les signatures CMS de la révision ajoutée, donc lisez les valeurs signées depuis le PDF fusionné, pas depuis lenveloppe FDF
Le transport Differences FDF, le moteur de politique de signature headless et les transactions de données de formulaires Windows sont tous livrés dans HotPDF Component pour Delphi, C++Builder et Free Pascal