/*
* Created on Jun 7, 2007
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* Copyright @2007-2013 the original author or authors.
*/
package org.fest.assertions;
import org.fest.util.VisibleForTesting;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import static org.fest.assertions.ErrorMessages.unexpectedEqual;
import static org.fest.assertions.ErrorMessages.unexpectedNotEqual;
import static org.fest.assertions.Fail.comparisonFailed;
import static org.fest.assertions.Fail.failWithMessage;
import static org.fest.assertions.Formatting.format;
import static org.fest.assertions.Threshold.threshold;
import static org.fest.util.Objects.areEqual;
import static org.fest.util.Preconditions.checkNotNull;
/**
* Assertions for {@code BufferedImage}s.
* <p/>
* To create a new instance of this class invoke {@link Assertions#assertThat(BufferedImage)}.
*
* @author Yvonne Wang
* @author Alex Ruiz
* @author Ansgar Konermann
*/
public class ImageAssert extends GenericAssert<ImageAssert, BufferedImage> {
private static final Threshold ZERO_THRESHOLD = threshold(0);
private static ImageReader imageReader = new ImageReader();
/**
* Creates a new {@link ImageAssert}.
*
* @param actual the target to verify.
*/
protected ImageAssert(@Nullable BufferedImage actual) {
super(ImageAssert.class, actual);
}
/**
* Reads the image in the specified path.
*
* @param imageFilePath the path of the image to read.
* @return the read image.
* @throws NullPointerException if the given path is {@code null}.
* @throws IllegalArgumentException if the given path does not belong to a file.
* @throws IOException if any I/O error occurred while reading the image.
*/
public static @Nullable BufferedImage read(@Nonnull String imageFilePath) throws IOException {
checkNotNull(imageFilePath);
File imageFile = new File(imageFilePath);
if (!imageFile.isFile()) {
throw new IllegalArgumentException(format("The path <%s> does not belong to a file", imageFilePath));
}
return imageReader.read(imageFile);
}
private static @Nullable Dimension sizeOf(@Nullable BufferedImage image) {
if (image == null) {
return null;
}
return new Dimension(image.getWidth(), image.getHeight());
}
@VisibleForTesting
static void imageReader(@Nonnull ImageReader newImageReader) {
imageReader = newImageReader;
}
/**
* Verifies that the actual image is equal to the given one. Two images are equal if they have the same size and the
* pixels at the same coordinates have the same color.
*
* @param expected the given image to compare the actual image to.
* @return this assertion object.
* @throws AssertionError if the actual image is not equal to the given one.
*/
@Override
public @Nonnull ImageAssert isEqualTo(@Nullable BufferedImage expected) {
return isEqualTo(expected, ZERO_THRESHOLD);
}
/**
* Verifies that the actual image is equal to the given one. Two images are equal if:
* <ol>
* <li>they have the same size</li>
* <li>the difference between the RGB values of the color of each pixel is less than or equal to the given threshold</li>
* </ol>
*
* @param expected the given image to compare the actual image to.
* @param threshold the threshold to use to decide if the color of two pixels are similar: two pixels that are
* identical to the human eye may still have slightly different color values. For example, by using a
* threshold of 1 we can indicate that a blue value of 60 is similar to a blue value of 61.
* @return this assertion object.
* @throws NullPointerException if {@code threshold} is {@code null}.
* @throws AssertionError if the actual image is not equal to the given one.
* @since 1.1
*/
public @Nonnull ImageAssert isEqualTo(@Nullable BufferedImage expected, @Nonnull Threshold threshold) {
checkNotNull(threshold);
if (areEqual(actual, expected)) {
return this;
}
failIfNull(expected);
failIfNotEqual(sizeOf(actual), sizeOf(expected));
failIfNotEqualColor(checkNotNull(expected), threshold);
return this;
}
private void failIfNull(@Nullable BufferedImage expected) {
if (expected != null) {
return;
}
failIfCustomMessageIsSet();
fail(unexpectedNotEqual(actual, null));
}
private void failIfNotEqual(@Nullable Dimension a, @Nullable Dimension e) {
if (areEqual(a, e)) {
return;
}
failIfCustomMessageIsSet();
fail(format("image size: expected:<%s> but was:<%s>", new DimensionFormatter(e), new DimensionFormatter(a)));
}
private void failIfNotEqualColor(@Nonnull BufferedImage expected, @Nonnull Threshold threshold) {
int w = actual.getWidth();
int h = actual.getHeight();
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
failIfNotEqual(new RGBColor(actual.getRGB(x, y)), new RGBColor(expected.getRGB(x, y)), threshold, x, y);
}
}
}
private void failIfNotEqual(@Nonnull RGBColor a, @Nonnull RGBColor e, @Nonnull Threshold threshold, int x, int y) {
if (a.isEqualTo(e, threshold.value())) {
return;
}
failIfCustomMessageIsSet();
fail(String.format("expected:<%s> but was:<%s> at pixel [%d,%d]", a, e, x, y));
}
/**
* Verifies that the actual image is not equal to the given one. Two images are equal if they have the same size and
* the pixels at the same coordinates have the same color.
*
* @param image the given image to compare the actual image to.
* @return this assertion object.
* @throws AssertionError if the actual image is equal to the given one.
*/
@Override
public @Nonnull ImageAssert isNotEqualTo(@Nullable BufferedImage image) {
if (areEqual(actual, image)) {
failIfCustomMessageIsSet();
throw failure(unexpectedEqual(actual, image));
}
if (image == null) {
return this;
}
if (areEqual(sizeOf(actual), sizeOf(image)) && hasEqualColor(image)) {
failIfCustomMessageIsSet();
throw failure("images are equal");
}
return this;
}
private boolean hasEqualColor(@Nonnull BufferedImage expected) {
int w = actual.getWidth();
int h = actual.getHeight();
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
if (actual.getRGB(x, y) != expected.getRGB(x, y)) {
return false;
}
}
}
return true;
}
/**
* Verifies that the size of the actual image is equal to the given one.
*
* @param expected the expected size of the actual image.
* @return this assertion object.
* @throws AssertionError if the actual image is {@code null}.
* @throws NullPointerException if the given size is {@code null}.
* @throws AssertionError if the size of the actual image is not equal to the given one.
*/
public @Nonnull ImageAssert hasSize(@Nonnull Dimension expected) {
isNotNull();
checkNotNull(expected);
Dimension actualDimension = new Dimension(actual.getWidth(), actual.getHeight());
if (areEqual(actualDimension, expected)) {
return this;
}
failWithMessage(customErrorMessage());
throw comparisonFailed(rawDescription(), new DimensionFormatter(actualDimension), new DimensionFormatter(expected));
}
private static class DimensionFormatter {
private final Dimension dimension;
DimensionFormatter(@Nullable Dimension dimension) {
this.dimension = dimension;
}
@Override
public String toString() {
if (dimension == null) {
return null;
}
String format = "(w=%d, h=%d)";
return String.format(format, dimension.width, dimension.height);
}
}
}