@appium/images-plugin 2.0.10 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/finder.js CHANGED
@@ -1,113 +1,72 @@
1
1
  import _ from 'lodash';
2
2
  import LRU from 'lru-cache';
3
3
  import {errors} from 'appium/driver';
4
- import {util, imageUtil} from 'appium/support';
5
- import {
6
- ImageElement,
7
- DEFAULT_TEMPLATE_IMAGE_SCALE,
8
- IMAGE_EL_TAP_STRATEGY_W3C,
9
- } from './image-element';
10
- import {MATCH_TEMPLATE_MODE, compareImages, DEFAULT_MATCH_THRESHOLD} from './compare';
4
+ import {ImageElement} from './image-element';
5
+ import {compareImages} from './compare';
11
6
  import log from './logger';
7
+ import {
8
+ DEFAULT_SETTINGS, MATCH_TEMPLATE_MODE, DEFAULT_TEMPLATE_IMAGE_SCALE,
9
+ DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
10
+ } from './constants';
11
+ import sharp from 'sharp';
12
12
 
13
- const MJSONWP_ELEMENT_KEY = 'ELEMENT';
14
- const W3C_ELEMENT_KEY = util.W3C_WEB_ELEMENT_IDENTIFIER;
15
- const DEFAULT_FIX_IMAGE_TEMPLATE_SCALE = 1;
16
13
  // Used to compare ratio and screen width
17
14
  // Pixel is basically under 1080 for example. 100K is probably enough fo a while.
18
15
  const FLOAT_PRECISION = 100000;
19
16
  const MAX_CACHE_ITEMS = 100;
20
- const MAX_CACHE_SIZE_BYTES = 1024 * 1024 * 40; // 40mb
21
-
22
- const DEFAULT_SETTINGS = {
23
- // value between 0 and 1 representing match strength, below which an image
24
- // element will not be found
25
- imageMatchThreshold: DEFAULT_MATCH_THRESHOLD,
26
-
27
- // One of possible image matching methods.
28
- // Read https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html
29
- // for more details.
30
- // TM_CCOEFF_NORMED by default
31
- imageMatchMethod: '',
32
-
33
- // if the image returned by getScreenshot differs in size or aspect ratio
34
- // from the screen, attempt to fix it automatically
35
- fixImageFindScreenshotDims: true,
36
-
37
- // whether Appium should ensure that an image template sent in during image
38
- // element find should have its size adjusted so the match algorithm will not
39
- // complain
40
- fixImageTemplateSize: false,
41
-
42
- // whether Appium should ensure that an image template sent in during image
43
- // element find should have its scale adjusted to display size so the match
44
- // algorithm will not complain.
45
- // e.g. iOS has `width=375, height=667` window rect, but its screenshot is
46
- // `width=750 × height=1334` pixels. This setting help to adjust the scale
47
- // if a user use `width=750 × height=1334` pixels's base template image.
48
- fixImageTemplateScale: false,
49
-
50
- // Users might have scaled template image to reduce their storage size.
51
- // This setting allows users to scale a template image they send to Appium server
52
- // so that the Appium server compares the actual scale users originally had.
53
- // e.g. If a user has an image of 270 x 32 pixels which was originally 1080 x 126 pixels,
54
- // the user can set {defaultImageTemplateScale: 4.0} to scale the small image
55
- // to the original one so that Appium can compare it as the original one.
56
- defaultImageTemplateScale: DEFAULT_TEMPLATE_IMAGE_SCALE,
57
-
58
- // whether Appium should re-check that an image element can be matched
59
- // against the current screenshot before clicking it
60
- checkForImageElementStaleness: true,
61
-
62
- // whether before clicking on an image element Appium should re-determine the
63
- // position of the element on screen
64
- autoUpdateImageElementPosition: false,
65
-
66
- // which method to use for tapping by coordinate for image elements. the
67
- // options are 'w3c' or 'mjsonwp'
68
- imageElementTapStrategy: IMAGE_EL_TAP_STRATEGY_W3C,
69
-
70
- // which method to use to save the matched image area in ImageElement class.
71
- // It is used for debugging purpose.
72
- getMatchedImageResult: false,
73
- };
17
+ const MAX_CACHE_AGE_MS = 24 * 60 * 60 * 1000;
18
+
19
+ /**
20
+ * Checks if one rect fully contains another
21
+ *
22
+ * @param {import('@appium/types').Rect} templateRect The bounding rect
23
+ * @param {import('@appium/types').Rect} rect The rect to be checked for containment
24
+ * @returns {boolean} True if templateRect contains rect
25
+ */
26
+ function containsRect(templateRect, rect) {
27
+ return templateRect.x <= rect.x && templateRect.y <= rect.y
28
+ && rect.width <= templateRect.x + templateRect.width - rect.x
29
+ && rect.height <= templateRect.y + templateRect.height - rect.y;
30
+ }
31
+
74
32
  const NO_OCCURRENCES_PATTERN = /Cannot find any occurrences/;
