HotPDF compara dos documentos PDF desde Delphi mediante THPDFDocComparison, que recorre el grafo de objetos de ambos archivos desde el catálogo hacia afuera y, cuando se le solicita, también renderiza cada par de páginas y mide los píxeles que difieren. El resultado es un informe JSON que nombra cada diferencia encontrada, el presupuesto consumido y si la comparación se ejecutó hasta completarse. Ambos recorridos importan, porque una comparación estructural y una comparación visual responden preguntas distintas
La pregunta detrás de esta función suele ser una pregunta de lanzamiento. Un motor de informes recibe un cambio, se regenera la salida, y alguien tiene que decidir si algo se movió. Abrir ambos archivos lado a lado funciona hasta unas tres páginas, antes de que falle la atención. Comparar los bytes en bruto falla de inmediato, ya que dos ejecuciones del mismo generador producen bytes distintos por razones que no tienen nada que ver con lo que ve un lector
¿Por qué pueden dos PDF ser distintos en bytes pero visualmente idénticos?
Dos PDF generados de forma independiente que se imprimen de manera idéntica difieren habitualmente en sus bytes, y las razones son estructurales, no cosméticas. Los números de objeto se asignan en el orden en que los objetos se van escribiendo. Los subconjuntos de fuentes asignan CID en el orden en que los glifos se encuentran por primera vez, así que un subconjunto construido durante un recorrido ligeramente distinto produce bytes de flujo de contenido diferentes para el mismo texto visible. Los desplazamientos de la tabla de referencias cruzadas cambian cada vez que algo anterior cambia de longitud
Por eso los números de objeto no pueden usarse como identidad entre documentos. En su lugar, HotPDF construye cada instantánea recorriendo desde el catálogo, expandiendo los diccionarios en el orden de bytes de sus claves y los arreglos por índice, de modo que cada objeto queda nombrado por la ruta que lo alcanza. Los objetos a los que el recorrido no puede llegar desde la raíz recurren a una ruta sintética $Unreachable[...] que lleva el número de objeto y la generación, lo que mantiene visible el contenido huérfano en el informe en lugar de que desaparezca en silencio
Los flujos no se comparan copiándolos. Cada flujo aporta una firma SHA-256 incremental, calculada mientras se restaura después la posición original del flujo, así que comparar dos archivos de cien megabytes no implica materializar doscientos megabytes dos veces
Alinear páginas cuando un documento tiene una inserción
Comparar la página 1 contra la página 1, la página 2 contra la página 2 y así sucesivamente solo es correcto cuando no se insertó nada. Al insertar una portada, una comparación ingenua reporta que cada página cambió, lo cual es técnicamente cierto e inútil en la práctica
HotPDF alinea las páginas antes de compararlas. Construye una firma por página a partir del texto extraíble, recurre a una firma estructural para las páginas sin texto, y luego calcula la subsecuencia creciente más larga sobre los índices de destino emparejados. Las páginas dentro de esa subsecuencia son las que simplemente se desplazaron; las páginas fuera de ella son movimientos genuinos. Esa distinción es lo que hace legible la comparación de un manual de 400 páginas, porque el informe dice que se insertó una página en lugar de que cambiaron cuatrocientas
Ejecutar una comparación estructural
La llamada más simple toma dos documentos cargados y un modo. cmStructural realiza el recorrido del grafo de objetos, cmRenderedImage realiza la comparación de píxeles, cmFull hace ambas cosas, y los modos más ligeros cmPageCount, cmPageText y cmObjectCount existen para verificaciones rápidas de humo:
uses
HPDFDoc, HPDFDocCompare;
var
DocA, DocB: THotPDF;
Report: AnsiString;
begin
DocA := THotPDF.Create(nil);
DocB := THotPDF.Create(nil);
try
if (DocA.LoadFromFile('baseline.pdf') <= 0) or
(DocB.LoadFromFile('candidate.pdf') <= 0) then
Exit;
Report := THPDFDocComparison.Compare(DocA, DocB, cmStructural);
with TFileStream.Create('diff.json', fmCreate) do
try
WriteBuffer(Report[1], Length(Report));
finally
Free;
end;
finally
DocB.Free;
DocA.Free;
end;
end;
El informe distingue tres estados que un booleano no puede expresar. identical indica si algo difirió, comparisonComplete indica si el recorrido terminó, y comparisonBudget nombra el límite que lo detuvo, si alguno lo hizo. Una comparación que agota un presupuesto reporta comparisonComplete=false e identical=false juntos, porque un recorrido truncado no tiene base para afirmar igualdad. Cualquier automatización que lea solo identical terminará tratando una interrupción por presupuesto como una diferencia real, así que hay que leer los tres campos
¿Qué límites mantienen acotado el recorrido?
Los valores predeterminados en THPDFStructuralCompareLimits.Default están dimensionados para documentos reales y no para documentos adversarios, y cada presupuesto con relevancia semántica tiene su propio tope: 250.000 objetos, 2.000.000 de aristas, profundidad 128, 10.000 diferencias reportadas, 64 MB por flujo y 512 MB de bytes de flujo en total, 1 MB por valor y 4096 bytes por ruta. Auméntalos deliberadamente cuando conozcas tu corpus, y redúcelos al comparar archivos que llegaron desde el exterior:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // fallar rápido en CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // el valor predeterminado es 72
Options.ColorTolerance := 2; // ignorar ruido de redondeo de 1-2 niveles
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // escribir imágenes superpuestas para revisión
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
El recorrido renderizado estima el conteo de píxeles a partir de las dimensiones de la página y el DPI solicitado antes de asignar cualquier mapa de bits, y vuelve a verificar el mapa de bits real después, de modo que una geometría de página malformada no pueda eludir el presupuesto mintiendo sobre su tamaño. Aumentar el DPI aumenta la fidelidad y el costo de forma cuadrática: 150 DPI equivale a cuatro veces los píxeles de 72, y los topes de píxeles por página y totales existen precisamente porque, de lo contrario, un trabajo por lotes a 300 DPI terminaría asignando memoria hasta meterse en problemas
¿Qué tan similar es lo bastante similar?
Dos páginas cuentan como similares solo cuando se cumplen ambas condiciones: la proporción de píxeles cambiados está en o por debajo de MaxChangedPixelRatio y la similitud está en o por encima de MinimumSimilarity. Dos umbrales en lugar de uno, porque un puñado de píxeles catastróficamente equivocados y un lavado amplio de pequeños cambios de color son fallas distintas, y cualquiera de las dos puede ser aceptable en un flujo de trabajo e inadmisible en otro. Las pruebas de umbral usan valores sin redondear; los seis decimales del JSON existen para mantener los informes estables y comparables como texto, no para definir la comparación
Los píxeles cambiados se agrupan en regiones usando mosaicos de tamaño fijo como nodos con adyacencia de cuatro direcciones, en lugar de un relleno por inundación píxel a píxel. Eso mantiene la memoria acotada y la lista de regiones estable entre ejecuciones. Truncar el detalle de región retenido afecta solo al listado, no al conteo de regiones reportado, así que una página con más regiones cambiadas que MaxChangedRegions igual reporta cuántas hubo
Un comportamiento vale la pena mencionarlo explícitamente porque invierte el instinto habitual. Los fallos del renderizador, los fallos de asignación de memoria y los fallos de superposición nunca se ocultan. Cualquier cosa de ese tipo se registra como renderError o renderBudget y fuerza renderComparisonComplete=false, porque una página que no se pudo renderizar es una página que nadie comparó, y reportarla como idéntica es peor que no reportar nada
Dónde encaja cada modo en una canalización
La comparación estructural responde qué cambió y es la opción predeterminada correcta para las suites de regresión: nombra la ruta, el índice de página y los números de objeto involucrados, así que un fallo apunta directamente al código que lo produjo. La comparación renderizada responde si alguien lo notará, que es la pregunta relevante para las aprobaciones y para verificar que un paso de optimización realmente no perdió información
Se combinan bien. Ejecuta cmStructural en cada compilación y deja que falle ruidosamente ante cambios inesperados a nivel de objeto; ejecuta cmFull con mapas de calor antes de un lanzamiento, cuando haya una persona disponible para revisar las superposiciones. Para canalizaciones que ya emiten marcado de página por otras razones, la salida de texto descrita en exportar páginas PDF a SVG ofrece una tercera vista, comparable por un humano, y las verificaciones automatizadas de automatización de informes de preflight cubren cuestiones de conformidad que ninguno de los dos modos de comparación pretende responder
La comparación, el preflight y el renderizado comparten el mismo modelo de objetos de documento cargado, así que un solo recorrido de un archivo puede alimentar a los tres. La lista completa de funciones para Delphi y C++Builder está en la página del componente PDF para Delphi HotPDF