@applitools/screenshoter 3.2.9 → 3.3.0

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.
Files changed (98) hide show
  1. package/.bongo/dry-run/package-lock.json +11 -11
  2. package/.bongo/dry-run.tgz +0 -0
  3. package/CHANGELOG.md +6 -0
  4. package/index.js +2 -1
  5. package/logs/screenshot_2021_12_16_19_06_06_332Z_full_app_failed_1639681566332.png +0 -0
  6. package/logs/screenshot_2021_12_16_19_15_59_935Z_full_app_failed_1639682159935.png +0 -0
  7. package/logs/screenshot_2021_12_16_19_33_20_679Z_full_app_failed_1639683200679.png +0 -0
  8. package/logs/screenshot_2021_12_16_19_37_22_120Z_ios_viewport_failed.png +0 -0
  9. package/logs/screenshot_2021_12_16_19_38_09_461Z_ios_full_page_failed.png +0 -0
  10. package/logs/screenshot_2021_12_16_19_59_45_182Z_ios_viewport_failed.png +0 -0
  11. package/package.json +5 -5
  12. package/src/find-image-pattern.js +10 -38
  13. package/src/image.js +104 -64
  14. package/src/take-screenshot.js +136 -160
  15. package/src/take-simple-screenshot.js +25 -0
  16. package/src/take-stitched-screenshot.js +32 -52
  17. package/src/take-viewport-screenshot.js +179 -16
  18. package/test/e2e/android.spec.js +42 -11
  19. package/test/e2e/external.spec.js +81 -10
  20. package/test/e2e/ios.spec.js +48 -10
  21. package/test/e2e/web-ios.spec.js +3 -5
  22. package/test/e2e/web.spec.js +20 -15
  23. package/test/fixtures/android/app-fully-non-scrollable.png +0 -0
  24. package/test/fixtures/android/app-fully-recycler.png +0 -0
  25. package/test/fixtures/android/app-fully-scroll-statusbar.png +0 -0
  26. package/test/fixtures/android/app-fully-scroll.png +0 -0
  27. package/test/fixtures/android/app-statusbar.png +0 -0
  28. package/test/fixtures/android/x-app-fully-collapsing.png +0 -0
  29. package/test/fixtures/android/x-app-fully-recycler.png +0 -0
  30. package/test/fixtures/android/x-element-fully.png +0 -0
  31. package/test/fixtures/image/{house.combined-higher-wider.png → house.framed-higher-wider.png} +0 -0
  32. package/test/fixtures/image/{house.combined-higher.png → house.framed-higher.png} +0 -0
  33. package/test/fixtures/image/house.framed-shorter-thinner.png +0 -0
  34. package/test/fixtures/image/{house.combined-wider.png → house.framed-wider.png} +0 -0
  35. package/test/fixtures/ios/app-fully-collapsing.png +0 -0
  36. package/test/fixtures/ios/app-fully-collection.png +0 -0
  37. package/test/fixtures/ios/app-fully-overlapped-statusbar.png +0 -0
  38. package/test/fixtures/ios/app-fully-overlapped.png +0 -0
  39. package/test/fixtures/ios/app-fully-scroll-statusbar.png +0 -0
  40. package/test/fixtures/ios/app-fully-scroll.png +0 -0
  41. package/test/fixtures/ios/app-fully-superview.png +0 -0
  42. package/test/fixtures/ios/app-fully-table.png +0 -0
  43. package/test/fixtures/ios/app-statusbar.png +0 -0
  44. package/test/fixtures/ios/app.png +0 -0
  45. package/test/fixtures/ios/element-fully.png +0 -0
  46. package/test/fixtures/ios/element.png +0 -0
  47. package/test/fixtures/ios/region.png +0 -0
  48. package/test/fixtures/pattern/iPad_5th_landscape.png +0 -0
  49. package/test/fixtures/pattern/iPad_5th_portrait.png +0 -0
  50. package/test/fixtures/pattern/iPad_9th_landscape.png +0 -0
  51. package/test/fixtures/pattern/iPad_9th_portrait.png +0 -0
  52. package/test/fixtures/pattern/iPhone_11_landscape.png +0 -0
  53. package/test/fixtures/pattern/iPhone_11_portrait.png +0 -0
  54. package/test/fixtures/pattern/iPhone_13_landscape.png +0 -0
  55. package/test/fixtures/pattern/iPhone_13_portrait.png +0 -0
  56. package/test/fixtures/pattern/iPhone_SE_landscape.png +0 -0
  57. package/test/fixtures/pattern/iPhone_SE_portrait.png +0 -0
  58. package/test/fixtures/pattern/iPhone_XS_portrait_noviewport.png +0 -0
  59. package/test/fixtures/web-ios/page-fully.png +0 -0
  60. package/test/fixtures/web-ios/page.png +0 -0
  61. package/test/it/find-pattern.spec.js +16 -11
  62. package/test/it/image.spec.js +42 -15
  63. package/logs/screenshot_2021_11_14_12_35_00_342Z_full_frame_failed.png +0 -0
  64. package/logs/screenshot_2021_11_14_12_38_00_715Z_frame_failed.png +0 -0
  65. package/logs/screenshot_2021_11_14_12_38_03_866Z_frame_failed.png +0 -0
  66. package/logs/screenshot_2021_11_14_13_02_56_464Z_full_app_failed_1636894976464.png +0 -0
  67. package/logs/screenshot_2021_11_14_13_04_27_904Z_full_app_failed_1636895067904.png +0 -0
  68. package/logs/screenshot_2021_11_14_13_06_13_662Z_full_app_failed_1636895173662.png +0 -0
  69. package/logs/screenshot_2021_11_14_13_06_23_745Z_full_app_failed_1636895183745.png +0 -0
  70. package/logs/screenshot_2021_11_14_13_18_31_571Z_full_app_failed_1636895911571.png +0 -0
  71. package/logs/screenshot_2021_11_14_13_25_54_557Z_viewport_failed_1636896354557.png +0 -0
  72. package/logs/screenshot_2021_11_14_13_29_32_326Z_viewport_failed_1636896572326.png +0 -0
  73. package/logs/screenshot_2021_11_14_13_34_22_483Z_viewport_failed_1636896862483.png +0 -0
  74. package/logs/screenshot_2021_11_14_13_37_25_734Z_viewport_failed_1636897045734.png +0 -0
  75. package/logs/screenshot_2021_11_14_13_42_25_024Z_viewport_failed_1636897345024.png +0 -0
  76. package/logs/screenshot_2021_11_14_13_57_24_366Z_full_app_failed_1636898244366.png +0 -0
  77. package/logs/screenshot_2021_11_14_14_20_42_951Z_full_app_failed_1636899642951.png +0 -0
  78. package/logs/screenshot_2021_11_14_14_31_07_853Z_full_app_failed_1636900267853.png +0 -0
  79. package/logs/screenshot_2021_11_14_14_32_07_195Z_full_app_failed_1636900327195.png +0 -0
  80. package/logs/screenshot_2021_11_14_14_42_16_716Z_full_app_failed_1636900936716.png +0 -0
  81. package/logs/screenshot_2021_11_14_14_47_37_646Z_full_app_failed_1636901257646.png +0 -0
  82. package/logs/screenshot_2021_11_14_14_54_18_522Z_full_app_failed_1636901658522.png +0 -0
  83. package/logs/screenshot_2021_11_14_14_55_36_756Z_full_app_failed_1636901736756.png +0 -0
  84. package/logs/screenshot_2021_11_14_15_00_26_000Z_full_app_failed_1636902026000.png +0 -0
  85. package/logs/screenshot_2021_11_14_15_04_13_598Z_full_app_failed_1636902253598.png +0 -0
  86. package/logs/screenshot_2021_11_14_15_07_37_914Z_full_app_failed_1636902457914.png +0 -0
  87. package/logs/screenshot_2021_11_14_15_12_20_039Z_full_app_failed_1636902740039.png +0 -0
  88. package/logs/screenshot_2021_11_14_15_15_44_401Z_full_app_failed_1636902944401.png +0 -0
  89. package/logs/screenshot_2021_11_14_15_26_23_318Z_viewport_failed_1636903583318.png +0 -0
  90. package/src/calculate-screenshot-regions.js +0 -31
  91. package/src/screenshoter.js +0 -159
  92. package/test/fixtures/pattern/iPad_Air_portrait.png +0 -0
  93. package/test/fixtures/pattern/iPhone_5S_landscape.png +0 -0
  94. package/test/fixtures/pattern/iPhone_XR_perfecto_landscape.png +0 -0
  95. package/test/fixtures/pattern/iPhone_XS_Max_perfecto_landscape.png +0 -0
  96. package/test/fixtures/pattern/iPhone_XS_landscape.png +0 -0
  97. package/test/fixtures/pattern/iPhone_XS_portrait.png +0 -0
  98. package/test/fixtures/pattern/iPhone_X_perfecto_portrait.png +0 -0
