ColorDiff.java
package io.github.giulong.spectrum.utils.visual_regression;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.File;
import java.nio.file.Path;
import javax.imageio.ImageIO;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.github.giulong.spectrum.interfaces.JsonSchemaTypes;
import io.github.giulong.spectrum.utils.FileUtils;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Getter
public abstract class ColorDiff extends ImageDiff {
@JsonIgnore
private final FileUtils fileUtils = FileUtils.getInstance();
@SuppressWarnings("unused")
@JsonSchemaTypes(String.class)
@JsonPropertyDescription("RGB color used to highlight changed pixels. Must be prefixed by a #, such as in '#ff0000'")
private Color color;
@SuppressWarnings("unused")
@JsonPropertyDescription("Number of pixels under which differences are ignored")
private int threshold;
protected abstract void apply(BufferedImage diff, int i, int j, int rgb, int[][] referencePixels, int[][] regressionPixels);
@Override
@SneakyThrows
public Result buildBetween(final Path reference, final Path regression, final Path destination, final String name) {
log.debug("Building diff between {} and {}", reference, regression);
final BufferedImage referenceImage = ImageIO.read(reference.toFile());
final BufferedImage regressionImage = ImageIO.read(regression.toFile());
final int width = referenceImage.getWidth();
final int height = referenceImage.getHeight();
final int regressionWidth = regressionImage.getWidth();
final int regressionHeight = regressionImage.getHeight();
if (width != regressionWidth || height != regressionHeight) {
log.warn("Snapshot reference is {}x{}, while current screenshot is {}x{}. They have different sizes. Cannot compare them.",
width, height, regressionWidth, regressionHeight);
return Result.builder().build();
}
final int[][] referencePixels = getPixelMatrixOf(referenceImage, width, height);
final int[][] regressionPixels = getPixelMatrixOf(regressionImage, width, height);
final int rgb = color.getRGB();
int count = 0;
for (int i = 0; i < referencePixels.length; i++) {
for (int j = 0; j < referencePixels[i].length; j++) {
if (referencePixels[i][j] != regressionPixels[i][j]) {
apply(referenceImage, j, i, rgb, referencePixels, regressionPixels);
count++;
}
}
}
log.debug("Images {} and {} differ by {} pixels", reference, regression, count);
if (count <= threshold) {
log.debug("Images {} and {} differ by {} pixels, below or equal to the threshold of {}", reference, regression, count, threshold);
return Result
.builder()
.regressionConfirmed(false)
.build();
}
final Path fullDestination = destination.resolve(name);
final File destinationFile = fullDestination.toFile();
log.debug("Writing diff between {} and {} at {}", reference, regression, destinationFile);
ImageIO.write(referenceImage, fileUtils.getExtensionOf(name), destinationFile);
return Result
.builder()
.path(fullDestination)
.build();
}
int[][] getPixelMatrixOf(final BufferedImage image, final int width, final int height) {
final byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
final int[][] result = new int[height][width];
if (image.getAlphaRaster() != null) {
for (int i = 0, j = 0, k = 0; k < pixels.length;) {
result[i][j] = pixels[k++] & 0xff; // blue
result[i][j] += (pixels[k++] & 0xff) << 8; // green
result[i][j] += (pixels[k++] & 0xff) << 16; // red
result[i][j] += (pixels[k++] & 0xff) << 24; // alpha
if (++j == width) {
j = 0;
i++;
}
}
} else {
for (int i = 0, j = 0, k = 0; k < pixels.length;) {
result[i][j] = pixels[k++] & 0xff; // blue
result[i][j] += (pixels[k++] & 0xff) << 8; // green
result[i][j] += (pixels[k++] & 0xff) << 16; // red
result[i][j] -= 16777216; // 255 alpha
if (++j == width) {
j = 0;
i++;
}
}
}
return result;
}
}