Added regression test suite that validates each release candidate version against a barrage of different DIY files

This commit is contained in:
Bane Stojkovic
2022-03-02 18:52:52 +01:00
parent 5539195778
commit 2cbaaa4a7d
15 changed files with 1472 additions and 2 deletions
@@ -3731,7 +3731,7 @@
<minor>22</minor>
<build>0</build>
</versionNumber>
<releaseDate>2022-02-27 00:00:00.000 CET</releaseDate>
<releaseDate>2022-03-02 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
@@ -3744,12 +3744,16 @@
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Separate WIRING layer (containing Hookup Wire and Twisted Wire at the moment) from TRACE layer and render it above the COMPONENT layer by default</description>
<description>Separate WIRING layer (containing 'Hookup Wire' and 'Twisted Wire' at the moment) from TRACE layer and render it above the COMPONENT layer by default</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Upgrade OSX application package built-in Java to JRE 1.8.0_321-b07</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added regression test suite that validates each release candidate version against a barrage of different DIY files</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
Binary file not shown.
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/diylc-swing/test/org/diylc/RegressionTestRunner.java"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
</listAttribute>
<booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="org.diylc.RegressionTestRunner"/>
<stringAttribute key="org.eclipse.jdt.launching.MODULE_NAME" value="diylc-swing"/>
<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="&quot;E:\Bane\Dev\DIYLC\regression&quot; TEST"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="diylc-swing"/>
</launchConfiguration>
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,708 @@
package com.github.romankh3.image.comparison;
import com.github.romankh3.image.comparison.model.ExcludedAreas;
import com.github.romankh3.image.comparison.model.ImageComparisonResult;
import com.github.romankh3.image.comparison.model.ImageComparisonState;
import com.github.romankh3.image.comparison.model.Rectangle;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
import static com.github.romankh3.image.comparison.ImageComparisonUtil.getDifferencePercent;
import static java.util.Collections.emptyList;
/**
* Main class for comparison images.
*/
public class ImageComparison {
/**
* The threshold which means the max distance between non-equal pixels.
* Could be changed according to the size and requirements of the image.
*/
private int threshold = 5;
/**
* Expected image for comparison
*/
private final BufferedImage expected;
/**
* Actual image for comparison
*/
private final BufferedImage actual;
/**
* Width of the line that is drawn the rectangle
*/
private int rectangleLineWidth = 1;
/**
* {@link File} of the result destination.
*/
private /* @Nullable */ File destination;
/**
* The number which marks how many rectangles. Beginning from 2.
*/
private int counter = 2;
/**
* The number of the marking specific rectangle.
*/
private int regionCount = counter;
/**
* The number of the minimal rectangle size. Count as (width x height).
*/
private Integer minimalRectangleSize = 1;
/**
* Maximal count of the {@link Rectangle}s.
* It means that would get the first x biggest rectangles.
* Default value is -1, that means that all the rectangles would be drawn.
*/
private Integer maximalRectangleCount = -1;
/**
* Level of the pixel tolerance. By default, it's 0.1 -> 10% difference.
* The value can be set from 0.0 to 0.99.
*/
private double pixelToleranceLevel = 0.1;
/**
* Constant using for counting the level of the difference.
*/
private double differenceConstant;
/**
* Matrix YxX => int[y][x].
* E.g.:
* | X - width ----
* | .....................................
* Y . (0, 0) .
* | . .
* | . .
* h . .
* e . .
* i . .
* g . .
* h . .
* t . (X, Y).
* | .....................................
*/
private int[][] matrix;
/**
* ExcludedAreas contains a List of {@link Rectangle}s to be ignored when comparing images
*/
private ExcludedAreas excludedAreas = new ExcludedAreas();
/**
* Flag which says draw excluded rectangles or not.
*/
private boolean drawExcludedRectangles = false;
/**
* The difference in percent between two images.
*/
private float differencePercent;
/**
* Flag for filling comparison difference rectangles.
*/
private boolean fillDifferenceRectangles = false;
/**
* Sets the opacity percentage of the fill of comparison difference rectangles. 0.0 means completely transparent and 100.0 means completely opaque.
*/
private double percentOpacityDifferenceRectangles = 20.0;
/**
* Flag for filling excluded rectangles.
*/
private boolean fillExcludedRectangles = false;
/**
* Sets the opacity percentage of the fill of excluded rectangles. 0.0 means completely transparent and 100.0 means completely opaque.
*/
private double percentOpacityExcludedRectangles = 20.0;
/**
* The percent of the allowing pixels to be different to stay {@link ImageComparisonState#MATCH} for comparison.
* E.g. percent of the pixels, which would ignore in comparison.
*/
private double allowingPercentOfDifferentPixels = 0.0;
/**
* Sets rectangle color of image difference. By default, it's red.
*/
private Color differenceRectangleColor = Color.RED;
/**
* Sets rectangle color of excluded part. By default, it's green.
*/
private Color excludedRectangleColor = Color.GREEN;
/**
* Create a new instance of {@link ImageComparison} that can compare the given images.
*
* @param expected expected image to be compared
* @param actual actual image to be compared
*/
public ImageComparison(String expected, String actual) {
this(ImageComparisonUtil.readImageFromResources(expected),
ImageComparisonUtil.readImageFromResources(actual),
null);
}
/**
* Create a new instance of {@link ImageComparison} that can compare the given images.
*
* @param expected expected image to be compared
* @param actual actual image to be compared
* @param destination destination to save the result. If null, the result is shown in the UI.
*/
public ImageComparison(BufferedImage expected, BufferedImage actual, File destination) {
this.expected = expected;
this.actual = actual;
this.destination = destination;
differenceConstant = calculateDifferenceConstant();
}
/**
* Create a new instance of {@link ImageComparison} that can compare the given images.
*
* @param expected expected image to be compared
* @param actual actual image to be compared
*/
public ImageComparison(BufferedImage expected, BufferedImage actual) {
this(expected, actual, null);
}
/**
* Draw rectangles which cover the regions of the difference pixels.
*
* @return the result of the drawing.
*/
public ImageComparisonResult compareImages() {
// check that the images have the same size
if (isImageSizesNotEqual(expected, actual)) {
BufferedImage actualResized = ImageComparisonUtil.resize(actual, expected.getWidth(), expected.getHeight());
return ImageComparisonResult.defaultSizeMisMatchResult(expected, actual, getDifferencePercent(actualResized, expected));
}
List<Rectangle> rectangles = populateRectangles();
if (rectangles.isEmpty()) {
ImageComparisonResult matchResult = ImageComparisonResult.defaultMatchResult(expected, actual);
if (drawExcludedRectangles) {
matchResult.setResult(drawRectangles(rectangles));
saveImageForDestination(matchResult.getResult());
}
return matchResult;
}
BufferedImage resultImage = drawRectangles(rectangles);
saveImageForDestination(resultImage);
return ImageComparisonResult.defaultMisMatchResult(expected, actual, getDifferencePercent(actual, expected))
.setResult(resultImage)
.setRectangles(rectangles);
}
/**
* Check images for equals their widths and heights.
*
* @param expected {@link BufferedImage} object of the expected image.
* @param actual {@link BufferedImage} object of the actual image.
* @return true if image size are not equal, false otherwise.
*/
private boolean isImageSizesNotEqual(BufferedImage expected, BufferedImage actual) {
return expected.getHeight() != actual.getHeight() || expected.getWidth() != actual.getWidth();
}
/**
* Populate binary matrix with "0" and "1". If the pixels are different set it as "1", otherwise "0".
*
* @return the count of different pixels
*/
private long populateTheMatrixOfTheDifferences() {
long countOfDifferentPixels = 0;
matrix = new int[expected.getHeight()][expected.getWidth()];
for (int y = 0; y < expected.getHeight(); y++) {
for (int x = 0; x < expected.getWidth(); x++) {
if (!excludedAreas.contains(new Point(x, y))) {
if (isDifferentPixels(expected.getRGB(x, y), actual.getRGB(x, y))) {
matrix[y][x] = 1;
countOfDifferentPixels++;
}
}
}
}
return countOfDifferentPixels;
}
/**
* Say if the two pixels equal or not. The rule is the difference between two pixels
* need to be more than {@link #pixelToleranceLevel}.
*
* @param expectedRgb the RGB value of the Pixel of the Expected image.
* @param actualRgb the RGB value of the Pixel of the Actual image.
* @return {@code true} if they' are difference, {@code false} otherwise.
*/
private boolean isDifferentPixels(int expectedRgb, int actualRgb) {
if (expectedRgb == actualRgb) {
return false;
} else if (pixelToleranceLevel == 0.0) {
return true;
}
int red1 = (expectedRgb >> 16) & 0xff;
int green1 = (expectedRgb >> 8) & 0xff;
int blue1 = (expectedRgb) & 0xff;
int red2 = (actualRgb >> 16) & 0xff;
int green2 = (actualRgb >> 8) & 0xff;
int blue2 = (actualRgb) & 0xff;
return (Math.pow(red2 - red1, 2) + Math.pow(green2 - green1, 2) + Math.pow(blue2 - blue1, 2))
> differenceConstant;
}
/**
* Populate rectangles of the differences
*
* @return the collection of the populated {@link Rectangle} objects.
*/
private List<Rectangle> populateRectangles() {
long countOfDifferentPixels = populateTheMatrixOfTheDifferences();
if (countOfDifferentPixels == 0) {
return emptyList();
}
if (isAllowedPercentOfDifferentPixels(countOfDifferentPixels)) {
return emptyList();
}
groupRegions();
List<Rectangle> rectangles = new ArrayList<>();
while (counter <= regionCount) {
Rectangle rectangle = createRectangle();
if (!rectangle.equals(Rectangle.createDefault()) && rectangle.size() >= minimalRectangleSize) {
rectangles.add(rectangle);
}
counter++;
}
return mergeRectangles(mergeRectangles(rectangles));
}
/**
* Say if provided {@param countOfDifferentPixels} is allowed for {@link ImageComparisonState#MATCH} state.
*
* @param countOfDifferentPixels the count of the different pixels in comparison.
* @return true, if percent of different pixels lower or equal {@link ImageComparison#allowingPercentOfDifferentPixels},
* false - otherwise.
*/
private boolean isAllowedPercentOfDifferentPixels(long countOfDifferentPixels) {
long totalPixelCount = matrix.length * matrix[0].length;
double actualPercentOfDifferentPixels = ((double) countOfDifferentPixels / (double) totalPixelCount) * 100;
return actualPercentOfDifferentPixels <= allowingPercentOfDifferentPixels;
}
/**
* Create a {@link Rectangle} object.
*
* @return the {@link Rectangle} object.
*/
private Rectangle createRectangle() {
Rectangle rectangle = Rectangle.createDefault();
for (int y = 0; y < matrix.length; y++) {
for (int x = 0; x < matrix[0].length; x++) {
if (matrix[y][x] == counter) {
updateRectangleCreation(rectangle, x, y);
}
}
}
return rectangle;
}
/**
* Update {@link Point} of the rectangle based on x and y coordinates.
*/
private void updateRectangleCreation(Rectangle rectangle, int x, int y) {
if (x < rectangle.getMinPoint().getX()) {
rectangle.getMinPoint().x = x;
}
if (x > rectangle.getMaxPoint().getX()) {
rectangle.getMaxPoint().x = x;
}
if (y < rectangle.getMinPoint().getY()) {
rectangle.getMinPoint().y = y;
}
if (y > rectangle.getMaxPoint().getY()) {
rectangle.getMaxPoint().y = y;
}
}
/**
* Find overlapping rectangles and merge them.
*/
private List<Rectangle> mergeRectangles(List<Rectangle> rectangles) {
int position = 0;
while (position < rectangles.size()) {
if (rectangles.get(position).equals(Rectangle.createZero())) {
position++;
}
for (int i = 1 + position; i < rectangles.size(); i++) {
Rectangle r1 = rectangles.get(position);
Rectangle r2 = rectangles.get(i);
if (r2.equals(Rectangle.createZero())) {
continue;
}
if (r1.isOverlapping(r2)) {
rectangles.set(position, r1.merge(r2));
r2.makeZeroRectangle();
if (position != 0) {
position--;
}
}
}
position++;
}
return rectangles.stream().filter(it -> !it.equals(Rectangle.createZero())).collect(Collectors.toList());
}
/**
* Draw the rectangles based on collection of the rectangles and result image.
*
* @param rectangles the collection of the {@link Rectangle} objects.
* @return result {@link BufferedImage} with drawn rectangles.
*/
private BufferedImage drawRectangles(List<Rectangle> rectangles) {
BufferedImage resultImage = ImageComparisonUtil.deepCopy(actual);
Graphics2D graphics = preparedGraphics2D(resultImage);
drawExcludedRectangles(graphics);
drawRectanglesOfDifferences(rectangles, graphics);
return resultImage;
}
/**
* Draw excluded rectangles.
*
* @param graphics prepared {@link Graphics2D}object.
*/
private void drawExcludedRectangles(Graphics2D graphics) {
if (drawExcludedRectangles) {
graphics.setColor(this.excludedRectangleColor);
draw(graphics, excludedAreas.getExcluded());
if (fillExcludedRectangles) {
fillRectangles(graphics, excludedAreas.getExcluded(), percentOpacityExcludedRectangles);
}
}
}
/**
* Draw rectangles with the differences.
*
* @param rectangles the collection of the {@link Rectangle} of differences.
* @param graphics prepared {@link Graphics2D}object.
*/
private void drawRectanglesOfDifferences(List<Rectangle> rectangles, Graphics2D graphics) {
List<Rectangle> rectanglesForDraw;
graphics.setColor(this.differenceRectangleColor);
if (maximalRectangleCount > 0 && maximalRectangleCount < rectangles.size()) {
rectanglesForDraw = rectangles.stream()
.sorted(Comparator.comparing(Rectangle::size))
.skip(rectangles.size() - maximalRectangleCount)
.collect(Collectors.toList());
} else {
rectanglesForDraw = new ArrayList<>(rectangles);
}
draw(graphics, rectanglesForDraw);
if (fillDifferenceRectangles) {
fillRectangles(graphics, rectanglesForDraw, percentOpacityDifferenceRectangles);
}
}
/**
* Prepare {@link Graphics2D} based on resultImage and rectangleLineWidth
*
* @param image image based on created {@link Graphics2D}.
* @return prepared {@link Graphics2D} object.
*/
private Graphics2D preparedGraphics2D(BufferedImage image) {
Graphics2D graphics = image.createGraphics();
graphics.setStroke(new BasicStroke(rectangleLineWidth));
return graphics;
}
/**
* Save image to destination object if exists.
*
* @param image {@link BufferedImage} to be saved.
*/
private void saveImageForDestination(BufferedImage image) {
if (Objects.nonNull(destination)) {
ImageComparisonUtil.saveImage(destination, image);
}
}
/**
* Draw rectangles based on collection of the {@link Rectangle} and {@link Graphics2D}.
* getWidth/getHeight return real width/height,
* so need to draw rectangle on one px smaller because minpoint + width/height is point on excluded pixel.
*
* @param graphics the {@link Graphics2D} object for drawing.
* @param rectangles the collection of the {@link Rectangle}.
*/
private void draw(Graphics2D graphics, List<Rectangle> rectangles) {
rectangles.forEach(rectangle -> graphics.drawRect(
rectangle.getMinPoint().x,
rectangle.getMinPoint().y,
rectangle.getWidth() - 1,
rectangle.getHeight() - 1)
);
}
/**
* Fill rectangles based on collection of the {@link Rectangle} and {@link Graphics2D}.
* getWidth/getHeight return real width/height,
* so need to draw rectangle fill two px smaller to fit inside rectangle borders.
*
* @param graphics the {@link Graphics2D} object for drawing.
* @param rectangles rectangles the collection of the {@link Rectangle}.
* @param percentOpacity the opacity of the fill.
*/
private void fillRectangles(Graphics2D graphics, List<Rectangle> rectangles, double percentOpacity) {
graphics.setColor(new Color(graphics.getColor().getRed(),
graphics.getColor().getGreen(),
graphics.getColor().getBlue(),
(int) (percentOpacity / 100 * 255)
));
rectangles.forEach(rectangle -> graphics.fillRect(
rectangle.getMinPoint().x - 1,
rectangle.getMinPoint().y - 1,
rectangle.getWidth() - 2,
rectangle.getHeight() - 2)
);
}
/**
* Group rectangle regions in matrix.
*/
private void groupRegions() {
for (int y = 0; y < matrix.length; y++) {
for (int x = 0; x < matrix[y].length; x++) {
if (matrix[y][x] == 1) {
joinToRegion(x, y);
regionCount++;
}
}
}
}
/**
* The recursive method which go to all directions and finds difference
* in binary matrix using {@code threshold} for setting max distance between values which equal "1".
* and set the {@code groupCount} to matrix.
*
* @param x the value of the X-coordinate.
* @param y the value of the Y-coordinate.
*/
private void joinToRegion(int x, int y) {
if (isJumpRejected(x, y)) {
return;
}
matrix[y][x] = regionCount;
for (int i = 0; i < threshold; i++) {
joinToRegion(x + 1 + i, y);
joinToRegion(x, y + 1 + i);
joinToRegion(x + 1 + i, y - 1 - i);
joinToRegion(x - 1 - i, y + 1 + i);
joinToRegion(x + 1 + i, y + 1 + i);
}
}
/**
* Returns the list of rectangles that would be drawn as a diff image.
* If you submit two images that are the same barring the parts you want to excludedAreas you get a list of
* rectangles that can be used as said excludedAreas
*
* @return List of {@link Rectangle}
*/
public List<Rectangle> createMask() {
return populateRectangles();
}
/**
* Check next step valid or not.
*
* @param x X-coordinate of the image.
* @param y Y-coordinate of the image
* @return true if jump rejected, otherwise false.
*/
private boolean isJumpRejected(int x, int y) {
return y < 0 || y >= matrix.length || x < 0 || x >= matrix[y].length || matrix[y][x] != 1;
}
public double getPixelToleranceLevel() {
return pixelToleranceLevel;
}
public ImageComparison setPixelToleranceLevel(double pixelToleranceLevel) {
if (0.0 <= pixelToleranceLevel && pixelToleranceLevel < 1) {
this.pixelToleranceLevel = pixelToleranceLevel;
differenceConstant = calculateDifferenceConstant();
}
return this;
}
private double calculateDifferenceConstant() {
return Math.pow(pixelToleranceLevel * Math.sqrt(Math.pow(255, 2) * 3), 2);
}
public boolean isDrawExcludedRectangles() {
return drawExcludedRectangles;
}
public ImageComparison setDrawExcludedRectangles(boolean drawExcludedRectangles) {
this.drawExcludedRectangles = drawExcludedRectangles;
return this;
}
public int getThreshold() {
return threshold;
}
public ImageComparison setThreshold(int threshold) {
this.threshold = threshold;
return this;
}
public Optional<File> getDestination() {
return Optional.ofNullable(destination);
}
public ImageComparison setDestination(File destination) {
this.destination = destination;
return this;
}
public BufferedImage getExpected() {
return expected;
}
public BufferedImage getActual() {
return actual;
}
public int getRectangleLineWidth() {
return rectangleLineWidth;
}
public ImageComparison setRectangleLineWidth(int rectangleLineWidth) {
this.rectangleLineWidth = rectangleLineWidth;
return this;
}
public Integer getMinimalRectangleSize() {
return minimalRectangleSize;
}
public ImageComparison setMinimalRectangleSize(Integer minimalRectangleSize) {
this.minimalRectangleSize = minimalRectangleSize;
return this;
}
public Integer getMaximalRectangleCount() {
return maximalRectangleCount;
}
public ImageComparison setMaximalRectangleCount(Integer maximalRectangleCount) {
this.maximalRectangleCount = maximalRectangleCount;
return this;
}
public ImageComparison setExcludedAreas(List<Rectangle> excludedAreas) {
this.excludedAreas = new ExcludedAreas(excludedAreas);
return this;
}
public boolean isFillDifferenceRectangles() {
return this.fillDifferenceRectangles;
}
public double getPercentOpacityDifferenceRectangles() {
return this.percentOpacityDifferenceRectangles;
}
public ImageComparison setDifferenceRectangleFilling(boolean fillRectangles, double percentOpacity) {
this.fillDifferenceRectangles = fillRectangles;
this.percentOpacityDifferenceRectangles = percentOpacity;
return this;
}
public boolean isFillExcludedRectangles() {
return this.fillExcludedRectangles;
}
public double getPercentOpacityExcludedRectangles() {
return this.percentOpacityExcludedRectangles;
}
public ImageComparison setExcludedRectangleFilling(boolean fillRectangles, double percentOpacity) {
this.fillExcludedRectangles = fillRectangles;
this.percentOpacityExcludedRectangles = percentOpacity;
return this;
}
public double getAllowingPercentOfDifferentPixels() {
return allowingPercentOfDifferentPixels;
}
public ImageComparison setAllowingPercentOfDifferentPixels(double allowingPercentOfDifferentPixels) {
if (0.0 <= allowingPercentOfDifferentPixels && allowingPercentOfDifferentPixels <= 100) {
this.allowingPercentOfDifferentPixels = allowingPercentOfDifferentPixels;
} else {
//todo add warning here
}
return this;
}
public Color getDifferenceRectangleColor() {
return this.differenceRectangleColor;
}
public ImageComparison setDifferenceRectangleColor(Color differenceRectangleColor) {
this.differenceRectangleColor = differenceRectangleColor;
return this;
}
public Color getExcludedRectangleColor() {
return this.excludedRectangleColor;
}
public ImageComparison setExcludedRectangleColor(Color excludedRectangleColor) {
this.excludedRectangleColor = excludedRectangleColor;
return this;
}
}
@@ -0,0 +1,169 @@
package com.github.romankh3.image.comparison;
import com.github.romankh3.image.comparison.exception.ImageComparisonException;
import com.github.romankh3.image.comparison.exception.ImageNotFoundException;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
/**
* Tools for the {@link ImageComparison} object.
*/
public final class ImageComparisonUtil {
/**
* Make a copy of the {@link BufferedImage} object.
*
* @param image the provided image.
* @return copy of the provided image.
*/
static BufferedImage deepCopy(BufferedImage image) {
ColorModel cm = image.getColorModel();
boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
WritableRaster raster = image.copyData(image.getRaster().createCompatibleWritableRaster());
return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
}
/**
* Read image from the provided path.
*
* @param path the path where contains image.
* @return the {@link BufferedImage} object of this specific image.
* @throws ImageComparisonException due to read the image from resources.
*/
public static BufferedImage readImageFromResources(String path) throws ImageComparisonException {
File imageFile = new File(path);
if (imageFile.exists()) {
try {
return ImageIO.read(imageFile);
} catch (IOException e) {
throw new ImageComparisonException(String.format("Cannot read image from the file, path=%s", path), e);
}
} else {
InputStream inputStream = ImageComparisonUtil.class.getClassLoader().getResourceAsStream(path);
if (inputStream != null) {
try {
return ImageIO.read(inputStream);
} catch (IOException e) {
throw new ImageComparisonException(String.format("Cannot read image from the file, path=%s", path), e);
}
} else {
throw new ImageNotFoundException(String.format("Image with path = %s not found", path));
}
}
}
/**
* Save image to the provided path.
*
* @param pathFile the path to the saving image.
* @param image the {@link BufferedImage} object of this specific image.
* @throws ImageComparisonException due to save image.
*/
public static void saveImage(File pathFile, BufferedImage image) throws ImageComparisonException {
File dir = pathFile.getParentFile();
// make dir if it's not using from Gradle.
boolean dirExists = dir == null || dir.isDirectory() || dir.mkdirs();
if (!dirExists) {
throw new ImageComparisonException("Unable to create directory " + dir);
}
try {
ImageIO.write(image, "png", pathFile);
} catch (IOException e) {
throw new ImageComparisonException(
String.format("Cannot save image to path=%s", pathFile.getAbsolutePath()), e);
}
}
/**
* Resize image to new dimensions and return new image.
*
* @param img the object of the image to be resized.
* @param newW the new width.
* @param newH the new height.
* @return resized {@link BufferedImage} object.
*/
public static BufferedImage resize(BufferedImage img, int newW, int newH) {
return toBufferedImage(img.getScaledInstance(newW, newH, Image.SCALE_SMOOTH));
}
/**
* Convert image to buffered image.
*
* @param img the object of the image to be converted to buffered image.
* @return the converted buffered image.
*/
public static BufferedImage toBufferedImage(Image img) {
if (img instanceof BufferedImage) {
return (BufferedImage) img;
}
float softenFactor = 0.05f;
final Image temp = new ImageIcon(img).getImage();
final BufferedImage bufferedImage = new BufferedImage(
temp.getWidth(null),
temp.getHeight(null),
BufferedImage.TYPE_INT_RGB);
final Graphics g = bufferedImage.createGraphics();
g.setColor(Color.white);
g.fillRect(0, 0, temp.getWidth(null), temp.getHeight(null));
g.drawImage(temp, 0, 0, null);
g.dispose();
final float[] softenArray = {0, softenFactor, 0, softenFactor, 1 - (softenFactor * 4), softenFactor, 0,
softenFactor, 0};
final Kernel kernel = new Kernel(3, 3, softenArray);
final ConvolveOp cOp = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
return cOp.filter(bufferedImage, null);
}
/**
* Return the difference in percent between two buffered images.
*
* @param img1 the first image.
* @param img2 the second image.
* @return difference percent.
*/
public static float getDifferencePercent(BufferedImage img1, BufferedImage img2) {
int width = img1.getWidth();
int height = img1.getHeight();
long diff = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
diff += pixelDiff(img1.getRGB(x, y), img2.getRGB(x, y));
}
}
long maxDiff = 3L * 255 * width * height;
return (float) (100.0 * diff / maxDiff);
}
/**
* Compare two pixels
*
* @param rgb1 the first rgb
* @param rgb2 the second rgn
* @return the difference.
*/
public static int pixelDiff(int rgb1, int rgb2) {
int r1 = (rgb1 >> 16) & 0xff;
int g1 = (rgb1 >> 8) & 0xff;
int b1 = rgb1 & 0xff;
int r2 = (rgb2 >> 16) & 0xff;
int g2 = (rgb2 >> 8) & 0xff;
int b2 = rgb2 & 0xff;
return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2);
}
}
@@ -0,0 +1,32 @@
package com.github.romankh3.image.comparison.exception;
/**
* A {@link RuntimeException} that is thrown in case of an image comparison failures.
*/
public final class ImageComparisonException extends RuntimeException {
/**
* Constructs a new {@link ImageComparisonException} with the specified detail message.
* The cause is not initialized, and may subsequently be initialized by a call to
* Throwable.initCause(java.lang.Throwable).
*
* @param message the detail message. The detail message is saved for later retrieval by the Throwable.getMessage()
* method.
*/
public ImageComparisonException(String message) {
super(message);
}
/**
* Constructs a new {@link ImageComparisonException} with the specified detail message and cause.
* Note that the detail message associated with cause is not automatically incorporated in this {@link
* ImageComparisonException}'s detail message.
*
* @param message the detail message (which is saved for later retrieval by the Throwable.getMessage() method).
* @param throwable the cause (which is saved for later retrieval by the Throwable.getCause() method).
* (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
*/
public ImageComparisonException(String message, Throwable throwable) {
super(message, throwable);
}
}
@@ -0,0 +1,19 @@
package com.github.romankh3.image.comparison.exception;
/**
* {@link RuntimeException} that is thrown in case of an image getting failures.
*/
public class ImageNotFoundException extends RuntimeException {
/**
* Constructs a new {@link ImageNotFoundException} with the specified detail message.
* The cause is not initialized, and may subsequently be initialized by a call to
* Throwable.initCause(java.lang.Throwable).
*
* @param message the detail message. The detail message is saved for later retrieval by the Throwable.getMessage()
* method.
*/
public ImageNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,48 @@
package com.github.romankh3.image.comparison.model;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
/**
* The area that will be excluded, masked, in the image.
*/
public class ExcludedAreas {
/**
* The collection of the areas which would be excluded from the comparison.
*/
private final List<Rectangle> excluded;
/**
* Create empty instance of the {@link ExcludedAreas}.
*/
public ExcludedAreas() {
excluded = new ArrayList<>();
}
/**
* Create instance of the {@link ExcludedAreas} with provided {@link Rectangle} areas.
*
* @param excluded provided collection of the {@link Rectangle} objects.
*/
public ExcludedAreas(List<Rectangle> excluded) {
this.excluded = excluded;
}
/**
* Check if this {@link Point} contains in the {@link ExcludedAreas#excluded}
* collection of the {@link Rectangle}.
*
* @param point the {@link Point} object to be checked.
*
* @return {@code true} if this {@link Point} contains in areas from {@link ExcludedAreas#excluded}.
*/
public boolean contains(Point point) {
return excluded.stream().anyMatch(rectangle -> rectangle.containsPoint(point));
}
public List<Rectangle> getExcluded() {
return excluded;
}
}
@@ -0,0 +1,158 @@
package com.github.romankh3.image.comparison.model;
import com.github.romankh3.image.comparison.ImageComparisonUtil;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.List;
/**
* Data transfer objects which contains all the needed data for result of the comparison.
*/
public class ImageComparisonResult {
/**
* {@link BufferedImage} object of the expected.
*/
private BufferedImage expected;
/**
* {@link BufferedImage} object of the actual.
*/
private BufferedImage actual;
/**
* {@link BufferedImage} object of the comparison result.
*/
private BufferedImage result;
/**
* State of the comparison.
*/
private ImageComparisonState imageComparisonState;
/**
* The difference percentage between two images.
*/
private float differencePercent;
/**
* Rectangles of the differences
*/
private List<Rectangle> rectangles;
/**
* Create default instance of the {@link ImageComparisonResult} with {@link ImageComparisonState#SIZE_MISMATCH}.
*
* @param expected expected {@link BufferedImage} object.
* @param actual actual {@link BufferedImage} object.
* @param differencePercent the percent of the differences between images.
* @return instance of the {@link ImageComparisonResult} object.
*/
public static ImageComparisonResult defaultSizeMisMatchResult(BufferedImage expected, BufferedImage actual,
float differencePercent) {
return new ImageComparisonResult()
.setImageComparisonState(ImageComparisonState.SIZE_MISMATCH)
.setDifferencePercent(differencePercent)
.setExpected(expected)
.setActual(actual)
.setResult(actual);
}
/**
* Create default instance of the {@link ImageComparisonResult} with {@link ImageComparisonState#MISMATCH}.
*
* @param expected expected {@link BufferedImage} object.
* @param actual actual {@link BufferedImage} object.
* @param differencePercent the persent of the differences between images.
* @return instance of the {@link ImageComparisonResult} object.
*/
public static ImageComparisonResult defaultMisMatchResult(BufferedImage expected, BufferedImage actual, float differencePercent) {
return new ImageComparisonResult()
.setImageComparisonState(ImageComparisonState.MISMATCH)
.setDifferencePercent(differencePercent)
.setExpected(expected)
.setActual(actual)
.setResult(actual);
}
/**
* Create default instance of the {@link ImageComparisonResult} with {@link ImageComparisonState#MATCH}.
*
* @param expected expected {@link BufferedImage} object.
* @param actual actual {@link BufferedImage} object.
* @return instance of the {@link ImageComparisonResult} object.
*/
public static ImageComparisonResult defaultMatchResult(BufferedImage expected, BufferedImage actual) {
return new ImageComparisonResult()
.setImageComparisonState(ImageComparisonState.MATCH)
.setExpected(expected)
.setActual(actual)
.setResult(actual);
}
/**
* Save the image to the provided {@link File} object.
*
* @param file the provided {@link File} object.
* @return this {@link ImageComparisonResult} object.
*/
public ImageComparisonResult writeResultTo(File file) {
ImageComparisonUtil.saveImage(file, result);
return this;
}
public BufferedImage getExpected() {
return expected;
}
public ImageComparisonResult setExpected(BufferedImage expected) {
this.expected = expected;
return this;
}
public BufferedImage getActual() {
return actual;
}
public ImageComparisonResult setActual(BufferedImage actual) {
this.actual = actual;
return this;
}
public BufferedImage getResult() {
return result;
}
public ImageComparisonResult setResult(BufferedImage result) {
this.result = result;
return this;
}
public ImageComparisonState getImageComparisonState() {
return imageComparisonState;
}
public ImageComparisonResult setImageComparisonState(ImageComparisonState imageComparisonState) {
this.imageComparisonState = imageComparisonState;
return this;
}
public float getDifferencePercent() {
return differencePercent;
}
ImageComparisonResult setDifferencePercent(float differencePercent) {
this.differencePercent = differencePercent;
return this;
}
public List<Rectangle> getRectangles() {
return rectangles;
}
public ImageComparisonResult setRectangles(List<Rectangle> rectangles) {
this.rectangles = rectangles;
return this;
}
}
@@ -0,0 +1,23 @@
package com.github.romankh3.image.comparison.model;
/**
* Enum for telling the result of the Comparison.
* For example {@link ImageComparisonState#MATCH} - means that the images are equal.
*/
public enum ImageComparisonState {
/**
* Result state of the comparison, where mismatch of the image sizes.
*/
SIZE_MISMATCH,
/**
* Result state of the comparison, where mismatch of the images.
*/
MISMATCH,
/**
* Result state of the images, where images are equal, e.g. match.
*/
MATCH
}
@@ -0,0 +1,195 @@
package com.github.romankh3.image.comparison.model;
import static java.lang.Integer.max;
import static java.lang.Integer.min;
import java.awt.Point;
import java.util.Objects;
/**
* Object contained data for a rectangle.
*/
public class Rectangle {
/**
* Left Top {@link Point} on the {@link Rectangle}.
*/
private Point minPoint;
/**
* Right bottom {@link Point} on the {@link Rectangle}.
*/
private Point maxPoint;
/**
* Create empty instance of the {@link Rectangle}.
*/
private Rectangle() {
minPoint = new Point();
maxPoint = new Point();
}
/**
* Create clone object based on the provided {@link Rectangle}.
*
* @param rectangle provided {@link Rectangle} object.
*/
public Rectangle(Rectangle rectangle) {
this.minPoint = new Point(rectangle.getMinPoint().x, rectangle.getMinPoint().y);
this.maxPoint = new Point(rectangle.getMaxPoint().x, rectangle.getMaxPoint().y);
}
/**
* Create instance of the {@link Rectangle} based on the provided coordinates.
*
* @param minX minimal X-coordinate.
* @param minY minimal Y-coordinate.
* @param maxX maximal X-coordinate.
* @param maxY maximal Y-coordinate.
*/
public Rectangle(int minX, int minY, int maxX, int maxY) {
this.minPoint = new Point(minX, minY);
this.maxPoint = new Point(maxX, maxY);
}
/**
* Create default {@link Rectangle} object.
*
* @return default rectangle {@link Rectangle}.
*/
public static Rectangle createDefault() {
Rectangle defaultRectangle = new Rectangle();
defaultRectangle.setDefaultValues();
return defaultRectangle;
}
/**
* Create instance with zero points.
*
* @return created {@link Rectangle} instance.
*/
public static Rectangle createZero() {
Rectangle rectangle = new Rectangle();
rectangle.makeZeroRectangle();
return rectangle;
}
/**
* Create new {@link Rectangle} via merging this and that.
*
* @param that {@link Rectangle} for merging with this.
* @return new merged {@link Rectangle}.
*/
public Rectangle merge(Rectangle that) {
return new Rectangle(min(this.getMinPoint().x, that.getMinPoint().x),
min(this.getMinPoint().y, that.getMinPoint().y),
max(this.getMaxPoint().x, that.getMaxPoint().x),
max(this.getMaxPoint().y, that.getMaxPoint().y));
}
/**
* Check is that rectangle overlapp this.
*
* @param that {@link Rectangle} which checks with this.
* @return true if this over lapp that, false otherwise.
*/
public boolean isOverlapping(Rectangle that) {
if (this.getMaxPoint().getY() < that.getMinPoint().getY() ||
that.getMaxPoint().getY() < this.getMinPoint().getY()) {
return false;
}
return this.getMaxPoint().getX() >= that.getMinPoint().getX() &&
that.getMaxPoint().getX() >= this.getMinPoint().getX();
}
/**
* Set default values for rectangle.
*/
public void setDefaultValues() {
this.maxPoint = new Point(Integer.MIN_VALUE, Integer.MIN_VALUE);
this.minPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
/**
* Make zero rectagle.
*/
public void makeZeroRectangle() {
this.minPoint = new Point();
this.maxPoint = new Point();
}
/**
* Size of the {@link Rectangle}, counted as width x height.
*
* @return the size of the {@link Rectangle}.
*/
public Integer size() {
return getWidth() * getHeight();
}
/**
* Count the width of the {@link Rectangle}.
* Min and max point are included, so real width is +1px
*
* @return rectangle width.
*/
public int getWidth() {
return maxPoint.x - minPoint.x + 1;
}
/**
* Count the height of the {@link Rectangle}.
* Min and max point are included, so real width is +1px.
*
* @return rectangle height.
*/
public int getHeight() {
return maxPoint.y - minPoint.y + 1;
}
/**
* Check in the provided {@link Point} contains in the {@link Rectangle}.
*
* @param point provided {@link Point}.
* @return {@code true} if provided {@link Point} contains, {@code false} - otherwise.
*/
boolean containsPoint(Point point) {
return point.x >= minPoint.x && point.x<= maxPoint.x && point.y >= minPoint.y && point.y <= maxPoint.y;
}
public Point getMinPoint() {
return minPoint;
}
public void setMinPoint(Point minPoint) {
this.minPoint = minPoint;
}
public Point getMaxPoint() {
return maxPoint;
}
public void setMaxPoint(Point maxPoint) {
this.maxPoint = maxPoint;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Rectangle rectangle = (Rectangle) o;
return minPoint.equals(rectangle.minPoint) &&
maxPoint.equals(rectangle.maxPoint);
}
@Override
public int hashCode() {
return Objects.hash(minPoint, maxPoint);
}
}
@@ -0,0 +1,100 @@
package org.diylc;
import java.awt.image.BufferedImage;
import java.io.File;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
import org.diylc.core.IView;
import org.diylc.presenter.Presenter;
import org.diylc.swing.plugins.file.ProjectDrawingProvider;
import org.diylc.swingframework.export.DrawingExporter;
import com.github.romankh3.image.comparison.ImageComparison;
import com.github.romankh3.image.comparison.ImageComparisonUtil;
import com.github.romankh3.image.comparison.model.ImageComparisonResult;
import com.github.romankh3.image.comparison.model.ImageComparisonState;
public class RegressionTestRunner {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("No path and command provided");
return;
}
ConfigurationManager.getInstance().initialize("diylc-test");
String basePath = args[0];
String command = args[1];
System.out.println("Running command " + command + " in: " + basePath);
File inputsDir = new File(basePath + File.separator + "input" + File.separator + "diy");
if (!inputsDir.exists()) {
System.out.println("Could not find the input directory: " + inputsDir.getAbsolutePath());
return;
}
File[] files = inputsDir.listFiles();
int okCount = 0;
int failCount = 0;
for (File file : files) {
boolean fileOk = processFile(file, basePath, command);
if (fileOk) {
okCount++;
} else {
failCount++;
}
}
System.out.println("Finished! OK files: " + okCount + "; Failed files: " + failCount);
}
private static boolean processFile(File file, String basePath, String command) {
System.out.print("Processing file: " + file.getName() + "...");
try {
IView view = new MockView();
Presenter presenter = new Presenter(view, InMemoryConfigurationManager.getInstance());
ProjectDrawingProvider drawingProvider = new ProjectDrawingProvider(presenter, false, true, false);
presenter.loadProjectFromFile(file.getAbsolutePath());
if ("PREPARE".equalsIgnoreCase(command)) {
// generate PNG input image for comparison
File pngFile = new File(basePath + File.separator + "input" + File.separator + "png" + File.separator + (file.getName().replace(".diy", ".png")));
if (!pngFile.exists()) {
DrawingExporter.getInstance().exportPNG(drawingProvider, pngFile);
}
} else if ("TEST".equalsIgnoreCase(command)) {
// test save
File outputFile = new File(basePath + File.separator + "output" + File.separator + "diy" + File.separator + file.getName());
if (outputFile.exists()) {
outputFile.delete();
}
presenter.saveProjectToFile(outputFile.getAbsolutePath(), false);
// test export png
File pngOutputFile = new File(basePath + File.separator + "output" + File.separator + "png" + File.separator + (file.getName().replace(".diy", ".png")));
if (pngOutputFile.exists()) {
pngOutputFile.delete();
}
DrawingExporter.getInstance().exportPNG(drawingProvider, pngOutputFile);
File pngInputFile = new File(basePath + File.separator + "input" + File.separator + "png" + File.separator + (file.getName().replace(".diy", ".png")));
if (!pngInputFile.exists()) {
System.out.println("Input image does not exist!");
return false;
}
BufferedImage expectedImage = ImageComparisonUtil.readImageFromResources(pngInputFile.getAbsolutePath());
BufferedImage actualImage = ImageComparisonUtil.readImageFromResources(pngOutputFile.getAbsolutePath());
//Create ImageComparison object and compare the images.
ImageComparisonResult imageComparisonResult = new ImageComparison(expectedImage, actualImage).compareImages();
//Check the result
if (ImageComparisonState.MATCH != imageComparisonResult.getImageComparisonState()) {
System.out.println("Images do not match!");
return false;
}
}
System.out.println("OK");
return true;
} catch (Exception e) {
System.out.println("ERROR: " + e.getMessage());
return false;
}
}
}
Binary file not shown.