HotPDF Component aplica un flujo /Differences de FDF desde Delphi a través de THPDFHeadlessDocument.SaveFDFDifferencesToFile: el método verifica que el PDF cargado todavía coincida con una revisión base que usted identifica por largo exacto en bytes y SHA-256, anexa los bytes de la revisión acumulativa exactamente una vez, pasa el resultado por la admisión de permisos de cifrado y DocMDP/FieldMDP, y lo publica atómicamente. Repetir la misma sumisión es un no-op byte a byte
El flujo de trabajo para el que esto existe no tiene glamor y es común. Usted envía un contrato o un formulario de inspección como PDF, alguien lo llena y lo firma en su propio visor, y lo que vuelve no es el PDF sino un archivo FDF. Los valores de campos viajan en /Fields, pero la revisión firmada misma, los bytes que la firma CMS realmente cubre, viaja en /Differences. Equivoque la fusión y o rompe la firma o, peor, acepta valores de campos que ninguna firma cubrió jamás
¿Qué lleva realmente un flujo /Differences de FDF?
Un flujo /Differences de FDF lleva bytes crudos de actualización incremental para el PDF de destino, no una historia a nivel FDF. ISO 32000-1 §12.7.7.3, Tabla 243 define la entrada Differences del diccionario FDF como los cambios incrementales al PDF subyacente hechos desde que se abrió, en la misma forma que una actualización incremental de PDF bajo §7.5.6: objetos nuevos y reemplazados, una sección de referencias cruzadas y un trailer cuyo /Prev apunta de vuelta al archivo base
El contenedor FDF mismo es lo contrario. §12.7.7.1 prohíbe actualizaciones incrementales a archivos FDF, así que un FDF tiene un solo trailer, solo objetos de generación 0 y números de objeto únicos. HotPDF Component aplica exactamente eso en su parser de FDF y nunca lo relaja para facilitar las cosas a /Differences. Los datos incrementales viven dentro de un solo flujo de un contenedor estrictamente no incremental, y mantener esas dos capas separadas es la mitad del trabajo
Dos propiedades del flujo moldean cada decisión de diseño aguas abajo:
- El flujo es acumulativo: contiene cada byte anexado desde que el PDF de destino se abrió, así que una segunda sumisión de la misma sesión repite la revisión que usted ya comprometió y agrega la nueva detrás
- Las entradas externas
/Fieldsy/Annotsdel mismo FDF no tienen garantía de coincidir con/Differences, así que los valores de campos que usted puede leer fácilmente no son los datos que la firma cubre
Ese segundo punto merece franqueza. Si su código de importación lee /V de /Fields y trata el resultado como firmado porque una firma llegó en el mismo archivo, usted ha construido un canal de falsificación. HotPDF Component mantiene las rutas separadas a propósito: el importador de anotaciones FDF rechaza /Differences de plano, y solo el transporte de Differences dedicado lo toca
Atar el anexo a una revisión base explícita
SaveFDFDifferencesToFile se rehúsa a adivinar a qué revisión pertenece el FDF. El llamador pasa BaseBytes, el largo exacto de la revisión PDF completa desde la que se abrió el formulario, y BaseSHA256, su SHA-256 hexadecimal de 64 dígitos. El método recalcula el hash de ese prefijo del documento cargado, rechaza un desfase con "The loaded PDF does not match the explicitly selected base", y verifica que el prefijo termine en un endpoint de revisión real cuyo startxref apunte a sus datos reales de referencias cruzadas
La regla práctica es registrar ambos números cuando envía el formulario, no cuando el FDF regresa. Un helper pequeño sobre las rutinas SHA256Init / SHA256Update / SHA256Final en 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 enviar: el archivo saliente completo es la revisión base
// BaseLength := tamaño de 'out\contract-0042.pdf' en bytes
// BaseHash := PrefixSHA256Hex('out\contract-0042.pdf', BaseLength);
El documento cargado no tiene que ser exactamente la base. Puede ser la base más revisiones que usted ya comprometió de sumisiones anteriores, que es como el flujo acumulativo se mantiene manejable: BaseBytes sigue nombrando el prefijo original, y todo lo que va después se trata como bytes de anexo previos
Aplicar los Differences con SaveFDFDifferencesToFile
THPDFHeadlessDocument.SaveFDFDifferencesToFile toma el nombre del archivo de salida, los bytes del FDF, el largo y hash de la base, una selección opcional de FDF incrustado y un registro de límites THPDFFDFPackageOptions, y devuelve un THPDFHeadlessFDFDifferencesResult. Por dentro, abre el FDF como un THPDFFDFPackage y llama a DecodeDifferences por su cuenta, así que usted pasa el archivo FDF, no un flujo predecodificado. Un Selection vacío significa el FDF de nivel superior; uno no vacío entra primero a un FDF hijo incrustado
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 nivel superior, sin hijo incrustado
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;
El orden de las verificaciones importa más que la llamada misma. Después de que el hash base pasa, el método compara los bytes de anexo ya comprometidos, todo lo que hay entre BaseBytes y el final del archivo cargado, contra el inicio de la carga decodificada. Una sumisión más corta que lo que usted ya comprometió falla con "A cumulative submission cannot remove committed append bytes", y una que no coincide falla con "The cumulative Differences conflict with the committed revision". Solo el sufijo restante es nuevo, y solo ese sufijo acaba en AddedBytes
El candidato ensamblado se escribe en un archivo temporal exclusivo en el directorio de destino, se vacía a disco, se reabre como fuente de revisión independiente, y se valida antes de reemplazar la salida. Las fallas de parseo, cancelación, política, presupuesto o publicación dejan tanto el documento cargado como cualquier archivo de salida existente intactos. Dos precondiciones atrapan a la gente temprano: la salida no debe ser alias de la fuente cargada, y el documento no debe tener ediciones sin guardar, porque un anexo opaco las descartaría en silencio ("Publish pending document edits before applying Differences")
¿Por qué una aplicación repetida es un no-op byte a byte?
Una aplicación repetida del mismo FDF acumulativo es un no-op porque cada byte de su carga ya está en el archivo. Cuando la carga es igual a los bytes de anexo previos exactamente, SaveFDFDifferencesToFile reporta NoOp = True y AddedBytes = 0, y la salida que publica es idéntica al PDF actual. Esa es una garantía más fuerte que "solo anexo", y es lo que hace seguros los reintentos tras un timeout de red o un worker caído
// La rev 1 ya está comprometida en contract-0042-r1.pdf; el mismo FDF llega dos veces
// (Doc, Again, Fdf, Selection, BaseLength y BaseHash declarados como arriba)
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,
// y el archivo de reintento es byte-idéntico a contract-0042-r1.pdf
finally
Doc.Free;
end;
Escriba sus pruebas de aceptación para que coincidan. Afirme igualdad completa de bytes entre la salida del reintento y la revisión actual, no meramente que el archivo no se encogió. Una prueba que solo verifica comportamiento de solo anexo pasará felizmente una implementación que anexa la misma revisión una segunda vez, lo que produce un PDF de aspecto válido con una sección de actualización duplicada que una auditoría DocMDP posterior tiene que explicar
¿Qué verificaciones de admisión corren antes de publicar la revisión?
Antes de la publicación el PDF candidato tiene que pasar verificaciones de identidad, firmas y permisos, y ninguna de ellas confía en los campos externos del FDF. El primer elemento del /ID del trailer debe sobrevivir sin cambios, y cada campo de firma ya presente en el documento cargado debe seguir existiendo con un diccionario de firma idéntico y la misma identidad de valor de firma. Una carga Differences que elimina, reemplaza o reescribe una firma existente falla con hdSignatureModificationUnsupported
Luego vienen las capas de autorización. Para un anexo no vacío sobre un documento cifrado, los objetos cambiados se clasifican por rol (catálogo y páginas, anotaciones, campos AcroForm, flujos de apariencia) y se verifican contra los bits de permisos del usuario autenticado, así que una revisión que edita anotaciones necesita el permiso de anotaciones, y así sucesivamente. Después el candidato pasa por la misma aplicación de certificación y bloqueo de campos que HotPDF Component usa en otros lados: una firma de certificación DocMDP (ISO 32000-1 §12.8.2.2) limita qué tipos de cambio pueden hacer las revisiones posteriores, y los bloqueos FieldMDP (§12.8.2.4) protegen campos nombrados. El análisis de revisiones DocMDP y FieldMDP en HotPDF cubre cómo se clasifican esos permisos por revisión
El manejo de errores debería separar "este FDF está malformado" de "esta revisión no está permitida", porque el segundo caso normalmente necesita un humano:
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 que no coincide, conflicto, endpoint malo
end;
end;
Lo que la operación nunca hace es tan importante como lo que verifica. SaveFDFDifferencesToFile nunca busca la especificación de archivo /F del FDF, así que el PDF nombrado dentro del FDF no puede redirigir la fusión a otro archivo en disco o en la red, y nunca ejecuta JavaScript ni acciones. Cada entrada llega explícitamente: la base cargada, los bytes del FDF y el material de confianza que usted configuró
Transacciones de Windows y la C ABI fdf.differences.apply
El mismo transporte está disponible sin escribir código headless directamente. THPDFWindowsFormDataTransaction.ApplyFDFDifferences toma los bytes del FDF, el largo base, el SHA-256 base, la selección, las opciones de paquete y una contraseña, delega a SaveFDFDifferencesToFile, y agrega la contabilidad de bytes, rollback, cancelación y comportamiento de reintento con el mismo handle de la transacción. Para llamadores que no son Delphi, la C ABI expone la operación JSON fdf.differences.apply, donde base lleva el largo y el hash:
{ "schemaVersion": 1, "type": "fdf.differences.apply", "format": "fdf",
"base": { "bytes": 673, "sha256": "..." },
"signaturePolicy": { "trustedCAPEM": "..." } }
La respuesta JSON reporta baseBytes, priorAppendBytes, totalAppendBytes, addedBytes y noop, los mismos campos que THPDFHeadlessFDFDifferencesResult. Las compuertas de aceptación publicadas para este transporte corren en Linux FPC 3.3.1, Windows Delphi de 32 y 64 bits y Windows FPC de 32 y 64 bits; cada objetivo de seguridad cubre 21 casos con 1,244 verificaciones nativas y 382 comandos de callback
Límites y trampas de fixtures que vale conocer
El transporte de Differences es deliberadamente angosto, y algunas de sus aristas han mordido suites de pruebas reales:
- La base debe ser una revisión PDF completa. Un largo que cae a mitad de archivo, o cuyo
startxrefno apunta a sus datos reales de referencias cruzadas, se rechaza antes de copiar cualquier byte - Los fixtures de firmas necesitan un
/Lockindirecto. El lector de permisos de documentos cargados de Windows rechaza un campo de firma cuyo/Lockes un diccionario inline con "Signature field /Lock must be an indirect reference", aunque algunas rutas de firma toleran la forma inline, así que escriba el diccionario de bloqueo como su propio objeto - Una CA de prueba hecha con un
openssl req -x509pelado carece deCA:TRUEykeyCertSign, así que la validación de cadena falla con un error de verificación de certificado que parece un bug de la biblioteca y no lo es - Los valores externos de
/Fieldsno se fusionan con esta llamada. Si también los quiere en el documento, decida explícitamente si son informativos, y nunca preséntelos como firmados - El FDF mismo se mantiene no incremental. Un archivo FDF con múltiples trailers, generaciones distintas de cero o números de objeto duplicados es un FDF malformado, no uno más rico
Para el lado de valores simples de FDF y XFDF, sin revisiones firmadas de por medio, el viaje de ida y vuelta de FDF y XFDF multi-selección en HotPDF y la importación y exportación de anotaciones XFDF en Delphi son mejores puntos de partida
FAQ
¿Puedo pasar el flujo Differences decodificado en lugar del archivo FDF?
No. SaveFDFDifferencesToFile parsea el FDF que usted pasa como un THPDFFDFPackage, aplica la selección opcional de incrustado y llama a DecodeDifferences internamente. Use THPDFFDFPackage.DecodeDifferences por su cuenta cuando quiera inspeccionar o archivar los bytes crudos de la revisión, pero entregue el archivo FDF a la llamada de aplicación
¿Qué pasa cuando el cliente envía un segundo FDF acumulativo más grande?
Cargue el PDF que ya contiene la primera revisión comprometida y conserve los mismos BaseBytes y BaseSHA256. El método verifica que la nueva carga empiece exactamente con los bytes ya comprometidos, anexa solo el sufijo nuevo y lo reporta en AddedBytes, luego corre la admisión completa de permisos y DocMDP/FieldMDP sobre el resultado
¿Una aplicación exitosa significa que los valores de campos en /Fields están firmados?
No. ISO 32000-1 no exige que /Fields, /Annots y /Differences en un mismo FDF coincidan, y HotPDF Component no los concilia. Lo firmado es lo que las firmas CMS de la revisión anexada cubren, así que lea los valores firmados del PDF fusionado, no del envoltorio FDF
El transporte de FDF Differences, el motor de política de firmas headless y las transacciones de form-data de Windows vienen todos en HotPDF Component para Delphi, C++Builder y Free Pascal