@appium/images-plugin 1.2.3 → 1.3.2
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/LICENSE +2 -2
- package/README.md +1 -1
- package/build/lib/compare.d.ts +28 -0
- package/build/lib/compare.d.ts.map +1 -0
- package/build/lib/compare.js +60 -76
- package/build/lib/finder.d.ts +163 -0
- package/build/lib/finder.d.ts.map +1 -0
- package/build/lib/finder.js +326 -376
- package/build/lib/image-element.d.ts +108 -0
- package/build/lib/image-element.d.ts.map +1 -0
- package/build/lib/image-element.js +194 -238
- package/build/lib/logger.d.ts +3 -0
- package/build/lib/logger.d.ts.map +1 -0
- package/build/lib/logger.js +15 -5
- package/build/lib/plugin.d.ts +25 -0
- package/build/lib/plugin.d.ts.map +1 -0
- package/build/lib/plugin.js +88 -86
- package/build/tsconfig.tsbuildinfo +1 -0
- package/index.js +1 -3
- package/lib/compare.js +34 -14
- package/lib/finder.js +141 -83
- package/lib/image-element.js +69 -65
- package/lib/logger.js +3 -1
- package/lib/plugin.js +14 -15
- package/package.json +36 -23
- package/build/index.js +0 -27
- package/build/test/e2e/plugin-e2e-specs.js +0 -77
- package/build/test/fixtures/appstore.png +0 -0
- package/build/test/fixtures/img1.png +0 -0
- package/build/test/fixtures/img2.png +0 -0
- package/build/test/fixtures/img2_part.png +0 -0
- package/build/test/fixtures/index.js +0 -24
- package/build/test/unit/basic-specs.js +0 -16
- package/build/test/unit/finder-specs.js +0 -409
- package/build/test/unit/image-element-specs.js +0 -320
- package/build/test/unit/plugin-specs.js +0 -199
package/lib/finder.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import LRU from 'lru-cache';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
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';
|
|
8
11
|
import log from './logger';
|
|
9
12
|
|
|
10
13
|
const MJSONWP_ELEMENT_KEY = 'ELEMENT';
|
|
@@ -13,7 +16,8 @@ const DEFAULT_FIX_IMAGE_TEMPLATE_SCALE = 1;
|
|
|
13
16
|
// Used to compare ratio and screen width
|
|
14
17
|
// Pixel is basically under 1080 for example. 100K is probably enough fo a while.
|
|
15
18
|
const FLOAT_PRECISION = 100000;
|
|
16
|
-
const
|
|
19
|
+
const MAX_CACHE_ITEMS = 100;
|
|
20
|
+
const MAX_CACHE_SIZE_BYTES = 1024 * 1024 * 40; // 40mb
|
|
17
21
|
|
|
18
22
|
const DEFAULT_SETTINGS = {
|
|
19
23
|
// value between 0 and 1 representing match strength, below which an image
|
|
@@ -69,26 +73,42 @@ const DEFAULT_SETTINGS = {
|
|
|
69
73
|
};
|
|
70
74
|
|
|
71
75
|
export default class ImageElementFinder {
|
|
72
|
-
|
|
76
|
+
/** @type {ExternalDriver} */
|
|
77
|
+
driver;
|
|
78
|
+
|
|
79
|
+
/** @type {LRU<string,ImageElement>} */
|
|
80
|
+
imgElCache;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
*
|
|
84
|
+
* @param {ExternalDriver} driver
|
|
85
|
+
* @param {number} [maxSize]
|
|
86
|
+
*/
|
|
87
|
+
constructor(driver, maxSize = MAX_CACHE_SIZE_BYTES) {
|
|
73
88
|
this.driver = driver;
|
|
74
89
|
this.imgElCache = new LRU({
|
|
75
|
-
max,
|
|
76
|
-
|
|
90
|
+
max: MAX_CACHE_ITEMS,
|
|
91
|
+
maxSize,
|
|
92
|
+
sizeCalculation: (el) => el.template.length,
|
|
77
93
|
});
|
|
78
94
|
}
|
|
79
95
|
|
|
80
|
-
setDriver
|
|
96
|
+
setDriver(driver) {
|
|
81
97
|
this.driver = driver;
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
|
|
100
|
+
/**
|
|
101
|
+
* @param {ImageElement} imgEl
|
|
102
|
+
* @returns {Element}
|
|
103
|
+
*/
|
|
104
|
+
registerImageElement(imgEl) {
|
|
85
105
|
this.imgElCache.set(imgEl.id, imgEl);
|
|
86
106
|
const protoKey = this.driver.isW3CProtocol() ? W3C_ELEMENT_KEY : MJSONWP_ELEMENT_KEY;
|
|
87
107
|
return imgEl.asElement(protoKey);
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
/**
|
|
91
|
-
* @typedef
|
|
111
|
+
* @typedef FindByImageOptions
|
|
92
112
|
* @property {boolean} [shouldCheckStaleness=false] - whether this call to find an
|
|
93
113
|
* image is merely to check staleness. If so we can bypass a lot of logic
|
|
94
114
|
* @property {boolean} [multiple=false] - Whether we are finding one element or
|
|
@@ -104,26 +124,25 @@ export default class ImageElementFinder {
|
|
|
104
124
|
*
|
|
105
125
|
* @param {string} b64Template - base64-encoded image used as a template to be
|
|
106
126
|
* matched in the screenshot
|
|
107
|
-
* @param {FindByImageOptions} - additional options
|
|
127
|
+
* @param {FindByImageOptions} opts - additional options
|
|
108
128
|
*
|
|
109
|
-
* @returns {
|
|
129
|
+
* @returns {Promise<Element|Element[]|ImageElement>} - WebDriver element with a special id prefix
|
|
110
130
|
*/
|
|
111
|
-
async findByImage
|
|
112
|
-
|
|
113
|
-
multiple = false,
|
|
114
|
-
|
|
115
|
-
}) {
|
|
131
|
+
async findByImage(
|
|
132
|
+
b64Template,
|
|
133
|
+
{shouldCheckStaleness = false, multiple = false, ignoreDefaultImageTemplateScale = false}
|
|
134
|
+
) {
|
|
116
135
|
if (!this.driver) {
|
|
117
136
|
throw new Error(`Can't find without a driver!`);
|
|
118
137
|
}
|
|
119
|
-
const settings =
|
|
138
|
+
const settings = {...DEFAULT_SETTINGS, ...this.driver.settings.getSettings()};
|
|
120
139
|
const {
|
|
121
140
|
imageMatchThreshold: threshold,
|
|
122
141
|
imageMatchMethod,
|
|
123
142
|
fixImageTemplateSize,
|
|
124
143
|
fixImageTemplateScale,
|
|
125
144
|
defaultImageTemplateScale,
|
|
126
|
-
getMatchedImageResult: visualize
|
|
145
|
+
getMatchedImageResult: visualize,
|
|
127
146
|
} = settings;
|
|
128
147
|
|
|
129
148
|
log.info(`Finding image element with match threshold ${threshold}`);
|
|
@@ -137,18 +156,22 @@ export default class ImageElementFinder {
|
|
|
137
156
|
// will not work unless we do. But because it requires some potentially
|
|
138
157
|
// expensive commands, only do this if the user has requested it in settings.
|
|
139
158
|
if (fixImageTemplateSize) {
|
|
140
|
-
b64Template = await this.ensureTemplateSize(b64Template, screenWidth,
|
|
141
|
-
screenHeight);
|
|
159
|
+
b64Template = await this.ensureTemplateSize(b64Template, screenWidth, screenHeight);
|
|
142
160
|
}
|
|
143
161
|
|
|
144
162
|
const results = [];
|
|
145
163
|
const condition = async () => {
|
|
146
164
|
try {
|
|
147
|
-
const {b64Screenshot, scale} = await this.getScreenshotForImageFind(
|
|
165
|
+
const {b64Screenshot, scale} = await this.getScreenshotForImageFind(
|
|
166
|
+
screenWidth,
|
|
167
|
+
screenHeight
|
|
168
|
+
);
|
|
148
169
|
|
|
149
170
|
b64Template = await this.fixImageTemplateScale(b64Template, {
|
|
150
|
-
defaultImageTemplateScale,
|
|
151
|
-
|
|
171
|
+
defaultImageTemplateScale,
|
|
172
|
+
ignoreDefaultImageTemplateScale,
|
|
173
|
+
fixImageTemplateScale,
|
|
174
|
+
...scale,
|
|
152
175
|
});
|
|
153
176
|
|
|
154
177
|
const comparisonOpts = {
|
|
@@ -160,18 +183,20 @@ export default class ImageElementFinder {
|
|
|
160
183
|
comparisonOpts.method = imageMatchMethod;
|
|
161
184
|
}
|
|
162
185
|
if (multiple) {
|
|
163
|
-
results.push(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
186
|
+
results.push(
|
|
187
|
+
...(await compareImages(
|
|
188
|
+
MATCH_TEMPLATE_MODE,
|
|
189
|
+
b64Screenshot,
|
|
190
|
+
b64Template,
|
|
191
|
+
comparisonOpts
|
|
192
|
+
))
|
|
193
|
+
);
|
|
167
194
|
} else {
|
|
168
|
-
results.push(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
comparisonOpts));
|
|
195
|
+
results.push(
|
|
196
|
+
await compareImages(MATCH_TEMPLATE_MODE, b64Screenshot, b64Template, comparisonOpts)
|
|
197
|
+
);
|
|
172
198
|
}
|
|
173
199
|
return true;
|
|
174
|
-
|
|
175
200
|
} catch (err) {
|
|
176
201
|
// if compareImages fails, we'll get a specific error, but we should
|
|
177
202
|
// retry, so trap that and just return false to trigger the next round of
|
|
@@ -226,47 +251,42 @@ export default class ImageElementFinder {
|
|
|
226
251
|
* Ensure that the image template sent in for a find is of a suitable size
|
|
227
252
|
*
|
|
228
253
|
* @param {string} b64Template - base64-encoded image
|
|
229
|
-
* @param {
|
|
230
|
-
* @param {
|
|
254
|
+
* @param {number} screenWidth - width of screen
|
|
255
|
+
* @param {number} screenHeight - height of screen
|
|
231
256
|
*
|
|
232
|
-
* @returns {string} base64-encoded image, potentially resized
|
|
257
|
+
* @returns {Promise<string>} base64-encoded image, potentially resized
|
|
233
258
|
*/
|
|
234
|
-
async ensureTemplateSize
|
|
259
|
+
async ensureTemplateSize(b64Template, screenWidth, screenHeight) {
|
|
235
260
|
let imgObj = await imageUtil.getJimpImage(b64Template);
|
|
236
261
|
let {width: tplWidth, height: tplHeight} = imgObj.bitmap;
|
|
237
262
|
|
|
238
|
-
log.info(
|
|
263
|
+
log.info(
|
|
264
|
+
`Template image is ${tplWidth}x${tplHeight}. Screen size is ${screenWidth}x${screenHeight}`
|
|
265
|
+
);
|
|
239
266
|
// if the template fits inside the screen dimensions, we're good
|
|
240
267
|
if (tplWidth <= screenWidth && tplHeight <= screenHeight) {
|
|
241
268
|
return b64Template;
|
|
242
269
|
}
|
|
243
270
|
|
|
244
|
-
log.info(
|
|
245
|
-
|
|
271
|
+
log.info(
|
|
272
|
+
`Scaling template image from ${tplWidth}x${tplHeight} to match ` +
|
|
273
|
+
`screen at ${screenWidth}x${screenHeight}`
|
|
274
|
+
);
|
|
246
275
|
// otherwise, scale it to fit inside the screen dimensions
|
|
247
276
|
imgObj = imgObj.scaleToFit(screenWidth, screenHeight);
|
|
248
277
|
return (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
|
|
249
278
|
}
|
|
250
279
|
|
|
251
|
-
/**
|
|
252
|
-
* @typedef {Object} Screenshot
|
|
253
|
-
* @property {string} b64Screenshot - base64 based screenshot string
|
|
254
|
-
*/
|
|
255
|
-
/**
|
|
256
|
-
* @typedef {Object} ScreenshotScale
|
|
257
|
-
* @property {float} xScale - Scale ratio for width
|
|
258
|
-
* @property {float} yScale - Scale ratio for height
|
|
259
|
-
*/
|
|
260
280
|
/**
|
|
261
281
|
* Get the screenshot image that will be used for find by element, potentially
|
|
262
282
|
* altering it in various ways based on user-requested settings
|
|
263
283
|
*
|
|
264
|
-
* @param {
|
|
265
|
-
* @param {
|
|
284
|
+
* @param {number} screenWidth - width of screen
|
|
285
|
+
* @param {number} screenHeight - height of screen
|
|
266
286
|
*
|
|
267
|
-
* @returns {Screenshot
|
|
287
|
+
* @returns {Promise<Screenshot & {scale?: ScreenshotScale}>} base64-encoded screenshot and ScreenshotScale
|
|
268
288
|
*/
|
|
269
|
-
async getScreenshotForImageFind
|
|
289
|
+
async getScreenshotForImageFind(screenWidth, screenHeight) {
|
|
270
290
|
if (!this.driver.getScreenshot) {
|
|
271
291
|
throw new Error("This driver does not support the required 'getScreenshot' command");
|
|
272
292
|
}
|
|
@@ -283,8 +303,10 @@ export default class ImageElementFinder {
|
|
|
283
303
|
}
|
|
284
304
|
|
|
285
305
|
if (screenWidth < 1 || screenHeight < 1) {
|
|
286
|
-
log.warn(
|
|
287
|
-
`
|
|
306
|
+
log.warn(
|
|
307
|
+
`The retrieved screen size ${screenWidth}x${screenHeight} does ` +
|
|
308
|
+
`not seem to be valid. No changes will be applied to the screenshot`
|
|
309
|
+
);
|
|
288
310
|
return {b64Screenshot};
|
|
289
311
|
}
|
|
290
312
|
|
|
@@ -296,8 +318,10 @@ export default class ImageElementFinder {
|
|
|
296
318
|
let {width: shotWidth, height: shotHeight} = imgObj.bitmap;
|
|
297
319
|
|
|
298
320
|
if (shotWidth < 1 || shotHeight < 1) {
|
|
299
|
-
log.warn(
|
|
300
|
-
`
|
|
321
|
+
log.warn(
|
|
322
|
+
`The retrieved screenshot size ${shotWidth}x${shotHeight} does ` +
|
|
323
|
+
`not seem to be valid. No changes will be applied to the screenshot`
|
|
324
|
+
);
|
|
301
325
|
return {b64Screenshot};
|
|
302
326
|
}
|
|
303
327
|
|
|
@@ -319,13 +343,17 @@ export default class ImageElementFinder {
|
|
|
319
343
|
const screenAR = screenWidth / screenHeight;
|
|
320
344
|
const shotAR = shotWidth / shotHeight;
|
|
321
345
|
if (Math.round(screenAR * FLOAT_PRECISION) === Math.round(shotAR * FLOAT_PRECISION)) {
|
|
322
|
-
log.info(
|
|
323
|
-
`
|
|
346
|
+
log.info(
|
|
347
|
+
`Screenshot aspect ratio '${shotAR}' (${shotWidth}x${shotHeight}) matched ` +
|
|
348
|
+
`screen aspect ratio '${screenAR}' (${screenWidth}x${screenHeight})`
|
|
349
|
+
);
|
|
324
350
|
} else {
|
|
325
|
-
log.warn(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
351
|
+
log.warn(
|
|
352
|
+
`When trying to find an element, determined that the screen ` +
|
|
353
|
+
`aspect ratio and screenshot aspect ratio are different. Screen ` +
|
|
354
|
+
`is ${screenWidth}x${screenHeight} whereas screenshot is ` +
|
|
355
|
+
`${shotWidth}x${shotHeight}.`
|
|
356
|
+
);
|
|
329
357
|
|
|
330
358
|
// In the case where the x-scale and y-scale are different, we need to decide
|
|
331
359
|
// which one to respect, otherwise the screenshot and template will end up
|
|
@@ -342,9 +370,11 @@ export default class ImageElementFinder {
|
|
|
342
370
|
const yScale = (1.0 * shotHeight) / screenHeight;
|
|
343
371
|
const scaleFactor = xScale >= yScale ? yScale : xScale;
|
|
344
372
|
|
|
345
|
-
log.warn(
|
|
346
|
-
|
|
347
|
-
|
|
373
|
+
log.warn(
|
|
374
|
+
`Resizing screenshot to ${shotWidth * scaleFactor}x${shotHeight * scaleFactor} to match ` +
|
|
375
|
+
`screen aspect ratio so that image element coordinates have a ` +
|
|
376
|
+
`greater chance of being correct.`
|
|
377
|
+
);
|
|
348
378
|
imgObj = imgObj.resize(shotWidth * scaleFactor, shotHeight * scaleFactor);
|
|
349
379
|
|
|
350
380
|
scale.xScale *= scaleFactor;
|
|
@@ -359,8 +389,10 @@ export default class ImageElementFinder {
|
|
|
359
389
|
// screenshot size like `@driver.window_rect #=>x=0, y=0, width=1080, height=1794` and
|
|
360
390
|
// `"deviceScreenSize"=>"1080x1920"`
|
|
361
391
|
if (screenWidth !== shotWidth && screenHeight !== shotHeight) {
|
|
362
|
-
log.info(
|
|
363
|
-
|
|
392
|
+
log.info(
|
|
393
|
+
`Scaling screenshot from ${shotWidth}x${shotHeight} to match ` +
|
|
394
|
+
`screen at ${screenWidth}x${screenHeight}`
|
|
395
|
+
);
|
|
364
396
|
imgObj = imgObj.resize(screenWidth, screenHeight);
|
|
365
397
|
|
|
366
398
|
scale.xScale *= (1.0 * screenWidth) / shotWidth;
|
|
@@ -372,14 +404,14 @@ export default class ImageElementFinder {
|
|
|
372
404
|
}
|
|
373
405
|
|
|
374
406
|
/**
|
|
375
|
-
* @typedef
|
|
407
|
+
* @typedef ImageTemplateSettings
|
|
376
408
|
* @property {boolean} fixImageTemplateScale - fixImageTemplateScale in device-settings
|
|
377
|
-
* @property {
|
|
409
|
+
* @property {number} defaultImageTemplateScale - defaultImageTemplateScale in device-settings
|
|
378
410
|
* @property {boolean} ignoreDefaultImageTemplateScale - Ignore defaultImageTemplateScale if it has true.
|
|
379
411
|
* If b64Template has been scaled to defaultImageTemplateScale or should ignore the scale,
|
|
380
412
|
* this parameter should be true. e.g. click in image-element module
|
|
381
|
-
* @property {
|
|
382
|
-
* @property {
|
|
413
|
+
* @property {number} xScale - Scale ratio for width
|
|
414
|
+
* @property {number} yScale - Scale ratio for height
|
|
383
415
|
|
|
384
416
|
*/
|
|
385
417
|
/**
|
|
@@ -391,9 +423,9 @@ export default class ImageElementFinder {
|
|
|
391
423
|
* matched in the screenshot
|
|
392
424
|
* @param {ImageTemplateSettings} opts - Image template scale related options
|
|
393
425
|
*
|
|
394
|
-
* @returns {string} base64-encoded scaled template screenshot
|
|
426
|
+
* @returns {Promise<string>} base64-encoded scaled template screenshot
|
|
395
427
|
*/
|
|
396
|
-
async fixImageTemplateScale
|
|
428
|
+
async fixImageTemplateScale(b64Template, opts) {
|
|
397
429
|
if (!opts) {
|
|
398
430
|
return b64Template;
|
|
399
431
|
}
|
|
@@ -403,7 +435,7 @@ export default class ImageElementFinder {
|
|
|
403
435
|
defaultImageTemplateScale = DEFAULT_TEMPLATE_IMAGE_SCALE,
|
|
404
436
|
ignoreDefaultImageTemplateScale = false,
|
|
405
437
|
xScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
|
|
406
|
-
yScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE
|
|
438
|
+
yScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
|
|
407
439
|
} = opts;
|
|
408
440
|
|
|
409
441
|
if (ignoreDefaultImageTemplateScale) {
|
|
@@ -424,13 +456,21 @@ export default class ImageElementFinder {
|
|
|
424
456
|
}
|
|
425
457
|
|
|
426
458
|
// xScale and yScale can be NaN if defaultImageTemplateScale is string, for example
|
|
427
|
-
if (!parseFloat(xScale) || !parseFloat(yScale)) {
|
|
459
|
+
if (!parseFloat(String(xScale)) || !parseFloat(String(yScale))) {
|
|
428
460
|
return b64Template;
|
|
429
461
|
}
|
|
430
462
|
|
|
431
463
|
// Return if the scale is default, 1, value
|
|
432
|
-
if (
|
|
433
|
-
|
|
464
|
+
if (
|
|
465
|
+
Math.round(xScale * FLOAT_PRECISION) ===
|
|
466
|
+
Math.round(DEFAULT_FIX_IMAGE_TEMPLATE_SCALE * FLOAT_PRECISION) &&
|
|
467
|
+
Math.round(
|
|
468
|
+
Number(
|
|
469
|
+
yScale * FLOAT_PRECISION ===
|
|
470
|
+
Math.round(DEFAULT_FIX_IMAGE_TEMPLATE_SCALE * FLOAT_PRECISION)
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
) {
|
|
434
474
|
return b64Template;
|
|
435
475
|
}
|
|
436
476
|
|
|
@@ -439,12 +479,30 @@ export default class ImageElementFinder {
|
|
|
439
479
|
|
|
440
480
|
const scaledWidth = baseTempWidth * xScale;
|
|
441
481
|
const scaledHeight = baseTempHeigh * yScale;
|
|
442
|
-
log.info(
|
|
443
|
-
|
|
482
|
+
log.info(
|
|
483
|
+
`Scaling template image from ${baseTempWidth}x${baseTempHeigh}` +
|
|
484
|
+
` to ${scaledWidth}x${scaledHeight}`
|
|
485
|
+
);
|
|
444
486
|
log.info(`The ratio is ${xScale} and ${yScale}`);
|
|
445
487
|
imgTempObj = await imgTempObj.resize(scaledWidth, scaledHeight);
|
|
446
488
|
return (await imgTempObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
|
|
447
489
|
}
|
|
448
490
|
}
|
|
449
491
|
|
|
450
|
-
export {
|
|
492
|
+
export {W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAGE_TEMPLATE_SCALE};
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
|
|
496
|
+
* @typedef {import('@appium/types').Element} Element
|
|
497
|
+
*/
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* @typedef Screenshot
|
|
501
|
+
* @property {string} b64Screenshot - base64 based screenshot string
|
|
502
|
+
*/
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* @typedef ScreenshotScale
|
|
506
|
+
* @property {number} xScale - Scale ratio for width
|
|
507
|
+
* @property {number} yScale - Scale ratio for height
|
|
508
|
+
*/
|