org.mapfish.print.test.util.ImageSimilarity Maven / Gradle / Ivy
Show all versions of print-lib Show documentation
package org.mapfish.print.test.util;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.export.JRGraphics2DExporter;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleGraphics2DExporterOutput;
import net.sf.jasperreports.export.SimpleGraphics2DReportConfiguration;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.mapfish.print.SvgUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.Iterator;
import java.util.List;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import javax.media.jai.iterator.RandomIter;
import javax.media.jai.iterator.RandomIterFactory;
/**
* Class for comparing an expected image to an actual image.
*
* CHECKSTYLE:OFF
*/
public final class ImageSimilarity {
private static final Logger LOGGER = LoggerFactory.getLogger(ImageSimilarity.class);
private static final boolean GENERATE_IN_SOURCE = true;
private final BufferedImage expectedImage;
private final BufferedImage maskImage;
private final BufferedImage diffImage;
private final File expectedPath;
/**
* The constructor, which creates the GUI and start the image processing task.
*/
public ImageSimilarity(final File expectedFile) throws IOException {
this.expectedImage = expectedFile.exists() ? ImageIO.read(expectedFile) : null;
if (GENERATE_IN_SOURCE) {
this.expectedPath = new File(expectedFile.toString().replaceAll(
"/out/", "/src/").replaceAll("/build/classes/test/", "/src/test/resources/"));
} else {
this.expectedPath = expectedFile;
}
final File maskFile = getRelatedFile("mask");
if (maskFile.exists()) {
this.maskImage = ImageIO.read(maskFile);
assert this.maskImage.getSampleModel().getNumBands() == 1;
} else {
this.maskImage = new BufferedImage(
this.expectedImage.getWidth(), this.expectedImage.getHeight(),
BufferedImage.TYPE_BYTE_GRAY);
final Graphics2D graphics = this.maskImage.createGraphics();
try {
graphics.setBackground(new Color(255, 255, 255));
graphics.clearRect(0, 0, this.expectedImage.getWidth(), this.expectedImage.getHeight());
} finally {
graphics.dispose();
}
}
this.diffImage = new BufferedImage(
this.expectedImage.getWidth(), this.expectedImage.getHeight(), BufferedImage.TYPE_INT_RGB);
}
/**
* Write the image to a file in uncompressed tiff format.
*
* @param image image to write
* @param file path and file name (extension will be ignored and changed to tiff.
*/
private static void writeUncompressedImage(BufferedImage image, String file) throws IOException {
try {
final File parentFile = new File(file).getParentFile();
Iterator writers = ImageIO.getImageWritersBySuffix("png");
final ImageWriter next = writers.next();
final ImageWriteParam param = next.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_DISABLED);
final File outputFile = new File(parentFile, FilenameUtils.getBaseName(file) + ".png");
try (FileImageOutputStream out = new FileImageOutputStream(outputFile)) {
next.setOutput(out);
next.write(image);
}
} catch (Throwable e) {
System.err.println(String.format(
"Error writing the image generated by the test: %s%n\t", file));
e.printStackTrace();
}
}
/**
* Merges a list of graphic files into a single graphic.
*
* @param graphicFiles a list of graphic files
* @param width the graphic width (required for svg files)
* @param height the graphic height (required for svg files)
* @return a single graphic
*/
public static BufferedImage mergeImages(List graphicFiles, int width, int height)
throws IOException, TranscoderException {
if (graphicFiles.isEmpty()) {
throw new IllegalArgumentException("no graphics given");
}
BufferedImage mergedImage = loadGraphic(graphicFiles.get(0), width, height);
Graphics g = mergedImage.getGraphics();
for (int i = 1; i < graphicFiles.size(); i++) {
BufferedImage image = loadGraphic(graphicFiles.get(i), width, height);
g.drawImage(image, 0, 0, null);
}
g.dispose();
return mergedImage;
}
private static BufferedImage loadGraphic(final URI path, final int width, final int height)
throws IOException, TranscoderException {
File file = new File(path);
if (file.getName().endsWith(".svg")) {
return convertFromSvg(path, width, height);
} else {
BufferedImage originalImage = ImageIO.read(file);
BufferedImage resizedImage = new BufferedImage(width, height, originalImage.getType());
Graphics2D g = resizedImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.drawImage(originalImage, 0, 0, width, height, null);
g.dispose();
return resizedImage;
}
}
/**
* Renders an SVG image into a {@link BufferedImage}.
*/
public static BufferedImage convertFromSvg(final URI svgFile, final int width, final int height)
throws TranscoderException {
return SvgUtil.convertFromSvg(svgFile, width, height);
}
/**
* Exports a rendered {@link JasperPrint} to a {@link BufferedImage}.
*/
public static BufferedImage exportReportToImage(final JasperPrint jasperPrint, final Integer page)
throws JRException {
BufferedImage pageImage = new BufferedImage(jasperPrint.getPageWidth(), jasperPrint.getPageHeight(),
BufferedImage.TYPE_INT_RGB);
JRGraphics2DExporter exporter = new JRGraphics2DExporter();
exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
SimpleGraphics2DExporterOutput output = new SimpleGraphics2DExporterOutput();
output.setGraphics2D((Graphics2D) pageImage.getGraphics());
exporter.setExporterOutput(output);
SimpleGraphics2DReportConfiguration configuration = new SimpleGraphics2DReportConfiguration();
configuration.setPageIndex(page);
exporter.setConfiguration(configuration);
exporter.exportReport();
return pageImage;
}
public static void main(final String args[]) throws IOException {
final String path = "core/src/test/resources/map-data";
final File root = new File(path);
final Iterable files = FileUtils.listFiles(root, new String[]{"png"}, true);
for (File file: files) {
final BufferedImage img = ImageIO.read(file);
writeUncompressedImage(img, file.getAbsolutePath());
}
}
private File getRelatedFile(final String name) {
final String expectedFileName = this.expectedPath.getName();
return new File(this.expectedPath.getParentFile(),
(expectedFileName.contains("expected") ?
expectedFileName.replace("expected", name) :
name + "-" + expectedFileName));
}
/**
* This method calculates the distance between the signatures of an image and the reference one. The
* signatures for the image passed as the parameter are calculated inside the method.
*
* @return a number between 0 and 10000 or Double.MAX_VALUE on images format error.
*/
private double calcDistance(final BufferedImage actual) {
// There are several ways to calculate distances between two vectors,
// we will calculate the sum of the distances between the RGB values of
// pixels in the same positions.
if (actual.getWidth() != this.expectedImage.getWidth()) {
LOGGER.error("Not the same width (expected: {}, actual: {})",
this.expectedImage.getWidth(), actual.getWidth());
return Double.MAX_VALUE;
}
if (actual.getHeight() != this.expectedImage.getHeight()) {
LOGGER.error("Not the same height (expected: {}, actual: {})",
this.expectedImage.getHeight(), actual.getHeight());
return Double.MAX_VALUE;
}
if (actual.getSampleModel().getNumBands() != this.expectedImage.getSampleModel().getNumBands()) {
LOGGER.error("Not the same number of bands (expected: {}, actual: {})",
this.expectedImage.getSampleModel().getNumBands(),
actual.getSampleModel().getNumBands());
return Double.MAX_VALUE;
}
double dist = 0;
double[] expectedPixel = new double[this.expectedImage.getSampleModel().getNumBands()];
double[] actualPixel = new double[this.expectedImage.getSampleModel().getNumBands()];
int[] maskPixel = new int[1];
RandomIter expectedIterator = RandomIterFactory.create(this.expectedImage, null);
RandomIter actualIterator = RandomIterFactory.create(actual, null);
RandomIter maskIterator = RandomIterFactory.create(this.maskImage, null);
Graphics2D diffGraphics = this.diffImage.createGraphics();
for (int x = 0; x < actual.getWidth(); x++) {
for (int y = 0; y < actual.getHeight(); y++) {
expectedIterator.getPixel(x, y, expectedPixel);
actualIterator.getPixel(x, y, actualPixel);
maskIterator.getPixel(x, y, maskPixel);
double squareDist = 0.0;
for (int i = 0; i < this.expectedImage.getSampleModel().getNumBands(); i++) {
double colorDist = (expectedPixel[i] - actualPixel[i]) * (maskPixel[0] / 255.0);
if (colorDist >
7.0) { // allow a small color change (JPEG compression, anti-aliasing, ...)
squareDist += colorDist * colorDist;
}
}
double pxDiff = Math.sqrt(squareDist) /
Math.sqrt(this.expectedImage.getSampleModel().getNumBands());
dist += pxDiff / 255;
diffGraphics.setColor(new Color((int) Math.round(pxDiff), 0, 0));
diffGraphics.drawRect(x, y, 1, 1);
}
}
diffGraphics.dispose();
// Normalise
dist = dist / this.expectedImage.getWidth() / this.expectedImage.getHeight() * 10000;
LOGGER.debug("Current distance: {}", dist);
return dist;
}
/**
* Check that the actual image and the image calculated by this object are within the given distance.
*
* @param actual the image to compare to "this" image.
*/
public void assertSimilarity(final File actual) throws IOException {
assertSimilarity(actual, 1);
}
/**
* Check that the actual image and the image calculated by this object are within the given distance.
*
* @param maxDistance the maximum distance between the two images.
*/
public void assertSimilarity(
final byte[] graphicData, final double maxDistance)
throws IOException {
assertSimilarity(ImageIO.read(new ByteArrayInputStream(graphicData)), maxDistance);
}
/**
* Check that the actual image and the image calculated by this object are within the given distance.
*
* @param graphicFiles a list of graphic files
* @param width the graphic width (required for svg files)
* @param height the graphic height (required for svg files)
* @param maxDistance the maximum distance between the two images.
*/
public void assertSimilarity(
final List graphicFiles, final int width, final int height, final double maxDistance)
throws IOException, TranscoderException {
assertSimilarity(mergeImages(graphicFiles, width, height), maxDistance);
}
/**
* Check that the actual image and the image calculated by this object are within the given distance.
*
* @param maxDistance the maximum distance between the two images.
*/
public void assertSimilarity(
final URI svgFile, final int width, final int height, final double maxDistance)
throws IOException, TranscoderException {
assertSimilarity(convertFromSvg(svgFile, width, height), maxDistance);
}
/**
* Check that the actual image and the image calculated by this object are within the given distance.
*
* @param maxDistance the maximum distance between the two images.
*/
public void assertSimilarity(
final JasperPrint jasperPrint, final Integer page, final double maxDistance)
throws IOException, JRException {
assertSimilarity(exportReportToImage(jasperPrint, page), maxDistance);
}
/**
* Check that the actual image and the image calculated by this object are within the given distance.
*
* @param actualFile the file to compare to "this" image.
* @param maxDistance the maximum distance between the two images.
*/
public void assertSimilarity(final File actualFile, final double maxDistance) throws IOException {
assertSimilarity(ImageIO.read(actualFile), maxDistance);
}
/**
* Check that the actual image and the image calculated by this object are within the given distance.
*
* @param actualImage the image to compare to "this" image.
* @param maxDistance the maximum distance between the two images.
*/
public void assertSimilarity(
final BufferedImage actualImage, final double maxDistance)
throws IOException {
if (!this.expectedPath.exists()) {
ImageIO.write(actualImage, "png", expectedPath);
throw new AssertionError("The expected file was missing and has been generated: " +
expectedPath.getAbsolutePath());
}
final double distance = calcDistance(actualImage);
if (distance > maxDistance) {
final File actualOutput = getRelatedFile("actual");
ImageIO.write(actualImage, "png", actualOutput);
final File diffOutput = getRelatedFile("diff");
ImageIO.write(this.diffImage, "png", diffOutput);
throw new AssertionError(String.format("similarity difference between images is: %s which is " +
"greater than the max distance of %s%n" +
"actual=%s%n" +
"expected=%s", distance, maxDistance,
actualOutput.getAbsolutePath(),
this.expectedPath.getAbsolutePath()));
}
}
}