75
33
  const CONDITION_UNMET_PATTERN = /Condition unmet/;
76
34
 
77
35
 
78
36
  export default class ImageElementFinder {
79
- /** @type {ExternalDriver} */
80
- driver;
81
-
82
37
  /** @type {LRU<string,ImageElement>} */
83
- imgElCache;
38
+ _imgElCache;
84
39
 
85
40
  /**
86
- *
87
- * @param {ExternalDriver} driver
88
- * @param {number} [maxSize]
41
+ * @param {number} max
89
42
  */
90
- constructor(driver, maxSize = MAX_CACHE_SIZE_BYTES) {
91
- this.driver = driver;
92
- this.imgElCache = new LRU({
93
- max: MAX_CACHE_ITEMS,
94
- maxSize,
95
- sizeCalculation: (el) => el.template.length,
43
+ constructor(max = MAX_CACHE_ITEMS) {
44
+ this._imgElCache = new LRU({
45
+ ttl: MAX_CACHE_AGE_MS,
46
+ updateAgeOnGet: true,
47
+ max,
96
48
  });
97
49
  }
98
50
 
99
- setDriver(driver) {
100
- this.driver = driver;
101
- }
102
-
103
51
  /**
104
52
  * @param {ImageElement} imgEl
105
53
  * @returns {Element}
106
54
  */
107
55
  registerImageElement(imgEl) {
108
- this.imgElCache.set(imgEl.id, imgEl);
109
- const protoKey = this.driver.isW3CProtocol() ? W3C_ELEMENT_KEY : MJSONWP_ELEMENT_KEY;
110
- return imgEl.asElement(protoKey);
56
+ this._imgElCache.set(imgEl.id, imgEl);
57
+ return imgEl.asElement();
58
+ }
59
+
60
+ /**
61
+ * @param {string} imgElId
62
+ * @returns {ImageElement|undefined}
63
+ */
64
+ getImageElement(imgElId) {
65
+ return this._imgElCache.get(imgElId);
66
+ }
67
+
68
+ clearImageElements() {
69
+ this._imgElCache.clear();
111
70
  }
112
71
 
113
72
  /**
@@ -118,27 +77,28 @@ export default class ImageElementFinder {
118
77
  * multiple
119
78
  * @property {boolean} [ignoreDefaultImageTemplateScale=false] - Whether we
120
79
  * ignore defaultImageTemplateScale. It can be used when you would like to
121
- * scale b64Template with defaultImageTemplateScale setting.
80
+ * scale template with defaultImageTemplateScale setting.
81
+ * @property {import('@appium/types').Rect?} containerRect - The bounding
82
+ * rectangle to limit the search in
122
83
  */
123
84
 
124
85
  /**
125
86
  * Find a screen rect represented by an ImageElement corresponding to an image
126
87
  * template sent in by the client
127
88
  *
128
- * @param {string} b64Template - base64-encoded image used as a template to be
89
+ * @param {Buffer} template - image used as a template to be
129
90
  * matched in the screenshot
91
+ * @param {ExternalDriver} driver
130
92
  * @param {FindByImageOptions} opts - additional options
131
93
  *
132
94
  * @returns {Promise<Element|Element[]|ImageElement>} - WebDriver element with a special id prefix
133
95
  */
134
96
  async findByImage(
135
- b64Template,
136
- {shouldCheckStaleness = false, multiple = false, ignoreDefaultImageTemplateScale = false}
97
+ template,
98
+ driver,
99
+ {shouldCheckStaleness = false, multiple = false, ignoreDefaultImageTemplateScale = false, containerRect = null}
137
100
  ) {
138
- if (!this.driver) {
139
- throw new Error(`Can't find without a driver!`);
140
- }
141
- const settings = {...DEFAULT_SETTINGS, ...this.driver.settings.getSettings()};
101
+ const settings = {...DEFAULT_SETTINGS, ...driver.settings.getSettings()};
142
102
  const {
143
103
  imageMatchThreshold: threshold,
144
104
  imageMatchMethod,
@@ -149,28 +109,38 @@ export default class ImageElementFinder {
149
109
  } = settings;
150
110
 
151
111
  log.info(`Finding image element with match threshold ${threshold}`);
152
- if (!this.driver.getWindowSize) {
153
- throw new Error("This driver does not support the required 'getWindowSize' command");
112
+ if (!driver.getWindowRect && !driver.getWindowSize) {
113
+ throw new Error("This driver does not support the required 'getWindowRect' command");
114
+ }
115
+ let screenSize;
116
+ if (driver.getWindowRect) {
117
+ const screenRect = await driver.getWindowRect();
118
+ screenSize = {
119
+ width: screenRect.width,
120
+ height: screenRect.height,
121
+ };
122
+ } else {
123
+ // TODO: Drop the deprecated endpoint
124
+ screenSize = await driver.getWindowSize();
154
125
  }
155
- const {width: screenWidth, height: screenHeight} = await this.driver.getWindowSize();
156
126
 
157
127
  // someone might have sent in a template that's larger than the screen
158
128
  // dimensions. If so let's check and cut it down to size since the algorithm
159
129
  // will not work unless we do. But because it requires some potentially
160
130
  // expensive commands, only do this if the user has requested it in settings.
161
131
  if (fixImageTemplateSize) {
162
- b64Template = await this.ensureTemplateSize(b64Template, screenWidth, screenHeight);
132
+ template = await this.ensureTemplateSize(template, {
133
+ width: containerRect ? containerRect.width : screenSize.width,
134
+ height: containerRect ? containerRect.height : screenSize.height,
135
+ });
163
136
  }
164
137
 
165
138
  const results = [];
166
139
  const condition = async () => {
167
140
  try {
168
- const {b64Screenshot, scale} = await this.getScreenshotForImageFind(
169
- screenWidth,
170
- screenHeight
171
- );
141
+ const {screenshot, scale} = await this.getScreenshotForImageFind(driver, screenSize);
172
142
 
173
- b64Template = await this.fixImageTemplateScale(b64Template, {
143
+ template = await this.fixImageTemplateScale(template, {
174
144
  defaultImageTemplateScale,
175
145
  ignoreDefaultImageTemplateScale,
176
146
  fixImageTemplateScale,
@@ -185,21 +155,21 @@ export default class ImageElementFinder {
185
155
  if (imageMatchMethod) {
186
156
  comparisonOpts.method = imageMatchMethod;
187
157
  }
188
- if (multiple) {
189
- results.push(
190
- ...(await compareImages(
191
- MATCH_TEMPLATE_MODE,
192
- b64Screenshot,
193
- b64Template,
194
- comparisonOpts
195
- ))
196
- );
197
- } else {
198
- results.push(
199
- await compareImages(MATCH_TEMPLATE_MODE, b64Screenshot, b64Template, comparisonOpts)
200
- );
201
- }
202
- return true;
158
+
159
+ const pushIfOk = (el) => {
160
+ if (containerRect && !containsRect(containerRect, el.rect)) {
161
+ log.debug(
162
+ `The matched element rectangle ${JSON.stringify(el.rect)} is not located ` +
163
+ `inside of the bounding rectangle ${JSON.stringify(containerRect)}, thus rejected`
164
+ );
165
+ return false;
166
+ }
167
+ results.push(el);
168
+ return true;
169
+ };
170
+
171
+ const elOrEls = await compareImages(MATCH_TEMPLATE_MODE, screenshot, template, comparisonOpts);
172
+ return _.some((_.isArray(elOrEls) ? elOrEls : [elOrEls]).map(pushIfOk));
203
173
  } catch (err) {
204
174
  // if compareImages fails, we'll get a specific error, but we should
205
175
  // retry, so trap that and just return false to trigger the next round of
@@ -213,7 +183,7 @@ export default class ImageElementFinder {
213
183
  };
214
184
 
215
185
  try {
216
- await this.driver.implicitWaitForCondition(condition);
186
+ await driver.implicitWaitForCondition(condition);
217
187
  } catch (err) {
218
188
  // this `implicitWaitForCondition` method will throw a 'Condition unmet'
219
189
  // error if an element is not found eventually. In that case, we will
@@ -235,7 +205,14 @@ export default class ImageElementFinder {
235
205
 
236
206
  const elements = results.map(({rect, score, visualization}) => {
237
207
  log.info(`Image template matched: ${JSON.stringify(rect)}`);
238
- return new ImageElement(b64Template, rect, score, visualization, this);
208
+ return new ImageElement({
209
+ template,
210
+ rect,
211
+ score,
212
+ match: visualization ? Buffer.from(visualization, 'base64') : null,
213
+ finder: this,
214
+ containerRect,
215
+ });
239
216
  });
240
217
 
241
218
  // if we're just checking staleness, return straightaway so we don't add
@@ -253,86 +230,90 @@ export default class ImageElementFinder {
253
230
  /**
254
231
  * Ensure that the image template sent in for a find is of a suitable size
255
232
  *
256
- * @param {string} b64Template - base64-encoded image
257
- * @param {number} screenWidth - width of screen
258
- * @param {number} screenHeight - height of screen
233
+ * @param {Buffer} template - template image
234
+ * @param {import('@appium/types').Size} maxSize - size of the bounding rectangle
259
235
  *
260
- * @returns {Promise<string>} base64-encoded image, potentially resized
236
+ * @returns {Promise<Buffer>} image, potentially resized
261
237
  */
262
- async ensureTemplateSize(b64Template, screenWidth, screenHeight) {
263
- let imgObj = await imageUtil.getJimpImage(b64Template);
264
- let {width: tplWidth, height: tplHeight} = imgObj.bitmap;
238
+ async ensureTemplateSize(template, maxSize) {
239
+ const imgObj = sharp(template);
240
+ const {width: tplWidth, height: tplHeight} = await imgObj.metadata();
265
241
 
266
242
  log.info(
267
- `Template image is ${tplWidth}x${tplHeight}. Screen size is ${screenWidth}x${screenHeight}`
243
+ `Template image is ${tplWidth}x${tplHeight}. Bounding rectangle size is ${maxSize.width}x${maxSize.height}`
268
244
  );
269
245
  // if the template fits inside the screen dimensions, we're good
270
- if (tplWidth <= screenWidth && tplHeight <= screenHeight) {
271
- return b64Template;
246
+ if (tplWidth <= maxSize.width && tplHeight <= maxSize.height) {
247
+ return template;
272
248
  }
273
249
 
274
250
  log.info(
275
251
  `Scaling template image from ${tplWidth}x${tplHeight} to match ` +
276
- `screen at ${screenWidth}x${screenHeight}`
252
+ `the bounding rectangle at ${maxSize.width}x${maxSize.height}`
277
253
  );
278
- // otherwise, scale it to fit inside the screen dimensions
279
- imgObj = imgObj.scaleToFit(screenWidth, screenHeight);
280
- return (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
254
+ // otherwise, scale it to fit inside the bounding rectangle dimensions:
255
+ // https://sharp.pixelplumbing.com/api-resize
256
+ return await imgObj.resize({
257
+ width: parseInt(maxSize.width, 10),
258
+ height: parseInt(maxSize.height, 10),
259
+ fit: 'inside',
260
+ })
261
+ .toBuffer();
281
262
  }
282
263
 
283
264
  /**
284
265
  * Get the screenshot image that will be used for find by element, potentially
285
266
  * altering it in various ways based on user-requested settings
286
267
  *
287
- * @param {number} screenWidth - width of screen
288
- * @param {number} screenHeight - height of screen
268
+ * @param {ExternalDriver} driver
269
+ * @param {import('@appium/types').Size} screenSize - The original size of the screen
289
270
  *
290
- * @returns {Promise<Screenshot & {scale?: ScreenshotScale}>} base64-encoded screenshot and ScreenshotScale
271
+ * @returns {Promise<Screenshot & {scale?: ScreenshotScale}>} PNG screenshot and ScreenshotScale
291
272
  */
292
- async getScreenshotForImageFind(screenWidth, screenHeight) {
293
- if (!this.driver.getScreenshot) {
273
+ async getScreenshotForImageFind(driver, screenSize) {
274
+ if (!driver.getScreenshot) {
294
275
  throw new Error("This driver does not support the required 'getScreenshot' command");
295
276
  }
296
- const settings = Object.assign({}, DEFAULT_SETTINGS, this.driver.settings.getSettings());
277
+ const settings = Object.assign({}, DEFAULT_SETTINGS, driver.settings.getSettings());
297
278
  const {fixImageFindScreenshotDims} = settings;
298
279
 
299
- let b64Screenshot = await this.driver.getScreenshot();
280
+ const screenshot = Buffer.from(await driver.getScreenshot(), 'base64');
300
281
 
301
282
  // if the user has requested not to correct for aspect or size differences
302
283
  // between the screenshot and the screen, just return the screenshot now
303
284
  if (!fixImageFindScreenshotDims) {
304
285
  log.info(`Not verifying screenshot dimensions match screen`);
305
- return {b64Screenshot};
286
+ return {screenshot};
306
287
  }
307
288
 
308
- if (screenWidth < 1 || screenHeight < 1) {
289
+ if (screenSize.width < 1 || screenSize.height < 1) {
309
290
  log.warn(
310
- `The retrieved screen size ${screenWidth}x${screenHeight} does ` +
311
- `not seem to be valid. No changes will be applied to the screenshot`
291
+ `The retrieved screen size ${screenSize.width}x${screenSize.height} does ` +
292
+ `not seem to be valid. No changes will be applied to the screenshot`
312
293
  );
313
- return {b64Screenshot};
294
+ return {screenshot};
314
295
  }
315
296
 
316
297
  // otherwise, do some verification on the screenshot to make sure it matches
317
298
  // the screen size and aspect ratio
318
299
  log.info('Verifying screenshot size and aspect ratio');
319
300
 
320
- let imgObj = await imageUtil.getJimpImage(b64Screenshot);
321
- let {width: shotWidth, height: shotHeight} = imgObj.bitmap;
301
+ let imgObj = sharp(screenshot);
302
+ let {width: shotWidth, height: shotHeight} = await imgObj.metadata();
322
303
 
323
304
  if (shotWidth < 1 || shotHeight < 1) {
324
305
  log.warn(
325
306
  `The retrieved screenshot size ${shotWidth}x${shotHeight} does ` +
326
- `not seem to be valid. No changes will be applied to the screenshot`
307
+ `not seem to be valid. No changes will be applied to the screenshot`
327
308
  );
328
- return {b64Screenshot};
309
+ return {screenshot};
329
310
  }
330
311
 
331
- if (screenWidth === shotWidth && screenHeight === shotHeight) {
312
+ if (screenSize.width === shotWidth && screenSize.height === shotHeight) {
332
313
  // the height and width of the screenshot and the device screen match, which
333
314
  // means we should be safe when doing template matches
334
315
  log.info('Screenshot size matched screen size');
335
- return {b64Screenshot};
316
+ return {screenshot};
336
317
  }
337
318
 
338
319
  // otherwise, if they don't match, it could spell problems for the accuracy
@@ -343,19 +324,19 @@ export default class ImageElementFinder {
343
324
 
344
325
  const scale = {xScale: 1.0, yScale: 1.0};
345
326
 
346
- const screenAR = screenWidth / screenHeight;
327
+ const screenAR = screenSize.width / screenSize.height;
347
328
  const shotAR = shotWidth / shotHeight;
348
329
  if (Math.round(screenAR * FLOAT_PRECISION) === Math.round(shotAR * FLOAT_PRECISION)) {
349
330
  log.info(
350
331
  `Screenshot aspect ratio '${shotAR}' (${shotWidth}x${shotHeight}) matched ` +
351
- `screen aspect ratio '${screenAR}' (${screenWidth}x${screenHeight})`
332
+ `screen aspect ratio '${screenAR}' (${screenSize.width}x${screenSize.height})`
352
333
  );
353
334
  } else {
354
335
  log.warn(
355
336
  `When trying to find an element, determined that the screen ` +
356
- `aspect ratio and screenshot aspect ratio are different. Screen ` +
357
- `is ${screenWidth}x${screenHeight} whereas screenshot is ` +
358
- `${shotWidth}x${shotHeight}.`
337
+ `aspect ratio and screenshot aspect ratio are different. Screen ` +
338
+ `is ${screenSize.width}x${screenSize.height} whereas screenshot is ` +
339
+ `${shotWidth}x${shotHeight}.`
359
340
  );
360
341
 
361
342
  // In the case where the x-scale and y-scale are different, we need to decide
@@ -364,46 +345,56 @@ export default class ImageElementFinder {
364
345
  // this.getScreenshot(shotWidth, shotHeight) is 540x397,
365
346
  // this.getDeviceSize(screenWidth, screenHeight) is 1080x1920.
366
347
  // The ratio would then be {xScale: 0.5, yScale: 0.2}.
367
- // In this case, we must should `yScale: 0.2` as scaleFactor, because
348
+ // In this case, we must use `yScale: 0.2` as scaleFactor, because
368
349
  // if we select the xScale, the height will be bigger than real screenshot size
369
350
  // which is used to image comparison by OpenCV as a base image.
370
351
  // All of this is primarily useful when the screenshot is a horizontal slice taken out of the
371
352
  // screen (for example not including top/bottom nav bars)
372
- const xScale = (1.0 * shotWidth) / screenWidth;
373
- const yScale = (1.0 * shotHeight) / screenHeight;
374
- const scaleFactor = xScale >= yScale ? yScale : xScale;
353
+ const xScale = (1.0 * shotWidth) / screenSize.width;
354
+ const yScale = (1.0 * shotHeight) / screenSize.height;
355
+ const scaleFactor = Math.min(xScale, yScale);
356
+ const [newWidth, newHeight] = [shotWidth * scaleFactor, shotHeight * scaleFactor]
357
+ .map((x) => parseInt(x, 10));
375
358
 
376
359
  log.warn(
377
- `Resizing screenshot to ${shotWidth * scaleFactor}x${shotHeight * scaleFactor} to match ` +
378
- `screen aspect ratio so that image element coordinates have a ` +
379
- `greater chance of being correct.`
360
+ `Resizing screenshot to ${newWidth}x${newHeight} to match ` +
361
+ `screen aspect ratio so that image element coordinates have a ` +
362
+ `greater chance of being correct.`
380
363
  );
381
- imgObj = imgObj.resize(shotWidth * scaleFactor, shotHeight * scaleFactor);
364
+ imgObj = imgObj.resize({
365
+ width: newWidth,
366
+ height: newHeight,
367
+ fit: 'fill',
368
+ });
382
369
 
383
370
  scale.xScale *= scaleFactor;
384
371
  scale.yScale *= scaleFactor;
385
-
386
- shotWidth = imgObj.bitmap.width;
387
- shotHeight = imgObj.bitmap.height;
372
+ [shotWidth, shotHeight] = [newWidth, newHeight];
388
373
  }
389
374
 
390
375
  // Resize based on the screen dimensions only if both width and height are mismatched
391
376
  // since except for that, it might be a situation which is different window rect and
392
377
  // screenshot size like `@driver.window_rect #=>x=0, y=0, width=1080, height=1794` and
393
378
  // `"deviceScreenSize"=>"1080x1920"`
394
- if (screenWidth !== shotWidth && screenHeight !== shotHeight) {
379
+ if (screenSize.width !== shotWidth && screenSize.height !== shotHeight) {
395
380
  log.info(
396
381
  `Scaling screenshot from ${shotWidth}x${shotHeight} to match ` +
397
- `screen at ${screenWidth}x${screenHeight}`
382
+ `screen at ${screenSize.width}x${screenSize.height}`
398
383
  );
399
- imgObj = imgObj.resize(screenWidth, screenHeight);
400
-
401
- scale.xScale *= (1.0 * screenWidth) / shotWidth;
402
- scale.yScale *= (1.0 * screenHeight) / shotHeight;
384
+ imgObj = imgObj.resize({
385
+ width: parseInt(screenSize.width, 10),
386
+ height: parseInt(screenSize.height, 10),
387
+ fit: 'fill',
388
+ });
389
+
390
+ scale.xScale *= (1.0 * screenSize.width) / shotWidth;
391
+ scale.yScale *= (1.0 * screenSize.height) / shotHeight;
403
392
  }
404
393
 
405
- b64Screenshot = (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
406
- return {b64Screenshot, scale};
394
+ return {
395
+ screenshot: await imgObj.toBuffer(),
396
+ scale,
397
+ };
407
398
  }
408
399
 
409
400
  /**
@@ -411,7 +402,7 @@ export default class ImageElementFinder {
411
402
  * @property {boolean} fixImageTemplateScale - fixImageTemplateScale in device-settings
412
403
  * @property {number} defaultImageTemplateScale - defaultImageTemplateScale in device-settings
413
404
  * @property {boolean} ignoreDefaultImageTemplateScale - Ignore defaultImageTemplateScale if it has true.
414
- * If b64Template has been scaled to defaultImageTemplateScale or should ignore the scale,
405
+ * If the template has been scaled to defaultImageTemplateScale or should ignore the scale,
415
406
  * this parameter should be true. e.g. click in image-element module
416
407
  * @property {number} xScale - Scale ratio for width
417
408
  * @property {number} yScale - Scale ratio for height
@@ -421,20 +412,19 @@ export default class ImageElementFinder {
421
412
  * Get a image that will be used for template maching.
422
413
  * Returns scaled image if scale ratio is provided.
423
414
  *
424
- *
425
- * @param {string} b64Template - base64-encoded image used as a template to be
415
+ * @param {Buffer} template - image used as a template to be
426
416
  * matched in the screenshot
427
417
  * @param {ImageTemplateSettings} opts - Image template scale related options
428
418
  *
429
- * @returns {Promise<string>} base64-encoded scaled template screenshot
419
+ * @returns {Promise<Buffer>} scaled template screenshot
430
420
  */
431
- async fixImageTemplateScale(b64Template, opts) {
421
+ async fixImageTemplateScale(template, opts) {
432
422
  if (!opts) {
433
- return b64Template;
423
+ return template;
434
424
  }
435
425
 
436
426
  let {
437
- fixImageTemplateScale = false,
427
+ fixImageTemplateScale: fixTplScale = false,
438
428
  defaultImageTemplateScale = DEFAULT_TEMPLATE_IMAGE_SCALE,
439
429
  ignoreDefaultImageTemplateScale = false,
440
430
  xScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
@@ -446,12 +436,12 @@ export default class ImageElementFinder {
446
436
  }
447
437
 
448
438
  // Default
449
- if (defaultImageTemplateScale === DEFAULT_TEMPLATE_IMAGE_SCALE && !fixImageTemplateScale) {
450
- return b64Template;
439
+ if (defaultImageTemplateScale === DEFAULT_TEMPLATE_IMAGE_SCALE && !fixTplScale) {
440
+ return template;
451
441
  }
452
442
 
453
443
  // Calculate xScale and yScale Appium should scale
454
- if (fixImageTemplateScale) {
444
+ if (fixTplScale) {
455
445
  xScale *= defaultImageTemplateScale;
456
446
  yScale *= defaultImageTemplateScale;
457
447
  } else {
@@ -460,7 +450,7 @@ export default class ImageElementFinder {
460
450
 
461
451
  // xScale and yScale can be NaN if defaultImageTemplateScale is string, for example
462
452
  if (!parseFloat(String(xScale)) || !parseFloat(String(yScale))) {
463
- return b64Template;
453
+ return template;
464
454
  }
465
455
 
466
456
  // Return if the scale is default, 1, value
@@ -474,26 +464,27 @@ export default class ImageElementFinder {
474
464
  )
475
465
  )
476
466
  ) {
477
- return b64Template;
467
+ return template;
478
468
  }
479
469
 
480
- let imgTempObj = await imageUtil.getJimpImage(b64Template);
481
- let {width: baseTempWidth, height: baseTempHeigh} = imgTempObj.bitmap;
470
+ let imgObj = sharp(template);
471
+ const {width: baseTempWidth, height: baseTempHeigh} = await imgObj.metadata();
482
472
 
483
473
  const scaledWidth = baseTempWidth * xScale;
484
474
  const scaledHeight = baseTempHeigh * yScale;
485
475
  log.info(
486
- `Scaling template image from ${baseTempWidth}x${baseTempHeigh}` +
487
- ` to ${scaledWidth}x${scaledHeight}`
476
+ `Scaling template image from ${baseTempWidth}x${baseTempHeigh} to ${scaledWidth}x${scaledHeight}`
488
477
  );
489
478
  log.info(`The ratio is ${xScale} and ${yScale}`);
490
- imgTempObj = await imgTempObj.resize(scaledWidth, scaledHeight);
491
- return (await imgTempObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
479
+ imgObj = imgObj.resize({
480
+ width: parseInt(scaledWidth, 10),
481
+ height: parseInt(scaledHeight, 10),
482
+ fit: 'fill',
483
+ });
484
+ return await imgObj.toBuffer();
492
485
  }
493
486
  }
494
487
 
495
- export {W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAGE_TEMPLATE_SCALE};
496
-
497
488
  /**
498
489
  * @typedef {import('@appium/types').ExternalDriver} ExternalDriver
499
490
  * @typedef {import('@appium/types').Element} Element
@@ -501,7 +492,7 @@ export {W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAG
501
492
 
502
493
  /**
503
494
  * @typedef Screenshot
504
- * @property {string} b64Screenshot - base64 based screenshot string
495
+ * @property {Buffer} screenshot - screenshot image as PNG
505
496
  */
506
497
 
507
498
  /**