@@ -9,12 +9,12 @@
9
9
  }
10
10
  },
11
11
  "node_modules/@applitools/screenshoter": {
12
- "version": "3.2.8",
12
+ "version": "3.2.9",
13
13
  "resolved": "file:../dry-run.tgz",
14
- "integrity": "sha512-GjN+h/vj5/lKFjJPm302jD0xSFIOoSG7gPU/ogEUcCS9l5FDcMW27PRMA/xGlolKrabmCgDHJqVbAXE1WCgjyQ==",
14
+ "integrity": "sha512-a9s+yYo2bQ61Nyt/Enqrjgi7BjHV97c5LrEREO2iFisgdq+SzNHaVOav63UIQ9a5qvMAAetyZgU2NgVtPNNWow==",
15
15
  "license": "SEE LICENSE IN LICENSE",
16
16
  "dependencies": {
17
- "@applitools/snippets": "2.1.7",
17
+ "@applitools/snippets": "2.1.8",
18
18
  "@applitools/utils": "1.2.4",
19
19
  "png-async": "0.9.4"
20
20
  },
@@ -23,9 +23,9 @@
23
23
  }
24
24
  },
25
25
  "node_modules/@applitools/snippets": {
26
- "version": "2.1.7",
27
- "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.1.7.tgz",
28
- "integrity": "sha512-Tr4Gj7Qov/oPy+8WI4oVmmubxqpOzr8P3Wjzpl6rA57xKLg6/TiIg5oZNb4+jEmO2ShjNYLaEwRWHl7kPgb4fw==",
26
+ "version": "2.1.8",
27
+ "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.1.8.tgz",
28
+ "integrity": "sha512-7CGFsbL9vAd6MBiGLVKHpKYywPXN499/fJMzEAVCLuGCY81CIc/PchqPFWrZZKZOY/IV21RJmE3MvqZAeXTIXA==",
29
29
  "engines": {
30
30
  "node": ">=8.9.0"
31
31
  }
@@ -47,17 +47,17 @@
47
47
  "dependencies": {
48
48
  "@applitools/screenshoter": {
49
49
  "version": "file:../dry-run.tgz",
50
- "integrity": "sha512-GjN+h/vj5/lKFjJPm302jD0xSFIOoSG7gPU/ogEUcCS9l5FDcMW27PRMA/xGlolKrabmCgDHJqVbAXE1WCgjyQ==",
50
+ "integrity": "sha512-a9s+yYo2bQ61Nyt/Enqrjgi7BjHV97c5LrEREO2iFisgdq+SzNHaVOav63UIQ9a5qvMAAetyZgU2NgVtPNNWow==",
51
51
  "requires": {
52
- "@applitools/snippets": "2.1.7",
52
+ "@applitools/snippets": "2.1.8",
53
53
  "@applitools/utils": "1.2.4",
54
54
  "png-async": "0.9.4"
55
55
  }
56
56
  },
57
57
  "@applitools/snippets": {
58
- "version": "2.1.7",
59
- "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.1.7.tgz",
60
- "integrity": "sha512-Tr4Gj7Qov/oPy+8WI4oVmmubxqpOzr8P3Wjzpl6rA57xKLg6/TiIg5oZNb4+jEmO2ShjNYLaEwRWHl7kPgb4fw=="
58
+ "version": "2.1.8",
59
+ "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.1.8.tgz",
60
+ "integrity": "sha512-7CGFsbL9vAd6MBiGLVKHpKYywPXN499/fJMzEAVCLuGCY81CIc/PchqPFWrZZKZOY/IV21RJmE3MvqZAeXTIXA=="
61
61
  },
62
62
  "@applitools/utils": {
63
63
  "version": "1.2.4",
Binary file
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@
4
4
  ## Unreleased
5
5
 
6
6
 
7
+ ## 3.3.0 - 2021/12/16
8
+
9
+ - fix ios web screenshots on pages without viewport meta tag
10
+ - improve native apps support
11
+ - updated to @applitools/snippets@2.1.8 (from 2.1.7)
12
+
7
13
  ## 3.2.9 - 2021/11/14
8
14
 
9
15
  - add support of scrollable elements that change its size during scrolling
package/index.js CHANGED
@@ -1 +1,2 @@
1
- module.exports = require('./src/screenshoter')
1
+ module.exports = require('./src/take-screenshot')
2
+ exports.takeScreenshot = require('./src/take-screenshot')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/screenshoter",
3
- "version": "3.2.9",
3
+ "version": "3.3.0",
4
4
  "description": "Applitools universal screenshoter for web and native applications",
5
5
  "keywords": [
6
6
  "applitools",
@@ -49,15 +49,15 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@applitools/snippets": "2.1.7",
52
+ "@applitools/snippets": "2.1.8",
53
53
  "@applitools/utils": "1.2.4",
54
54
  "png-async": "0.9.4"
55
55
  },
56
56
  "devDependencies": {
57
- "@applitools/driver": "1.3.2",
57
+ "@applitools/driver": "1.4.1",
58
58
  "@applitools/sdk-release-kit": "0.13.4",
59
- "@applitools/spec-driver-webdriverio": "1.2.0",
60
- "@applitools/test-utils": "1.0.9",
59
+ "@applitools/spec-driver-webdriverio": "1.2.2",
60
+ "@applitools/test-utils": "1.0.10",
61
61
  "chromedriver": "^95.0.0",
62
62
  "eslint": "^7.9.0",
63
63
  "eslint-plugin-mocha-no-only": "^1.1.1",
@@ -1,60 +1,32 @@
1
1
  function findImagePattern(image, pattern) {
2
2
  for (let pixel = 0; pixel < image.width * image.height; ++pixel) {
3
3
  if (isPattern(image, pixel, pattern)) {
4
- return {
5
- x: (pixel % image.width) - pattern.offset,
6
- y: Math.floor(pixel / image.width) - pattern.offset,
7
- }
4
+ return {x: (pixel % image.width) - pattern.offset, y: Math.floor(pixel / image.width) - pattern.offset}
8
5
  }
9
6
  }
10
7
  return null
11
8
  }
12
9
 
13
10
  function isPattern(image, index, pattern) {
14
- const channels = 4
15
- const roundNumber = pattern.size - Math.floor(pattern.size / 2)
16
- for (const [chunkIndex, chunkColor] of pattern.mask.entries()) {
17
- const pixelOffset = index + image.width * pattern.size * chunkIndex
18
- for (let round = 0; round < roundNumber; ++round) {
19
- const sideLength = pattern.size - round * 2
20
- const stepsNumber = sideLength * channels - channels
21
- const threshold = Math.min((roundNumber - round) * 10 + 10, 100)
22
- for (let step = 0; step < stepsNumber; ++step) {
23
- let pixelIndex = pixelOffset + round + round * image.width
24
-
25
- if (step < sideLength) {
26
- pixelIndex += step
27
- } else if (step < sideLength * 2 - 1) {
28
- pixelIndex += sideLength - 1 + ((step % sideLength) + 1) * image.width
29
- } else if (step < sideLength * 3 - 2) {
30
- pixelIndex += (sideLength - 1) * image.width + (sideLength - (step % sideLength) - 1)
31
- } else {
32
- pixelIndex += (step % sideLength) * image.width
33
- }
34
-
35
- const pixelColor = pixelColorAt(image, pixelIndex, threshold)
36
- if (pixelColor !== chunkColor) {
37
- return false
38
- }
39
- }
11
+ const itemLength = pattern.size * pattern.pixelRatio
12
+ for (const [itemIndex, itemColor] of pattern.mask.entries()) {
13
+ for (let partOffset = itemIndex * itemLength; partOffset < (itemIndex + 1) * itemLength; ++partOffset) {
14
+ const pixelColor = pixelColorAt(image, index + partOffset)
15
+ if (pixelColor !== itemColor) return false
40
16
  }
41
17
  }
42
18
  return true
43
19
  }
44
20
 
45
- function pixelColorAt(image, index, threshold = 0) {
21
+ function pixelColorAt(image, index) {
46
22
  const channels = 4
47
23
  const r = image.data[index * channels]
48
24
  const g = image.data[index * channels + 1]
49
25
  const b = image.data[index * channels + 2]
50
- const rgb = [r, g, b]
51
26
 
52
- // WHITE
53
- if (rgb.every(sub => sub >= 255 - threshold)) return 1
54
- // BLACK
55
- else if (rgb.every(sub => sub <= threshold)) return 0
56
- // OTHER
57
- else return -1
27
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
28
+
29
+ return luminance < 128 ? /* black */ 1 : /* white */ 0
58
30
  }
59
31
 
60
32
  module.exports = findImagePattern
package/src/image.js CHANGED
@@ -6,7 +6,7 @@ const utils = require('@applitools/utils')
6
6
 
7
7
  function makeImage(data) {
8
8
  let image, size
9
- let transforms = {rotate: 0, scale: 1, crop: null}
9
+ let transforms = {rotate: 0, scale: 1, crop: null, modifiers: []}
10
10
 
11
11
  if (utils.types.isBase64(data)) {
12
12
  const buffer = Buffer.from(data, 'base64')
@@ -20,21 +20,19 @@ function makeImage(data) {
20
20
  image = fromBuffer(data)
21
21
  size = extractPngSize(data)
22
22
  } else if (data.isImage) {
23
- image = data.toRaw()
24
- size = data.size
25
23
  transforms = data.transforms
24
+ image = data.toRaw()
25
+ size = utils.geometry.scale(data.size, 1 / transforms.scale)
26
26
  } else if (utils.types.has(data, ['width', 'height'])) {
27
27
  image = fromSize(data)
28
28
  if (data.data) image.data = data.data
29
29
  size = {width: data.width, height: data.height}
30
+ } else if (data.auto) {
31
+ size = {width: -1, height: -1}
30
32
  } else {
31
33
  throw new Error('Unable to create an image abstraction from unknown data')
32
34
  }
33
35
 
34
- if (!transforms.crop) {
35
- transforms.crop = utils.geometry.region({x: 0, y: 0}, size)
36
- }
37
-
38
36
  return {
39
37
  get isImage() {
40
38
  return true
@@ -46,10 +44,10 @@ function makeImage(data) {
46
44
  return {...transforms}
47
45
  },
48
46
  get width() {
49
- return size.width
47
+ return this.size.width
50
48
  },
51
49
  get height() {
52
- return size.height
50
+ return this.size.height
53
51
  },
54
52
  scale(ratio) {
55
53
  transforms.scale *= ratio
@@ -67,7 +65,9 @@ function makeImage(data) {
67
65
  region = utils.geometry.scale(region, 1 / transforms.scale)
68
66
  }
69
67
  region = utils.geometry.rotate(region, transforms.rotate)
70
- transforms.crop = utils.geometry.intersect(transforms.crop, utils.geometry.offset(region, transforms.crop))
68
+ transforms.crop = transforms.crop
69
+ ? utils.geometry.intersect(transforms.crop, utils.geometry.offset(region, transforms.crop))
70
+ : utils.geometry.intersect({x: 0, y: 0, ...size}, region)
71
71
 
72
72
  size = utils.geometry.round(utils.geometry.size(transforms.crop))
73
73
 
@@ -77,18 +77,43 @@ function makeImage(data) {
77
77
  transforms.rotate = (transforms.rotate + degree) % 360
78
78
  return this
79
79
  },
80
- async copy(srcImage, offset) {
81
- const [dst, src] = await Promise.all([this.toObject(), srcImage.toObject()])
82
- image = await copy(dst, src, offset)
80
+ copy(srcImage, offset) {
81
+ const scale = srcImage.transforms.scale
82
+ if (!image) {
83
+ size = {
84
+ width: Math.max(Math.floor((offset.x + srcImage.width) / scale), size.width),
85
+ height: Math.max(Math.floor((offset.y + srcImage.height) / scale), size.height),
86
+ }
87
+ transforms.scale = Math.min(scale, transforms.scale)
88
+ }
89
+ transforms.modifiers.push({
90
+ type: 'copy',
91
+ image: srcImage.scale(scale === transforms.scale ? 1 / scale : scale / transforms.scale).toObject(),
92
+ offset: utils.geometry.scale(offset, 1 / transforms.scale),
93
+ })
94
+
83
95
  return this
84
96
  },
85
- async combine(firstImage, lastImage, region) {
86
- const [first, last, src] = await Promise.all([firstImage.toObject(), lastImage.toObject(), this.toObject()])
87
- image = await combine(first, last, src, region)
88
- size = {width: image.width, height: image.height}
89
- transforms.crop = utils.geometry.region({x: 0, y: 0}, size)
97
+ frame(topImage, bottomImage, region) {
98
+ const scale = topImage.transforms.scale
99
+ const prevSize = size
100
+ region = utils.geometry.scale(region, 1 / scale)
101
+ size = {
102
+ width: Math.floor(topImage.width / scale + Math.max(size.width - region.width, 0)),
103
+ height: Math.floor(topImage.height / scale + Math.max(size.height - region.height, 0)),
104
+ }
105
+ transforms.modifiers.push({
106
+ type: 'frame',
107
+ top: topImage.scale(scale === transforms.scale ? 1 / scale : scale / transforms.scale).toObject(),
108
+ bottom: bottomImage.scale(scale === transforms.scale ? 1 / scale : scale / transforms.scale).toObject(),
109
+ region,
110
+ })
111
+ transforms.added = {width: size.width - prevSize.width, height: size.height - prevSize.height}
90
112
  return this
91
113
  },
114
+ async toRaw() {
115
+ return image
116
+ },
92
117
  async toBuffer() {
93
118
  const image = await this.toObject()
94
119
  return image.data
@@ -99,19 +124,17 @@ function makeImage(data) {
99
124
  async toFile(path) {
100
125
  return toFile(await image, path)
101
126
  },
102
- async toRaw() {
103
- return image
104
- },
105
127
  async toObject() {
106
- image = await transform(await image, transforms)
107
- transforms = {rotate: 0, scale: 1, crop: utils.geometry.region({x: 0, y: 0}, size)}
128
+ image = await transform(image ? await image : size, transforms)
129
+ transforms = {crop: null, scale: 1, rotate: 0, modifiers: []}
108
130
  return image
109
131
  },
110
132
  async debug(debug) {
111
133
  if (!debug || !debug.path) return
112
134
  const timestamp = new Date().toISOString().replace(/[-T:.]/g, '_')
113
135
  const filename = ['screenshot', timestamp, debug.name, debug.suffix].filter(part => part).join('_') + '.png'
114
- return toFile(await transform(await image, transforms), path.join(debug.path, filename)).catch(() => null)
136
+ const transformedImage = await transform(image ? await image : size, transforms)
137
+ return toFile(transformedImage, path.join(debug.path, filename)).catch(() => null)
115
138
  },
116
139
  }
117
140
  }
@@ -164,10 +187,27 @@ async function toFile(image, path) {
164
187
  }
165
188
 
166
189
  async function transform(image, transforms) {
167
- const croppedImage = transforms.crop ? await extract(image, transforms.crop) : image
168
- const scaledImage = transforms.scale !== 1 ? await scale(croppedImage, transforms.scale) : croppedImage
169
- const rotatedImage = transforms.rotate > 0 ? await rotate(scaledImage, transforms.rotate) : scaledImage
170
- return rotatedImage
190
+ if (!image.data) {
191
+ const size = transforms.added
192
+ ? {width: image.width - transforms.added.width, height: image.height - transforms.added.height}
193
+ : image
194
+ image = new png.Image(size)
195
+ }
196
+
197
+ image = await transforms.modifiers.reduce(async (image, modifier) => {
198
+ if (modifier.type === 'copy') {
199
+ return copy(await image, await modifier.image, modifier.offset)
200
+ } else if (modifier.type === 'frame') {
201
+ return frame(await modifier.top, await modifier.bottom, await image, modifier.region)
202
+ } else {
203
+ return image
204
+ }
205
+ }, image)
206
+
207
+ image = transforms.rotate > 0 ? await rotate(image, transforms.rotate) : image
208
+ image = transforms.crop ? await extract(image, transforms.crop) : image
209
+ image = transforms.scale !== 1 ? await scale(image, transforms.scale) : image
210
+ return image
171
211
  }
172
212
 
173
213
  async function scale(image, scaleRatio) {
@@ -288,98 +328,98 @@ async function copy(dstImage, srcImage, offset) {
288
328
  return dstImage
289
329
  }
290
330
 
291
- async function combine(firstImage, lastImage, srcImage, region) {
331
+ async function frame(topImage, bottomImage, srcImage, region) {
292
332
  region = utils.geometry.intersect(
293
- {x: 0, y: 0, width: firstImage.width, height: firstImage.height},
333
+ {x: 0, y: 0, width: topImage.width, height: topImage.height},
294
334
  utils.geometry.round(region),
295
335
  )
296
336
 
297
- if (region.x === 0 && region.y === 0 && region.width >= firstImage.width && region.height >= firstImage.height) {
337
+ if (region.x === 0 && region.y === 0 && region.width >= topImage.width && region.height >= topImage.height) {
298
338
  return srcImage
299
339
  }
300
340
 
301
- if (region.width === srcImage.width && region.height === srcImage.height) {
302
- await copy(firstImage, srcImage, {x: region.x, y: region.y})
303
- return firstImage
341
+ if (region.width >= srcImage.width && region.height >= srcImage.height) {
342
+ await copy(topImage, srcImage, {x: region.x, y: region.y})
343
+ return topImage
304
344
  }
305
345
 
306
346
  const dstImage = new png.Image({
307
- width: firstImage.width - region.width + srcImage.width,
308
- height: firstImage.height - region.height + srcImage.height,
347
+ width: topImage.width + Math.max(srcImage.width - region.width, 0),
348
+ height: topImage.height + Math.max(srcImage.height - region.height, 0),
309
349
  })
310
350
 
311
351
  if (region.width === srcImage.width) {
312
- const topImage = await extract(firstImage, {
352
+ const topExtImage = await extract(topImage, {
313
353
  x: 0,
314
354
  y: 0,
315
- width: firstImage.width,
355
+ width: topImage.width,
316
356
  height: region.y + region.height,
317
357
  })
318
- await copy(dstImage, topImage, {x: 0, y: 0})
358
+ await copy(dstImage, topExtImage, {x: 0, y: 0})
319
359
  } else if (region.height === srcImage.height) {
320
- const leftImage = await extract(firstImage, {
360
+ const leftExtImage = await extract(topImage, {
321
361
  x: 0,
322
362
  y: 0,
323
363
  width: region.x + region.width,
324
- height: firstImage.height,
364
+ height: topImage.height,
325
365
  })
326
- await copy(dstImage, leftImage, {x: 0, y: 0})
366
+ await copy(dstImage, leftExtImage, {x: 0, y: 0})
327
367
  } else {
328
- const topLeftImage = await extract(firstImage, {
368
+ const topLeftExtImage = await extract(topImage, {
329
369
  x: 0,
330
370
  y: 0,
331
371
  width: region.x + region.width,
332
372
  height: region.y + region.height,
333
373
  })
334
- await copy(dstImage, topLeftImage, {x: 0, y: 0})
374
+ await copy(dstImage, topLeftExtImage, {x: 0, y: 0})
335
375
 
336
- const rightExtImage = await extract(firstImage, {
376
+ const rightExtImage = await extract(topImage, {
337
377
  x: region.x + region.width,
338
378
  y: 0,
339
- width: firstImage.width - (region.x + region.width),
379
+ width: topImage.width - (region.x + region.width),
340
380
  height: region.y,
341
381
  })
342
382
  await copy(dstImage, rightExtImage, {x: region.x + region.width, y: 0})
343
383
 
344
- const bottomExtImage = await extract(firstImage, {
384
+ const bottomExtImage = await extract(topImage, {
345
385
  x: 0,
346
386
  y: region.y + region.height,
347
387
  width: region.x,
348
- height: firstImage.height - (region.y + region.height),
388
+ height: topImage.height - (region.y + region.height),
349
389
  })
350
390
  await copy(dstImage, bottomExtImage, {x: 0, y: region.y + region.height})
351
391
  }
352
392
 
353
- if (lastImage.height > region.y + region.height || lastImage.width > region.x + region.width) {
393
+ if (bottomImage.height > region.y + region.height || bottomImage.width > region.x + region.width) {
354
394
  // first image might be higher
355
- const yDiff = firstImage.height - lastImage.height
395
+ const yDiff = topImage.height - bottomImage.height
356
396
  if (region.width === srcImage.width) {
357
- const bottomImage = await extract(lastImage, {
397
+ const bottomExtImage = await extract(bottomImage, {
358
398
  x: 0,
359
399
  y: region.y - yDiff + region.height,
360
- width: lastImage.width,
361
- height: lastImage.height - (region.y - yDiff + region.height),
400
+ width: bottomImage.width,
401
+ height: bottomImage.height - (region.y - yDiff + region.height),
362
402
  })
363
- await copy(dstImage, bottomImage, {x: 0, y: region.y + srcImage.height})
403
+ await copy(dstImage, bottomExtImage, {x: 0, y: region.y + Math.max(srcImage.height, region.height)})
364
404
  } else if (region.height === srcImage.height) {
365
- const rightImage = await extract(lastImage, {
405
+ const rightExtImage = await extract(bottomImage, {
366
406
  x: region.x + region.width,
367
407
  y: 0,
368
- width: lastImage.width - (region.x + region.width),
369
- height: lastImage.height,
408
+ width: bottomImage.width - (region.x + region.width),
409
+ height: bottomImage.height,
370
410
  })
371
- await copy(dstImage, rightImage, {x: region.x + srcImage.width, y: 0})
411
+ await copy(dstImage, rightExtImage, {x: region.x + Math.max(srcImage.width, region.width), y: 0})
372
412
  } else {
373
- const bottomRightImage = await extract(lastImage, {
413
+ const bottomRightExtImage = await extract(bottomImage, {
374
414
  x: region.x,
375
415
  y: region.y - yDiff,
376
- width: lastImage.width - region.x,
377
- height: lastImage.height - (region.y - yDiff),
416
+ width: bottomImage.width - region.x,
417
+ height: bottomImage.height - (region.y - yDiff),
378
418
  })
379
419
 
380
- await copy(dstImage, bottomRightImage, {
381
- x: region.x + srcImage.width - region.width,
382
- y: region.y + srcImage.height - region.height,
420
+ await copy(dstImage, bottomRightExtImage, {
421
+ x: region.x + Math.max(srcImage.width - region.width, 0),
422
+ y: region.y + Math.max(srcImage.height - region.height, 0),
383
423
  })
384
424
  }
385
425
  }