1616
1717package io .appium .java_client ;
1818
19- import static nu .pattern .OpenCV .loadShared ;
19+ import static com .google .common .base .Preconditions .checkNotNull ;
20+ import static java .util .Optional .ofNullable ;
2021
21- import org .opencv .core .Core ;
22- import org .opencv .core .CvType ;
23- import org .opencv .core .Mat ;
24- import org .opencv .core .Size ;
25- import org .opencv .imgproc .Imgproc ;
26-
27- import java .awt .AlphaComposite ;
28- import java .awt .Graphics2D ;
2922import java .awt .image .BufferedImage ;
30- import java .awt .image .DataBufferByte ;
23+ import java .io .ByteArrayOutputStream ;
24+ import java .io .IOException ;
3125import java .time .Duration ;
3226import java .time .LocalDateTime ;
33- import java .util .Optional ;
27+ import java .util .Base64 ;
3428import java .util .function .Function ;
3529import java .util .function .Supplier ;
3630
31+ import javax .imageio .ImageIO ;
32+
3733public class ScreenshotState {
3834 private static final Duration DEFAULT_INTERVAL_MS = Duration .ofMillis (500 );
3935
40- private Optional <BufferedImage > previousScreenshot = Optional .empty ();
41- private Supplier <BufferedImage > stateProvider ;
36+ private BufferedImage previousScreenshot ;
37+ private final Supplier <BufferedImage > stateProvider ;
38+ private final ComparesImages comparator ;
4239
4340 private Duration comparisonInterval = DEFAULT_INTERVAL_MS ;
4441
@@ -72,10 +69,15 @@ public class ScreenshotState {
7269 *
7370 * @param stateProvider lambda function, which returns a screenshot for further comparison
7471 */
75- public ScreenshotState (Supplier <BufferedImage > stateProvider ) {
72+ public ScreenshotState (ComparesImages comparator , Supplier <BufferedImage > stateProvider ) {
73+ this .comparator = checkNotNull (comparator );
7674 this .stateProvider = stateProvider ;
7775 }
7876
77+ public ScreenshotState (ComparesImages comparator ) {
78+ this (comparator , null );
79+ }
80+
7981 /**
8082 * Gets the interval value in ms between similarity verification rounds in <em>verify*</em> methods.
8183 *
@@ -103,7 +105,7 @@ public ScreenshotState setComparisonInterval(Duration comparisonInterval) {
103105 * @return self instance for chaining
104106 */
105107 public ScreenshotState remember () {
106- this .previousScreenshot = Optional . of ( stateProvider .get () );
108+ this .previousScreenshot = stateProvider .get ();
107109 return this ;
108110 }
109111
@@ -116,7 +118,7 @@ public ScreenshotState remember() {
116118 * @return self instance for chaining
117119 */
118120 public ScreenshotState remember (BufferedImage customInitialState ) {
119- this .previousScreenshot = Optional . of (customInitialState );
121+ this .previousScreenshot = checkNotNull (customInitialState );
120122 return this ;
121123 }
122124
@@ -134,7 +136,7 @@ public static class ScreenshotComparisonError extends RuntimeException {
134136
135137 public static class ScreenshotComparisonTimeout extends RuntimeException {
136138 private static final long serialVersionUID = 6336247721154252476L ;
137- private double currentScore = Double . NaN ;
139+ private final double currentScore ;
138140
139141 ScreenshotComparisonTimeout (String message , double currentScore ) {
140142 super (message );
@@ -147,17 +149,13 @@ public double getCurrentScore() {
147149 }
148150
149151 private ScreenshotState checkState (Function <Double , Boolean > checkerFunc , Duration timeout ) {
150- return checkState (checkerFunc , timeout , ResizeMode .NO_RESIZE );
151- }
152-
153- private ScreenshotState checkState (Function <Double , Boolean > checkerFunc , Duration timeout , ResizeMode resizeMode ) {
154152 final LocalDateTime started = LocalDateTime .now ();
155153 double score ;
156154 do {
157155 final BufferedImage currentState = stateProvider .get ();
158- score = getOverlapScore (this .previousScreenshot
156+ score = getOverlapScore (ofNullable ( this .previousScreenshot )
159157 .orElseThrow (() -> new ScreenshotComparisonError ("Initial screenshot state is not set. "
160- + "Nothing to compare" )), currentState , resizeMode );
158+ + "Nothing to compare" )), currentState );
161159 if (checkerFunc .apply (score )) {
162160 return this ;
163161 }
@@ -188,25 +186,6 @@ public ScreenshotState verifyChanged(Duration timeout, double minScore) {
188186 return checkState ((x ) -> x < minScore , timeout );
189187 }
190188
191- /**
192- * Verifies whether the state of the screenshot provided by stateProvider lambda function
193- * is changed within the given timeout.
194- *
195- * @param timeout timeout value
196- * @param minScore the value in range (0.0, 1.0)
197- * @param resizeMode one of <em>ResizeMode</em> enum values.
198- * Set it to a value different from <em>NO_RESIZE</em>
199- * if the actual screenshot is expected to have different
200- * dimensions in comparison to the previously remembered one
201- * @return self instance for chaining
202- * @throws ScreenshotComparisonTimeout if the calculated score is still
203- * greater or equal to the given score after timeout happens
204- * @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet
205- */
206- public ScreenshotState verifyChanged (Duration timeout , double minScore , ResizeMode resizeMode ) {
207- return checkState ((x ) -> x < minScore , timeout , resizeMode );
208- }
209-
210189 /**
211190 * Verifies whether the state of the screenshot provided by stateProvider lambda function
212191 * is not changed within the given timeout.
@@ -222,112 +201,28 @@ public ScreenshotState verifyNotChanged(Duration timeout, double minScore) {
222201 return checkState ((x ) -> x >= minScore , timeout );
223202 }
224203
225- /**
226- * Verifies whether the state of the screenshot provided by stateProvider lambda function
227- * is changed within the given timeout.
228- *
229- * @param timeout timeout value
230- * @param minScore the value in range (0.0, 1.0)
231- * @param resizeMode one of <em>ResizeMode</em> enum values.
232- * Set it to a value different from <em>NO_RESIZE</em>
233- * if the actual screenshot is expected to have different
234- * dimensions in comparison to the previously remembered one
235- * @return self instance for chaining
236- * @throws ScreenshotComparisonTimeout if the calculated score is still
237- * less than the given score after timeout happens
238- * @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet
239- */
240- public ScreenshotState verifyNotChanged (Duration timeout , double minScore , ResizeMode resizeMode ) {
241- return checkState ((x ) -> x >= minScore , timeout , resizeMode );
242- }
243-
244- private static Mat prepareImageForComparison (BufferedImage srcImage ) {
245- final BufferedImage normalizedBitmap = new BufferedImage (srcImage .getWidth (), srcImage .getHeight (),
246- BufferedImage .TYPE_3BYTE_BGR );
247- final Graphics2D g = normalizedBitmap .createGraphics ();
248- try {
249- g .setComposite (AlphaComposite .Src );
250- g .drawImage (srcImage , 0 , 0 , null );
251- } finally {
252- g .dispose ();
253- }
254- final byte [] pixels = ((DataBufferByte ) normalizedBitmap .getRaster ().getDataBuffer ()).getData ();
255- final Mat result = new Mat (normalizedBitmap .getHeight (), normalizedBitmap .getWidth (), CvType .CV_8UC3 );
256- result .put (0 , 0 , pixels );
257- return result ;
258- }
259-
260- private static Mat resizeFirstMatrixToSecondMatrixResolution (Mat first , Mat second ) {
261- if (first .width () != second .width () || first .height () != second .height ()) {
262- final Mat result = new Mat ();
263- final Size sz = new Size (second .width (), second .height ());
264- Imgproc .resize (first , result , sz );
265- return result ;
266- }
267- return first ;
268- }
269-
270- /**
271- * A shortcut to {@link #getOverlapScore(BufferedImage, BufferedImage, ResizeMode)} method
272- * for the case if both reference and template images are expected to have the same dimensions.
273- *
274- * @param refImage reference image
275- * @param tplImage template
276- * @return similarity score value in range (-1.0, 1.0). 1.0 is returned if the images are equal
277- * @throws ScreenshotComparisonError if provided images are not valid or have different resolution
278- */
279- public static double getOverlapScore (BufferedImage refImage , BufferedImage tplImage ) {
280- return getOverlapScore (refImage , tplImage , ResizeMode .NO_RESIZE );
281- }
282-
283204 /**
284205 * Compares two valid java bitmaps and calculates similarity score between them.
206+ * Both images are expected to be of the same size/resolution. The method
207+ * implicitly invokes {@link ComparesImages#getImagesSimilarity(byte[], byte[])}.
285208 *
286209 * @param refImage reference image
287210 * @param tplImage template
288- * @param resizeMode one of possible enum values. Set it either to <em>TEMPLATE_TO_REFERENCE_RESOLUTION</em> or
289- * <em>REFERENCE_TO_TEMPLATE_RESOLUTION</em> if given bitmaps have different dimensions
290- * @return similarity score value in range (-1.0, 1.0). 1.0 is returned if the images are equal
211+ * @return similarity score value in range (-1.0, 1.0]. 1.0 is returned if the images are equal
291212 * @throws ScreenshotComparisonError if provided images are not valid or have
292- * different resolution, but resizeMode has been set to <em>NO_RESIZE</em>
213+ * different resolution
293214 */
294- public static double getOverlapScore (BufferedImage refImage , BufferedImage tplImage , ResizeMode resizeMode ) {
295- Mat ref = prepareImageForComparison (refImage );
296- if (ref .empty ()) {
297- throw new ScreenshotComparisonError ("Reference image cannot be converted for further comparison" );
298- }
299- Mat tpl = prepareImageForComparison (tplImage );
300- if (tpl .empty ()) {
301- throw new ScreenshotComparisonError ("Template image cannot be converted for further comparison" );
302- }
303- switch (resizeMode ) {
304- case TEMPLATE_TO_REFERENCE_RESOLUTION :
305- tpl = resizeFirstMatrixToSecondMatrixResolution (tpl , ref );
306- break ;
307- case REFERENCE_TO_TEMPLATE_RESOLUTION :
308- ref = resizeFirstMatrixToSecondMatrixResolution (ref , tpl );
309- break ;
310- default :
311- // do nothing
312- }
313-
314- if (ref .width () != tpl .width () || ref .height () != tpl .height ()) {
315- throw new ScreenshotComparisonError (
316- "Resolutions of template and reference images are expected to be equal. "
317- + "Try different resizeMode value."
318- );
215+ public double getOverlapScore (BufferedImage refImage , BufferedImage tplImage ) {
216+ try (ByteArrayOutputStream img1 = new ByteArrayOutputStream ();
217+ ByteArrayOutputStream img2 = new ByteArrayOutputStream ()) {
218+ ImageIO .write (refImage , "png" , img1 );
219+ ImageIO .write (tplImage , "png" , img2 );
220+ return comparator
221+ .getImagesSimilarity (Base64 .getEncoder ().encode (img1 .toByteArray ()),
222+ Base64 .getEncoder ().encode (img2 .toByteArray ()))
223+ .getScore ();
224+ } catch (IOException e ) {
225+ throw new ScreenshotComparisonError (e );
319226 }
320-
321- Mat res = new Mat (ref .rows () - tpl .rows () + 1 , ref .cols () - tpl .cols () + 1 , CvType .CV_32FC1 );
322- Imgproc .matchTemplate (ref , tpl , res , Imgproc .TM_CCOEFF_NORMED );
323- return Core .minMaxLoc (res ).maxVal ;
324- }
325-
326- public enum ResizeMode {
327- NO_RESIZE , TEMPLATE_TO_REFERENCE_RESOLUTION , REFERENCE_TO_TEMPLATE_RESOLUTION
328- }
329-
330- static {
331- loadShared ();
332227 }
333228}
0 commit comments