La comparación visual de PDFlibPas renderiza dos PDF al mismo DPI y hace diff de lo que un lector realmente ve. 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 por página, detectan páginas insertadas y eliminadas, y reportan todo como JSON
El caso de uso es la prueba de regresión visual. Alguien reescribe la plantilla de facturas, actualiza el paquete de fuentes o cambia el motor de render, y la pregunta no es si los bytes cambiaron (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 marcaría. Los píxeles son la única capa donde "se ve igual" es verificable
¿Qué compara realmente un diff visual de PDF?
PDFlibPas compara el resultado visible sobre una página blanca, no los contenidos crudos del buffer. Cada página se renderiza vía RenderPageToString como un BMP sin compresión, se decodifica a un bitmap top-down, y cualquier píxel que cargue alfa se compone sobre blanco antes de comparar los canales. Un bitmap RGB y un bitmap RGBA que se ven idénticos en pantalla por lo tanto comparan como idénticos, que es exactamente lo que usted quiere cuando los dos lados vinieron de rutas de render distintas
El decodificador BMP tiene una peculiaridad deliberada que vale conocer. Un bitmap de 32 bits BI_RGB carga un cuarto byte por píxel, pero el formato deja ese byte indefinido: algunos escritores lo llenan con 0, otros con 255, otros con sobras. PLDecodeVisualBMP lo ignora y conserva el color. Tratar ese byte como opacidad permitiría que un renderizador que escribe ceros borre cada píxel de la página, y el diff felizmente reportaría dos documentos en blanco e idénticos. Si usted alimenta sus propios bitmaps a la API de más bajo nivel, el alfa solo se honra cuando usted mismo declara el formato como vpfRGBA o vpfBGRA
Un píxel cuenta como cambiado cuando cualquiera de sus canales rojo, verde o azul difiere por más de ColorTolerance. Tolerancia 0 significa bit a bit exacto. En la práctica un valor de un dígito absorbe el jitter de anti-aliasing entre builds del renderizador sin ocultar un glifo cambiado, porque un cambio real de texto mueve los canales por mucho más que eso. Elija el número a partir de una corrida de línea base confiable, no adivinando
Comparar un solo par de páginas con CompareRenderedPagesWith
TPDFlib.CompareRenderedPagesWith es la herramienta cuando ya sabe qué páginas van juntas y quiere detalle: un bounding box, regiones de cambio conectadas, una máscara opcional por píxel y un overlay opcional teñido de rojo. El record de opciones y los tipos de resultado viven en la unidad PDFlibVisualCompare, y PLDefaultVisualCompareOptions le 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;
// Enmascare la marca de tiempo 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 están en píxeles renderizados con origen arriba a la izquierda e intervalos medio abiertos, así que Right y Bottom son exclusivos y el mismo rectángulo cubre una parte distinta de la página si cambia el DPI. Los rectángulos a ignorar que se traslapan se cuentan una vez, y los píxeles ignorados aparecen en IgnoredPixels en lugar de desaparecer, lo que mantiene honesto a ChangedRatio porque divide solo entre píxeles comparados. Por último, la llamada de una sola página exige que ambos renders tengan las mismas dimensiones; una página que cambió de tamaño falla con un 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 conteo de píxeles. Tiles más grandes dan menos regiones, más gruesas, que suele ser lo que un reporte de pruebas quiere. El overlay es una copia RGB de la segunda página con los píxeles cambiados teñidos de rojo, devuelta como bytes crudos en Comparison.Overlay; escribirlo a un PNG para sus artefactos de CI es código suyo, no de la biblioteca
¿Cómo maneja el emparejamiento a nivel documento las páginas insertadas?
TPDFlib.CompareDocumentVisualsWith empareja páginas con una subsecuencia común más larga sobre fingerprints exactos de píxeles, de modo que una página insertada no convierta cada página siguiente en un falso cambio. Un bucle ingenuo de página 1 con página 1, página 2 con página 2 es inútil en el momento en que una plantilla gana una portada: cada página después de la inserción queda desplazada en uno y el reporte se enciende en rojo de arriba abajo
Cada página se renderiza y se reduce a una llave compuesta por 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 aparenta: una página vertical toda blanca y una página horizontal toda blanca de la misma área tienen buffers de píxeles byte a byte idénticos, y sin las dimensiones en la llave se anclarían la una a la otra. Las llaves 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 carga más páginas de un lado, el sobrante se reporta como added o removed
var
Options: TPDFVisualCompareOptions;
Report: WideString;
begin
// Baseline y Candidate cargados como antes, con settings 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 bit a bit exactas, y eso es una decisión de diseño y no un descuido. ColorTolerance aplica solo a la comparación de páginas emparejadas, nunca al fingerprint. Una página con nada más que ruido de anti-aliasing no anclará, pero aterriza en un hueco, se empareja en orden con su contraparte y luego compara como unchanged bajo tolerancia. Unas anclas difusas serían más indulgentes y mucho menos predecibles, porque páginas casi duplicadas (separadores en blanco, páginas de términos repetidas) empezarían a emparejarse entre sí
Sea claro sobre qué es este alineamiento. El JSON lo llama exact-raster-lcs-gap-order a propósito: es un alineamiento de raster, no un reconocimiento semántico de páginas. Si un hueco contiene una página reescrita y una página insertada, el emparejamiento 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 emparejamiento importa para un veredicto, mire los pares, no solo los contadores
Leer el informe JSON
El reporte de documento tiene una forma fija sobre la que puede hacer aserciones en un test runner. En éxito carga "status":"complete", "complete":true, el nombre del alineamiento, firstPageCount, secondPageCount y los contadores changedPages, addedPages, removedPages y unchangedPages. El arreglo pairs luego lista cada fila con firstPage, secondPage (0 para el lado faltante) 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 tiene comparison en absoluto; reporta firstWidth, firstHeight, secondWidth y secondHeight y cuenta hacia 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 detiene ahí
El reporte de documento nunca contiene máscaras ni overlays. CompareDocumentVisualsWith fuerza IncludeMask y IncludeOverlay a off aunque sus opciones los pidan, de modo que la memoria se mantiene plana a medida que crece el conteo de páginas en lugar de sostener una vista previa de página completa por cada par. Use el reporte para encontrar las páginas interesantes, luego llame CompareRenderedPagesWith solo sobre esas páginas para obtener el overlay. Las fallas regresan 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 sola llamada para CI: carga el otro PDF, copia los settings actuales del renderizador sobre él y devuelve el mismo JSON de documento. La firma es CompareFileVisuals(OtherFileName, Password, DPI, ColorTolerance), donde la tolerancia debe ser de 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 renderizador es la razón para preferir esta llamada en lugar de cargar el segundo archivo usted mismo. El documento cargado toma el render scale, el crop type y los settings gráficos de la instancia actual, más la elección de motor Cairo o PDFium cuando esos están compilados. Si usted arma dos instancias de TPDFlib a mano para CompareDocumentVisualsWith, configurar ambas de forma idéntica es cosa suya, y un desajuste aparece como cada página cambiada. Tampoco puede recibir regiones a ignorar; acuda a la API tipada cuando las necesite
Límites, costo y dónde encaja el diff estructural
PDFlibPas acota el trabajo explícitamente para que una entrada hostil o enorme falle con un error en lugar de agotar la memoria. Una página renderizada no puede exceder 64 Mi de píxeles, la cuadrícula de tiles se limita a 4 Mi de tiles, y el alineamiento de páginas rechaza entradas donde la suma de los dos conteos de páginas pasa de 8192 o donde la matriz LCS excedería 4 Mi de celdas. La comparación de documentos además renderiza cada página dos veces, una para el fingerprint y otra para la comparación del par, así que presupueste eso a 150 DPI o más. Para la mayoría de las suites de regresión, 72 a 96 DPI atrapa cambios de layout y de texto en una fracción del tiempo
El diff visual y el diff estructural responden preguntas distintas, y un pipeline serio usualmente quiere ambos. La comparación de píxeles le dice si un lector notaría algo. No puede decirle si un documento firmado ganó una anotación que renderiza fuera de página, o si una actualización incremental reemplazó silenciosamente el valor de un campo de formulario con el mismo texto visible. Para esa capa, clasificar qué cambió en un PDF después de que fue firmado recorre el diff a nivel de revisión. Y como la elección del renderizador maneja los píxeles, el render multi-motor de PDF en Delphi vale la pena leerlo antes de fijar una línea base a un solo motor
Preguntas frecuentes
¿La comparación visual de PDFlibPas estira las páginas que cambiaron de tamaño?
No. CompareRenderedPagesWith devuelve un error para páginas de tamaño de píxel distinto, y el reporte 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 un fingerprint SHA-256 exacto 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, cuando se comparan las páginas emparejadas, así que las páginas de solo ruido terminan en 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. Use el JSON para encontrar las páginas cambiadas, luego llame CompareRenderedPagesWith con IncludeOverlay sobre esas páginas
La comparación visual, el emparejamiento de páginas y el resto de las APIs de render y de documento vienen con la biblioteca PDF PDFlibPas para Delphi