Genere la misma factura dos veces desde la misma plantilla y los mismos datos, y haga diff entre los dos PDFs. Dos lugares difieren: las cadenas hex del arreglo /ID del trailer y la cadena ModDate del diccionario de información del documento, ambos tomados del reloj. En losLab PDF Library, SetDeterministicDocumentID(1) más un ModDate fijo hace los archivos idénticos byte a byte, para un documento sin UUIDs de XMP por ejecución ni adjuntos
El primer intento obvio es fijar las fechas y volver a correr. CreationDate y ModDate ahora coinciden, y los hashes siguen difiriendo, ahora solo en dos cadenas hex de 32 dígitos cerca del final del archivo, dentro de /ID [<…> <…>]. Ese identificador se computa por separado de los metadatos, y hasta que deje de depender de la hora del guardado, un cache de builds indexado por el hash del archivo nunca acierta y un store direccionable por contenido guarda un blob por ejecución en lugar de uno por documento
Para qué sirve el arreglo ID del trailer
El /ID del trailer identifica un archivo; no es un checksum de su contenido. ISO 32000-1 §14.4 lo define como un arreglo de dos cadenas de bytes. La primera es el identificador permanente, asignado cuando el documento se crea y pensado para sobrevivir cada edición posterior. La segunda es el identificador cambiante, que un escritor refresca cada vez que modifica el archivo. Un sistema de gestión documental compara el par para decidir si dos archivos son revisiones de un documento o dos documentos sin relación. La Tabla 15 de §7.5.5 hace la entrada obligatoria cuando el trailer trae /Encrypt y opcional en caso contrario, y §14.4 advierte que omitirla puede estorbar a flujos de trabajo que dependen de la identidad del archivo
La especificación nunca dice cómo computar el valor. Sugiere un digest MD5 sobre cosas como la hora actual, la ruta del archivo, el tamaño del archivo y los valores del diccionario de información, y el reloj es el ingrediente que hace único el resultado. La unicidad es exactamente lo que usted quiere de un marcador de identidad y exactamente lo que arruina la reproducibilidad. Por omisión losLab PDF Library sigue esa sugerencia en su forma más simple, un MD5 del timestamp actual, así que dos documentos creados con un segundo de diferencia reciben identificadores permanentes distintos aunque todos los demás bytes coincidan
Activar un identificador reproducible
El modo determinista es un interruptor por documento y está apagado por omisión, así que la salida existente no cambia hasta que usted lo pida. SetDeterministicDocumentID toma 0 o 1 y devuelve 1 cuando el valor fue aceptado, 0 para cualquier otra cosa; GetDeterministicDocumentID reporta el ajuste actual. SetDocumentIDSeed suministra una cadena semilla explícita, y con una semilla en su lugar el identificador es un MD5 de esa cadena, escrito en ambos elementos del arreglo. GetDocumentFileID lee /ID[0] de vuelta después del guardado, lo que resulta útil para logs o para una aserción de prueba
var
Lib: TPDFlib;
FileID: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1); // justo después de Create
Lib.SetDocumentIDSeed('invoice-4471-rev3');
Lib.SetInformation(7, 'D:20260101000000Z'); // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z'); // ModDate
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Invoice 4471');
Lib.SaveToFile('invoice.pdf');
FileID := Lib.GetDocumentFileID; // el mismo valor en cada ejecución
finally
Lib.Free;
end;
end;
El identificador se computa al momento de guardar. Activar el interruptor tarde en la construcción del documento igual funciona, y una semilla cambiada llega al archivo en el siguiente guardado completo: guarde con la semilla A, guarde otra vez con la semilla B, y las dos salidas llevan identificadores distintos, mientras que volver a la semilla A le devuelve el valor original
Aunque active el interruptor inmediatamente después de crear el objeto. Las fuentes, las imágenes, los estados gráficos y otros recursos de página reciben nombres en el diccionario de recursos de la página, y mientras el modo determinista está activo esos nombres vienen de un contador. Cualquier cosa agregada antes del interruptor conserva un nombre generado al azar, lo que deja el identificador estable pero cambia unos cuantos bytes en los content streams en cada ejecución
¿Por qué el identificador sigue cambiando cuando no hay semilla?
Porque el diccionario de información es parte del hash. Deje la semilla vacía, o pase una cadena vacía a SetDocumentIDSeed para limpiar una, y losLab PDF Library deriva el identificador del propio archivo: pasa el cuerpo serializado por SHA-256 mientras lo escribe y usa los primeros 16 bytes del digest para ambos elementos de /ID. Cualquier cambio al contenido de página cambia el identificador, que es todo el punto. La trampa es que CreationDate y ModDate viven en ese cuerpo también, y las rutinas de guardado estampan ModDate con la hora actual salvo que quien llama la haya fijado
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed(''); // derive el /ID de los bytes guardados
// El hash del cuerpo cubre el diccionario Info, así que las fechas deben ser
// entradas del build. La clave 8 también evita que el guardado estampe ModDate.
Lib.SetInformation(2, 'Quarterly Report'); // Title
Lib.SetInformation(5, 'reporting-service 4.2'); // Creator
Lib.SetInformation(7, 'D:20260101000000Z'); // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z'); // ModDate
Lib.SaveToFile('report.pdf');
La clave 8 hace doble trabajo aquí, y es la línea que la gente más suele omitir. Fijar ModDate vía SetInformation marca la fecha como suministrada por quien llama, y desde entonces SaveToFile y SaveToString la dejan tranquila. Sin eso, cada guardado escribe un timestamp fresco, el cuerpo hasheado cambia, y el identificador derivado del contenido cambia con él. Tome las fechas del registro de origen o de un epoch fijo. Now no tiene nada que hacer en un build reproducible
Algunas cosas se dejan fuera del hash a propósito. Los nombres de recursos generados al azar y el UUID de una entrada XMP xmpMM:InstanceID se normalizan antes de hashear, así que dos ejecuciones que difieran solo en esos tokens reciben el mismo /ID. Los bytes del archivo igual difieren en esos lugares, así que un documento estilo PDF/A con un UUID de instancia fresco por ejecución le da un identificador reproducible pero no un archivo idéntico byte a byte. Los adjuntos incrustados son otra fuente de deriva: AddAssociatedFileFromString estampa el /ModDate propio del adjunto con la hora actual, y esa fecha se hashea como cualquier otro byte del cuerpo
Semilla explícita o hash de contenido
Para artefactos de build dejaríamos la semilla vacía. Un identificador derivado del contenido cambia exactamente cuando los bytes cambian, así que un cache indexado por él nunca puede servir un archivo obsoleto, y usted no tiene una segunda clave que mantener sincronizada con los datos. Una semilla explícita encaja en otro trabajo: un archivo de destino aguas abajo que trata /ID[0] como identidad del documento y espera el mismo identificador a través de regeneraciones de la misma factura, incluso después de que la plantilla cambie. Eso coincide mejor con la intención de §14.4, al precio de que dos archivos visiblemente distintos puedan compartir identificador
Una consecuencia de la ruta de contenido merece una decisión deliberada. Cuando usted carga un PDF existente y lo guarda completo con el modo determinista activo, el /ID entrante se descarta y el nuevo depende solo de lo que se escribe. Dos archivos fuente que difieren solo en sus identificadores de trailer salen con el mismo /ID. Eso es lo que un paso de build normalizador quiere. Si usted está editando el documento de un cliente y su identidad tiene que sobrevivir, deje el modo determinista apagado para ese trabajo o guárdelo incrementalmente
¿Por qué reescribir el ID rompe un PDF cifrado?
Porque en un documento cifrado /ID[0] es material de clave. ISO 32000-1 §7.6.3.3 Algoritmo 2 alimenta el primer elemento del identificador de archivo al cálculo de la clave de cifrado para el manejador de seguridad estándar en las revisiones 2 a 4, junto con la contraseña con padding, el valor /O y los bits de permisos. Esa clave produce luego la cadena /U que un lector verifica al abrir. La clave se deriva cuando usted llama a Encrypt o carga un archivo cifrado, antes de cualquier guardado, así que reemplazar el identificador durante el guardado produciría un archivo bien formado cuya verificación de contraseña falla para todos, incluido usted
losLab PDF Library por lo tanto refresca el identificador determinista solo en guardados completos sin cifrar. Un documento cifrado conserva el /ID que ya tenía, y el interruptor no tiene efecto en esa ruta. Reabrir ese archivo con la contraseña de usuario funciona como siempre. Tampoco meta salida cifrada en una comparación de bytes: el cifrado AES bajo §7.6.2 comienza cada cadena y stream cifrado con un vector de inicialización aleatorio de 16 bytes, así que dos cifrados del mismo documento nunca coinciden byte a byte. Si necesita una comprobación de regresión ahí, descifre ambos y compare lo que recibe de vuelta. El manejo de revisiones y los bits de permisos están cubiertos con más profundidad en el recorrido de auditoría de cifrado y permisos de PDF
Los guardados incrementales conservan el identificador original
Una actualización incremental deja en su lugar cada byte anterior del archivo y agrega una nueva revisión después. La permanencia de /ID[0] es lo que le dice a un consumidor que la revisión agregada pertenece al mismo documento que las anteriores. Reescribirlo contradiría las revisiones ya presentes en el archivo y socavaría los flujos de firma, ya que una firma cubre un rango de bytes de una revisión específica. El modo append nunca toca el identificador en modo determinista, lo que mantiene intacto el comportamiento descrito en el artículo sobre actualizaciones incrementales de PDF y append a stream
Mantener la salida reproducible a lo largo del tiempo
Cada cambio posterior al generador es una oportunidad de meter un valor de reloj de vuelta al archivo: una marca de agua que imprime la hora de generación, una ruta de código que adjunta un archivo, una opción de guardado distinta. Las opciones de guardado importan tanto como el contenido: empacar objetos en object streams comprimidos produce una disposición de archivo completamente distinta a la tabla de referencias cruzadas clásica, como explican las notas sobre object streams y cross reference streams, así que mantenga esos ajustes fijos por canalización. La defensa más barata es una prueba que construye el mismo documento dos veces en memoria y compara los bytes
function BuildQuote(const Seed: WideString): AnsiString;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed(Seed);
Lib.SetInformation(7, 'D:20260101000000Z');
Lib.SetInformation(8, 'D:20260101000000Z');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Quote 8812');
Result := Lib.SaveToString;
finally
Lib.Free;
end;
end;
// Dos builds independientes deben dar una sola secuencia de bytes
if BuildQuote('quote-8812') = BuildQuote('quote-8812') then
WriteLn('reproducible')
else
WriteLn('nondeterminism leaked into the output');
El mismo arnés puede comparar una corrida serial y una paralela de RecompressStreams sobre la misma fuente: cárguela en dos instancias TPDFlib, active IDs determinista y fije ModDate en ambas, ponga SetOptimizationWorkers(1) en una y un conteo de workers mayor en la otra, y haga diff de los dos resultados SaveToString. Con el reloj fuera de ambos archivos, la diferencia que quede viene de la recompresión misma
Identificadores de trailer reproducibles, control de metadatos y guardado incremental forman parte de losLab PDF Library for Delphi y C++Builder, cuya referencia también lista las opciones de cifrado mencionadas aquí