blob: 11f9866e5fc0c5f385f3d2484544ff3111600d60 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2018 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Matthew Khouzam - Initial API and implementation
*******************************************************************************/
package org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared;
import java.awt.AWTException;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.imageio.ImageIO;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swtbot.swt.finder.finders.UIThreadRunnable;
import org.eclipse.swtbot.swt.finder.results.Result;
import org.eclipse.swtbot.swt.finder.results.VoidResult;
import org.eclipse.tracecompass.tmf.ui.tests.shared.WaitTimeoutException;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
/**
* Test helpers, allow looking up the frame buffer and testing what is really
* displayed
*/
public final class ImageHelper {
private final int[] fPixels;
private final Rectangle fBounds;
/**
* Constructor
*
* @param pixels
* the pixel map
* @param bounds
* the bounds
*/
private ImageHelper(int[] pixels, Rectangle bounds) {
if (pixels.length != bounds.height * bounds.width) {
throw new IllegalArgumentException("Incoherent image");
}
fPixels = Arrays.copyOf(pixels, pixels.length);
fBounds = bounds;
}
/**
* Gets a screen grab of the rectangle r; the way to access a given pixel is
* <code>pixel = rect.width * y + x;</code>
*
* @param rect
* the area to grab in display relative coordinates (top left is the
* origin)
* @return an ImageHelper, cannot be null
*/
public static ImageHelper grabImage(final Rectangle rect) {
return UIThreadRunnable.syncExec(new Result<ImageHelper>() {
@Override
public ImageHelper run() {
try {
// note: awt is explicitly called until we can use SWT to
// replace it.
java.awt.Robot rb = new java.awt.Robot();
java.awt.image.BufferedImage bi = rb.createScreenCapture(new java.awt.Rectangle(rect.x, rect.y, rect.width, rect.height));
return new ImageHelper(bi.getRGB(0, 0, rect.width, rect.height, null, 0, rect.width), rect);
} catch (AWTException e) {
}
return new ImageHelper(new int[0], new Rectangle(0, 0, 0, 0));
}
});
}
/**
* Create an image helper from a file
*
* @param file
* the file (only tested with PNG)
* @return an image helper or null if failed
* @throws IOException
* the file is not valid
*/
public static ImageHelper fromFile(File file) throws IOException {
BufferedImage bufferedImage = ImageIO.read(file);
if (bufferedImage == null) {
return null;
}
Rectangle bounds = new Rectangle(0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
int[] rgb = bufferedImage.getRGB(0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), null, 0, bufferedImage.getWidth());
return new ImageHelper(rgb, bounds);
}
/**
* Wait for the image at the specified bounds to change from the current image.
* The method returns when the new image is stable.
*
* @param bounds
* the bounds in display coordinates
* @param currentImage
* the current image, or null
* @return the new image
*/
public static ImageHelper waitForNewImage(Rectangle bounds, ImageHelper currentImage) {
SWTBotUtils.waitUntil(arg -> {
return !ImageHelper.grabImage(bounds).equals(currentImage);
}, null, "Image at bounds " + bounds + " did not change");
AtomicReference<ImageHelper> newImage = new AtomicReference<>();
AtomicInteger count = new AtomicInteger();
SWTBotUtils.waitUntil(arg -> {
ImageHelper image = ImageHelper.grabImage(bounds);
if (image.equals(newImage.get())) {
count.incrementAndGet();
} else {
count.set(0);
newImage.set(image);
}
return count.get() > 8;
}, null, "Image at bounds " + bounds + " is not stable");
if (newImage.get().equals(currentImage)) {
throw new WaitTimeoutException("Image at bounds " + bounds + " did not change");
}
return newImage.get();
}
/**
* Get the bounds
*
* @return the bounds
*/
public Rectangle getBounds() {
return fBounds;
}
/**
* Get the pixel for a given set of coordinates
*
* @param x
* x
* @param y
* y
* @return the RGB, can return an {@link ArrayIndexOutOfBoundsException}
*/
public RGB getPixel(int x, int y) {
return getRgbFromRGBPixel(fPixels[x + y * fBounds.width]);
}
/**
* Sample an image at n points
*
* @param samplePoints
* a list of points to sample at
* @return a list of RGBs corresponding to the pixel coordinates. Can throw an
* {@link IllegalArgumentException} if the point is outside of the image
* bounds
*/
public List<RGB> sample(List<Point> samplePoints) {
for (Point p : samplePoints) {
if (!getBounds().contains(p)) {
throw new IllegalArgumentException("Point outside of the image");
}
}
List<RGB> retVal = new ArrayList<>(samplePoints.size());
for (Point p : samplePoints) {
retVal.add(getPixel(p.x, p.y));
}
return retVal;
}
/**
* Get the color histogram of the image
*
* @return The color density of the image
*/
public Multiset<RGB> getHistogram() {
Multiset<RGB> colors = HashMultiset.create();
for (int pixel : fPixels) {
RGB pixelColor = getRgbFromRGBPixel(pixel);
colors.add(pixelColor);
}
return colors;
}
/**
* Get the color histogram of the row of the image
*
* @param row
* the row to lookup
*
* @return The x oriented line
*/
public List<RGB> getPixelRow(int row) {
List<RGB> retVal = new ArrayList<>();
for (int x = 0; x < getBounds().width; x++) {
retVal.add(getPixel(x, row));
}
return retVal;
}
/**
* Get the color histogram of a column of the image
*
* @param col
* the column to lookup
*
* @return The y oriented line
*/
public List<RGB> getPixelColumn(int col) {
List<RGB> retVal = new ArrayList<>();
for (int y = 0; y < getBounds().height; y++) {
retVal.add(getPixel(col, y));
}
return retVal;
}
/**
* Difference between two images (this - other)
*
* @param other
* the other image to compare
* @return an {@link ImageHelper} that is the per pixel difference between the
* two images
*
*/
public ImageHelper diff(ImageHelper other) {
if (other.getBounds().width != fBounds.width && other.getBounds().height != fBounds.height) {
throw new IllegalArgumentException("Different sized images");
}
int[] fBuffer = new int[fPixels.length];
for (int i = 0; i < fPixels.length; i++) {
RGB local = getRgbFromRGBPixel(fPixels[i]);
RGB otherPixel = getRgbFromRGBPixel(other.fPixels[i]);
byte r = (byte) (local.red - otherPixel.red);
byte g = (byte) (local.green - otherPixel.green);
byte b = (byte) (local.blue - otherPixel.blue);
fBuffer[i] = 0xff << 24 | ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff);
}
return new ImageHelper(fBuffer, getBounds());
}
/**
* Write the image to disk in PNG form
*
* @param outputFile
* the file to write it to
* @throws IOException
* file not found and such
*/
public void writePng(File outputFile) throws IOException {
BufferedImage image = new BufferedImage(fBounds.width, fBounds.height, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, fBounds.width, fBounds.height, fPixels, 0, fBounds.width);
ImageIO.write(image, "png", outputFile);
}
/**
* Converter from ARGB to {@link RGB}
*
* @param pixel
* an ARGB encoded pixel
* @return the {@link RGB}
*/
private static RGB getRgbFromRGBPixel(int pixel) {
return new RGB(((pixel >> 16) & 0xff), ((pixel >> 8) & 0xff), ((pixel) & 0xff));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((fBounds == null) ? 0 : fBounds.hashCode());
result = prime * result + Arrays.hashCode(fPixels);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ImageHelper other = (ImageHelper) obj;
if (fBounds == null) {
if (other.fBounds != null) {
return false;
}
} else if (!fBounds.equals(other.fBounds)) {
return false;
}
if (!Arrays.equals(fPixels, other.fPixels)) {
return false;
}
return true;
}
/**
* On some platforms, RGB values that are captured with ImageHelper are affected
* by monitor color profiles. To account for this, we can draw the expected
* color in a simple shell and use that color as expected value instead.
*
* @param original
* original color to adjust
* @return adjusted color
*/
public static synchronized RGB adjustExpectedColor(RGB original) {
/* Create shell with desired color as background */
boolean painted[] = new boolean[1];
final Shell shell = UIThreadRunnable.syncExec(new Result<Shell>() {
@Override
public Shell run() {
Shell s = new Shell(Display.getDefault(), SWT.ON_TOP);
s.setSize(16, 16);
Color color = new Color(Display.getDefault(), original);
s.setBackground(color);
s.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
e.gc.setBackground(color);
e.gc.fillRectangle(0, 0, 16, 16);
painted[0] = true;
}
});
s.open();
return s;
}
});
/* Make sure the shell has been painted before getting the color */
SWTBotUtils.waitUntil(arg -> painted[0], null, "Shell was not painted");
/* Get the color, after waiting for it to stabilize */
AtomicReference<RGB> adjusted = new AtomicReference<>();
AtomicInteger count = new AtomicInteger();
SWTBotUtils.waitUntil(s -> {
RGB pixel = UIThreadRunnable.syncExec(new Result<RGB>() {
@Override
public RGB run() {
shell.redraw();
shell.update();
return ImageHelper.grabImage(shell.getBounds()).getPixel(8, 8);
}
});
if (pixel.equals(adjusted.get())) {
count.incrementAndGet();
} else {
count.set(0);
adjusted.set(pixel);
}
return count.get() > 8;
}, shell, "Color did not stabilize");
UIThreadRunnable.syncExec(new VoidResult() {
@Override
public void run() {
shell.close();
}
});
return adjusted.get();
}
}