La comparación visual de PDFlibPas renderiza dos PDF al mismo DPI y hace diff de lo que un lector ve de verdad. TPDFlib.CompareRenderedPagesWith compara un par de páginas y devuelve píxeles cambiados, regiones, una máscara y un overlay. TPDFlib.CompareDocumentVisualsWith y TPDFlib.CompareFileVisuals emparejan documentos completos página a página, detectan páginas insertadas y eliminadas y reportan todo como JSON
El caso de uso es el testing de regresión visual. Alguien reescribe la plantilla de facturas, actualiza un paquete de fuentes o cambia el motor de render, y la pregunta no es si cambiaron los bytes (siempre cambian) sino si algún cliente verá una diferencia. Un diff estructural responde otra pregunta. Dos archivos pueden diferir en cada content stream y aun así pintar páginas idénticas, y un solo operando desplazado puede mover un total tres puntos sin tocar nada que un extractor de texto señalaría. Los píxeles son la única capa donde "se ve igual" es comprobable
¿Qué compara en realidad un diff visual de PDF?
PDFlibPas compara el resultado visible sobre una página blanca, no los contenidos en bruto del buffer. Cada página se renderiza mediante RenderPageToString como un BMP sin compresión, se decodifica a un bitmap top-down y cualquier píxel que lleve alfa se compone sobre blanco antes de comparar los canales. Un bitmap RGB y un bitmap RGBA que se ven iguales en pantalla se comparan por tanto como idénticos, que es justo lo que quieres cuando los dos lados vienen de rutas de render distintas
El decodificador BMP tiene una rareza deliberada que conviene conocer. Un bitmap de 32 bits BI_RGB lleva un cuarto byte por píxel, pero el formato deja ese byte sin definir: unos escritores lo rellenan con 0, otros con 255 y otros con residuos. PLDecodeVisualBMP lo ignora y conserva el color. Tratar ese byte como opacidad permitiría que un renderer que escribe ceros borrara cada píxel de la página, y el diff reportaría encantado dos documentos en blanco e idénticos. Si alimentas tus propios bitmaps a la API de más bajo nivel, el alfa solo se respeta cuando tú declaras el formato como vpfRGBA o vpfBGRA
Un píxel cuenta como cambiado cuando cualquiera de sus canales rojo, verde o azul difiere en más de ColorTolerance. Tolerancia 0 significa bit a bit exacto. En la práctica un valor de un dígito absorbe el temblor del anti-aliasing entre builds del renderer sin ocultar un glifo cambiado, porque un cambio real de texto mueve los canales mucho más que eso. Elige el número a partir de una ejecución baseline conocida buena, no a ojo
Comparar un solo par de páginas con CompareRenderedPagesWith
TPDFlib.CompareRenderedPagesWith es la herramienta cuando ya sabes qué páginas van juntas y quieres detalle: un bounding box, regiones de cambio conexas, una máscara por píxel opcional y un overlay teñido de rojo opcional. El record de opciones y los tipos de resultado viven en la unidad PDFlibVisualCompare, y PLDefaultVisualCompareOptions te da tolerancia 0, tiles de 32 píxeles, sin regiones a ignorar y sin máscara ni overlay
uses
PDFlibrary, PDFlibVisualCompare;
var
Baseline, Candidate: TPDFlib;
Options: TPDFVisualCompareOptions;
Comparison: TPDFVisualCompareResult;
ErrorText: WideString;
begin
Baseline := TPDFlib.Create;
Candidate := TPDFlib.Create;
try
if (Baseline.LoadFromFile('invoice-v1.pdf', '') = 1) and
(Candidate.LoadFromFile('invoice-v2.pdf', '') = 1) then
begin
Options := PLDefaultVisualCompareOptions;
Options.ColorTolerance := 8;
Options.IncludeOverlay := True;
// Enmascara el timestamp de impresión; unidades en píxeles al DPI de abajo
SetLength(Options.IgnoreRects, 1);
Options.IgnoreRects[0].Left := 600;
Options.IgnoreRects[0].Top := 20;
Options.IgnoreRects[0].Right := 790;
Options.IgnoreRects[0].Bottom := 50;
if Baseline.CompareRenderedPagesWith(Candidate, 1, 1, 96, Options,
Comparison, ErrorText) then
begin
if Comparison.ChangedPixels > 0 then
Writeln(PLVisualCompareResultToJSON(Comparison));
end
else
Writeln('Compare failed: ', ErrorText);
end;
finally
Candidate.Free;
Baseline.Free;
end;
end;
Tres detalles muerden aquí. Los rectángulos a ignorar van en píxeles renderizados con origen arriba a la izquierda e intervalos medio abiertos, así que Right y Bottom son excluyentes y el mismo rectángulo cubre otra parte de la página si cambias el DPI. Los rectángulos solapados se cuentan una vez, y los píxeles ignorados aparecen en IgnoredPixels en vez de desaparecer, lo que mantiene honesto a ChangedRatio porque divide solo por píxeles comparados. Por último, la llamada de página única exige que ambos renders tengan las mismas dimensiones; una página que cambió de tamaño falla con error en lugar de estirarse para encajar
Las regiones salen de una pasada de tiles: los píxeles cambiados se agrupan en cuadrados de TileSize y los tiles adyacentes se fusionan en una región con sus propios límites y recuento de píxeles. Tiles mayores dan menos regiones y más gruesas, que suele ser lo que quiere un informe de tests. El overlay es una copia RGB de la segunda página con los píxeles cambiados teñidos de rojo, devuelta como bytes en bruto en Comparison.Overlay; escribirlo a PNG para los artefactos de tu CI es cosa de tu código, no de la biblioteca
¿Cómo gestiona el emparejado a nivel de documento las páginas insertadas?
TPDFlib.CompareDocumentVisualsWith empareja páginas con una subsecuencia común más larga sobre huellas de píxeles exactas, así que una página insertada no convierte todas las siguientes en cambios falsos. Un bucle ingenuo de página 1 con página 1, página 2 con página 2 es inútil en cuanto a una plantilla le añaden una portada: cada página posterior a la inserción queda desfasada en uno y el informe se enciende en rojo de arriba abajo
Cada página se renderiza y se reduce a una clave hecha de su ancho y alto en píxeles y un digest SHA-256 de los píxeles visibles. El tamaño importa más de lo que parece: una página vertical toda blanca y una apaisada toda blanca de la misma área tienen buffers de píxeles idénticos byte a byte, y sin las dimensiones en la clave se anclarían la una a la otra. Las claves idénticas en ambos lados se vuelven anclas, la LCS elige la cadena ordenada más larga de ellas, y las páginas entre dos anclas consecutivas se emparejan en orden. Cuando un hueco guarda más páginas en un lado, el excedente se reporta como added o removed
var
Options: TPDFVisualCompareOptions;
Report: WideString;
begin
// Baseline y Candidate cargados como antes, con ajustes de render coincidentes
Options := PLDefaultVisualCompareOptions;
Options.ColorTolerance := 8;
Options.TileSize := 64;
Report := Baseline.CompareDocumentVisualsWith(Candidate, 96, Options);
if Baseline.LastErrorCode <> 0 then
Writeln('Visual diff failed: ', Report)
else
Writeln(Report);
end;
Las anclas son exactas al bit, y eso es una decisión de diseño, no un descuido. ColorTolerance se aplica solo a la comparación de páginas emparejadas, nunca a la huella. Una página sin nada más que ruido de anti-aliasing no anclará, pero sigue cayendo en un hueco, se empareja en orden con su contraparte y luego compara como unchanged bajo tolerancia. Anclas difusas serían más permisivas y mucho menos predecibles, porque páginas casi duplicadas (separadores en blanco, páginas de términos repetidas) empezarían a casarse entre sí
Ten claro qué es este alineamiento. El JSON lo llama exact-raster-lcs-gap-order a propósito: es un alineamiento de ráster, no reconocimiento semántico de páginas. Si un hueco contiene una página reescrita y una insertada, el emparejado en orden puede comparar la reescritura contra la inserción y reportar la original como eliminada. Los documentos llenos de páginas idénticas son igual de ambiguos. Cuando el emparejado importa para un veredicto, mira los pares, no solo los contadores
Leer el informe JSON
El informe de documento tiene una forma fija sobre la que puedes hacer aserciones en un test runner. Con éxito lleva "status":"complete", "complete":true, el nombre del alineamiento, firstPageCount, secondPageCount y los contadores changedPages, addedPages, removedPages y unchangedPages. El array pairs lista luego cada fila con firstPage, secondPage (0 para el lado ausente) y un kind de unchanged, changed, size_changed, added o removed
Las filas de tipo changed y unchanged incrustan un objeto comparison con width, height, comparedPixels, changedPixels, ignoredPixels, changedRatio, boundingBox (null cuando nada cambió) y una lista regions de bounds más changedPixels. Una fila size_changed no lleva comparison en absoluto; reporta firstWidth, firstHeight, secondWidth y secondHeight y cuenta en changedPages. Estirar una página redimensionada para compararla produciría un muro de píxeles cambiados sin sentido, así que la biblioteca reporta el cambio de geometría y se queda ahí
El informe de documento nunca contiene máscaras ni overlays. CompareDocumentVisualsWith fuerza IncludeMask y IncludeOverlay a off aunque tus opciones lo pidan, así que la memoria se mantiene plana a medida que crece el número de páginas en lugar de guardar una vista previa de página completa por cada par. Usa el informe para encontrar las páginas interesantes y llama luego a CompareRenderedPagesWith solo sobre esas para obtener el overlay. Los fallos vuelven como "status":"error", "complete":false con una cadena error, y LastErrorCode se fija en 401
Comparar contra un archivo con CompareFileVisuals
TPDFlib.CompareFileVisuals es la forma de una llamada para CI: carga el otro PDF, copia sobre él los ajustes de render de la instancia actual y devuelve el mismo JSON de documento. La firma es CompareFileVisuals(OtherFileName, Password, DPI, ColorTolerance), donde la tolerancia debe ser 0 a 255 y el DPI debe ser finito y positivo
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('baseline\statement.pdf', '') = 1 then
begin
Report := Lib.CompareFileVisuals('build\statement.pdf', '', 96, 8);
Writeln(Report);
end;
finally
Lib.Free;
end;
end;
La herencia del renderer es la razón para preferir esta llamada a cargar el segundo archivo tú mismo. El documento cargado toma la escala de render, el tipo de crop y los ajustes de gráficos de la instancia actual, más la elección de motor Cairo o PDFium cuando están compilados. Si construyes dos instancias TPDFlib a mano para CompareDocumentVisualsWith, configurar ambas idénticas va por tu cuenta, y un desfase se manifiesta como cada página cambiada. Tampoco admite regiones a ignorar; acude a la API tipada cuando las necesites
Límites, coste y dónde encaja el diff estructural
PDFlibPas acota el trabajo de forma explícita para que una entrada hostil o enorme falle con error en lugar de agotar la memoria. Una página renderizada no puede pasar de 64 Mi píxeles, la rejilla de tiles tiene un tope de 4 Mi tiles y el alineamiento de páginas rechaza entradas donde la suma de páginas pasa de 8192 o la matriz LCS superaría 4 Mi celdas. La comparación de documentos además renderiza cada página dos veces, una para la huella y otra para la comparación del par, así que presupuesta eso a 150 DPI o más. Para la mayoría de suites de regresión, 72 a 96 DPI cazan cambios de maquetación y de texto en una fracción del tiempo
El diff visual y el diff estructural responden preguntas distintas, y una tubería seria normalmente quiere ambos. La comparación de píxeles te dice si un lector notaría algo. No puede decirte si un documento firmado ganó una anotación que se renderiza fuera de página, ni si una actualización incremental sustituyó en silencio el valor de un campo de formulario por el mismo texto visible. Para esa capa, clasificar qué cambió en un PDF después de firmarse recorre el diff a nivel de revisión. Y como la elección del motor gobierna los píxeles, el render multi-motor de PDF en Delphi merece lectura antes de fijar una baseline a un solo motor
FAQ
¿Estira la comparación visual de PDFlibPas las páginas que cambiaron de tamaño?
No. CompareRenderedPagesWith devuelve error para páginas de tamaño en píxeles distinto, y el informe de documento marca esos pares como size_changed con ambas dimensiones en lugar de compararlos
¿Por qué no se usan páginas casi idénticas como anclas?
Las anclas usan una huella SHA-256 exacta de los píxeles visibles más el tamaño de página, así que el alineamiento se mantiene predecible. La tolerancia se aplica después, al comparar las páginas emparejadas, así que las páginas de solo ruido acaban igualmente como unchanged
¿Puedo obtener una imagen overlay para cada página de un documento?
No desde la llamada de documento, que omite deliberadamente máscaras y overlays para mantener la memoria plana. Usa el JSON para encontrar las páginas cambiadas y llama luego a CompareRenderedPagesWith con IncludeOverlay sobre esas páginas
La comparación visual, el emparejado de páginas y el resto de las APIs de render y de documento vienen con la biblioteca PDF PDFlibPas para Delphi