@wdio/image-comparison-core 1.3.0 → 2.0.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 (42) hide show
  1. package/CHANGELOG.md +41 -3
  2. package/dist/helpers/options.d.ts +1 -4
  3. package/dist/helpers/options.d.ts.map +1 -1
  4. package/dist/helpers/options.js +2 -11
  5. package/dist/methods/compareReport.interfaces.d.ts +1 -1
  6. package/dist/methods/compareReport.interfaces.d.ts.map +1 -1
  7. package/dist/methods/createCompareReport.test.js +4 -2
  8. package/dist/methods/images.d.ts +2 -1
  9. package/dist/methods/images.d.ts.map +1 -1
  10. package/dist/methods/images.executeImageCompare.test.js +39 -44
  11. package/dist/methods/images.js +31 -31
  12. package/dist/methods/images.test.js +80 -220
  13. package/dist/methods/processDiffPixels.d.ts +1 -1
  14. package/dist/methods/processDiffPixels.d.ts.map +1 -1
  15. package/dist/methods/processDiffPixels.js +11 -2
  16. package/dist/methods/rectangles.d.ts.map +1 -1
  17. package/dist/methods/rectangles.interfaces.d.ts +1 -1
  18. package/dist/methods/rectangles.interfaces.d.ts.map +1 -1
  19. package/dist/methods/rectangles.js +12 -9
  20. package/dist/pixelmatch/compare.interfaces.d.ts +43 -0
  21. package/dist/pixelmatch/compare.interfaces.d.ts.map +1 -0
  22. package/dist/pixelmatch/compareImages.d.ts.map +1 -0
  23. package/dist/pixelmatch/compareImages.js +173 -0
  24. package/dist/pixelmatch/compareImages.test.d.ts +2 -0
  25. package/dist/pixelmatch/compareImages.test.d.ts.map +1 -0
  26. package/dist/pixelmatch/compareImages.test.js +262 -0
  27. package/dist/utils/imageUtils.d.ts +17 -0
  28. package/dist/utils/imageUtils.d.ts.map +1 -0
  29. package/dist/utils/imageUtils.js +189 -0
  30. package/dist/utils/imageUtils.test.d.ts +2 -0
  31. package/dist/utils/imageUtils.test.d.ts.map +1 -0
  32. package/dist/utils/imageUtils.test.js +277 -0
  33. package/package.json +3 -2
  34. package/dist/resemble/compare.interfaces.d.ts +0 -136
  35. package/dist/resemble/compare.interfaces.d.ts.map +0 -1
  36. package/dist/resemble/compareImages.d.ts.map +0 -1
  37. package/dist/resemble/compareImages.js +0 -21
  38. package/dist/resemble/resemble.jimp.cjs +0 -981
  39. package/dist/resemble/resemble.jimp.d.cts +0 -46
  40. package/dist/resemble/resemble.jimp.d.cts.map +0 -1
  41. /package/dist/{resemble → pixelmatch}/compare.interfaces.js +0 -0
  42. /package/dist/{resemble → pixelmatch}/compareImages.d.ts +0 -0
@@ -0,0 +1,189 @@
1
+ import { decode, encode } from 'fast-png';
2
+ // Convert any channel count to RGBA 8-bit. fast-png may decode RGB, grayscale, or clamped PNGs.
3
+ function toRGBA(data, channels, width, height) {
4
+ if (channels === 4 && (data instanceof Uint8Array || data instanceof Uint8ClampedArray)) {
5
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
6
+ }
7
+ const pixels = width * height;
8
+ const out = new Uint8Array(pixels * 4);
9
+ for (let i = 0; i < pixels; i++) {
10
+ if (channels === 3) {
11
+ out[i * 4] = data[i * 3];
12
+ out[i * 4 + 1] = data[i * 3 + 1];
13
+ out[i * 4 + 2] = data[i * 3 + 2];
14
+ out[i * 4 + 3] = 255;
15
+ }
16
+ else if (channels === 1) {
17
+ const v = data[i];
18
+ out[i * 4] = v;
19
+ out[i * 4 + 1] = v;
20
+ out[i * 4 + 2] = v;
21
+ out[i * 4 + 3] = 255;
22
+ }
23
+ else if (channels === 2) {
24
+ const v = data[i * 2];
25
+ out[i * 4] = v;
26
+ out[i * 4 + 1] = v;
27
+ out[i * 4 + 2] = v;
28
+ out[i * 4 + 3] = data[i * 2 + 1];
29
+ }
30
+ else {
31
+ // 4-channel Uint16Array: downsample 16-bit → 8-bit
32
+ out[i * 4] = data[i * 4] >> 8;
33
+ out[i * 4 + 1] = data[i * 4 + 1] >> 8;
34
+ out[i * 4 + 2] = data[i * 4 + 2] >> 8;
35
+ out[i * 4 + 3] = data[i * 4 + 3] >> 8;
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ export function decodeImage(buffer) {
41
+ const png = decode(buffer);
42
+ return {
43
+ data: toRGBA(png.data, png.channels, png.width, png.height),
44
+ width: png.width,
45
+ height: png.height,
46
+ };
47
+ }
48
+ export function encodeImage(img) {
49
+ return Buffer.from(encode({ data: img.data, width: img.width, height: img.height, channels: 4, depth: 8 }));
50
+ }
51
+ export function toBase64Png(img) {
52
+ return encodeImage(img).toString('base64');
53
+ }
54
+ export function createCanvas(width, height, r = 0, g = 0, b = 0, a = 0) {
55
+ const data = new Uint8Array(width * height * 4);
56
+ if (r !== 0 || g !== 0 || b !== 0 || a !== 0) {
57
+ for (let i = 0; i < data.length; i += 4) {
58
+ data[i] = r;
59
+ data[i + 1] = g;
60
+ data[i + 2] = b;
61
+ data[i + 3] = a;
62
+ }
63
+ }
64
+ return { data, width, height };
65
+ }
66
+ export function cropImage(img, x, y, w, h) {
67
+ const data = new Uint8Array(w * h * 4);
68
+ const rowBytes = w * 4;
69
+ for (let row = 0; row < h; row++) {
70
+ const srcOffset = ((y + row) * img.width + x) * 4;
71
+ data.set(img.data.subarray(srcOffset, srcOffset + rowBytes), row * rowBytes);
72
+ }
73
+ return { data, width: w, height: h };
74
+ }
75
+ // Porter-Duff "over" compositing. Mutates base in place.
76
+ export function compositeImage(base, overlay, offsetX, offsetY, opacity = 1) {
77
+ for (let oy = 0; oy < overlay.height; oy++) {
78
+ const by = oy + offsetY;
79
+ if (by < 0 || by >= base.height) {
80
+ continue;
81
+ }
82
+ for (let ox = 0; ox < overlay.width; ox++) {
83
+ const bx = ox + offsetX;
84
+ if (bx < 0 || bx >= base.width) {
85
+ continue;
86
+ }
87
+ const si = (oy * overlay.width + ox) * 4;
88
+ const di = (by * base.width + bx) * 4;
89
+ const srcA = (overlay.data[si + 3] / 255) * opacity;
90
+ const dstA = base.data[di + 3] / 255;
91
+ const outA = srcA + dstA * (1 - srcA);
92
+ if (outA === 0) {
93
+ continue;
94
+ }
95
+ base.data[di] = Math.round((overlay.data[si] * srcA + base.data[di] * dstA * (1 - srcA)) / outA);
96
+ base.data[di + 1] = Math.round((overlay.data[si + 1] * srcA + base.data[di + 1] * dstA * (1 - srcA)) / outA);
97
+ base.data[di + 2] = Math.round((overlay.data[si + 2] * srcA + base.data[di + 2] * dstA * (1 - srcA)) / outA);
98
+ base.data[di + 3] = Math.round(outA * 255);
99
+ }
100
+ }
101
+ }
102
+ export function resizeBilinear(img, newW, newH) {
103
+ const data = new Uint8Array(newW * newH * 4);
104
+ const xRatio = img.width / newW;
105
+ const yRatio = img.height / newH;
106
+ for (let dy = 0; dy < newH; dy++) {
107
+ const sy = dy * yRatio;
108
+ const y0 = Math.floor(sy);
109
+ const y1 = Math.min(y0 + 1, img.height - 1);
110
+ const yFrac = sy - y0;
111
+ for (let dx = 0; dx < newW; dx++) {
112
+ const sx = dx * xRatio;
113
+ const x0 = Math.floor(sx);
114
+ const x1 = Math.min(x0 + 1, img.width - 1);
115
+ const xFrac = sx - x0;
116
+ const i00 = (y0 * img.width + x0) * 4;
117
+ const i10 = (y0 * img.width + x1) * 4;
118
+ const i01 = (y1 * img.width + x0) * 4;
119
+ const i11 = (y1 * img.width + x1) * 4;
120
+ const di = (dy * newW + dx) * 4;
121
+ const w00 = (1 - xFrac) * (1 - yFrac);
122
+ const w10 = xFrac * (1 - yFrac);
123
+ const w01 = (1 - xFrac) * yFrac;
124
+ const w11 = xFrac * yFrac;
125
+ data[di] = Math.round(img.data[i00] * w00 + img.data[i10] * w10 + img.data[i01] * w01 + img.data[i11] * w11);
126
+ data[di + 1] = Math.round(img.data[i00 + 1] * w00 + img.data[i10 + 1] * w10 + img.data[i01 + 1] * w01 + img.data[i11 + 1] * w11);
127
+ data[di + 2] = Math.round(img.data[i00 + 2] * w00 + img.data[i10 + 2] * w10 + img.data[i01 + 2] * w01 + img.data[i11 + 2] * w11);
128
+ data[di + 3] = Math.round(img.data[i00 + 3] * w00 + img.data[i10 + 3] * w10 + img.data[i01 + 3] * w01 + img.data[i11 + 3] * w11);
129
+ }
130
+ }
131
+ return { data, width: newW, height: newH };
132
+ }
133
+ // 90° clockwise: new width = srcHeight, new height = srcWidth
134
+ // dst(dx, dy) ← src(col=dy, row=srcH-1-dx)
135
+ export function rotate90CW(img) {
136
+ const { width: srcW, height: srcH } = img;
137
+ const data = new Uint8Array(srcW * srcH * 4);
138
+ const newW = srcH;
139
+ const newH = srcW;
140
+ for (let dy = 0; dy < newH; dy++) {
141
+ for (let dx = 0; dx < newW; dx++) {
142
+ const si = ((srcH - 1 - dx) * srcW + dy) * 4;
143
+ const di = (dy * newW + dx) * 4;
144
+ data[di] = img.data[si];
145
+ data[di + 1] = img.data[si + 1];
146
+ data[di + 2] = img.data[si + 2];
147
+ data[di + 3] = img.data[si + 3];
148
+ }
149
+ }
150
+ return { data, width: newW, height: newH };
151
+ }
152
+ // 90° counter-clockwise: new width = srcHeight, new height = srcWidth
153
+ // dst(dx, dy) ← src(col=srcW-1-dy, row=dx)
154
+ export function rotate90CCW(img) {
155
+ const { width: srcW, height: srcH } = img;
156
+ const data = new Uint8Array(srcW * srcH * 4);
157
+ const newW = srcH;
158
+ const newH = srcW;
159
+ for (let dy = 0; dy < newH; dy++) {
160
+ for (let dx = 0; dx < newW; dx++) {
161
+ const si = (dx * srcW + (srcW - 1 - dy)) * 4;
162
+ const di = (dy * newW + dx) * 4;
163
+ data[di] = img.data[si];
164
+ data[di + 1] = img.data[si + 1];
165
+ data[di + 2] = img.data[si + 2];
166
+ data[di + 3] = img.data[si + 3];
167
+ }
168
+ }
169
+ return { data, width: newW, height: newH };
170
+ }
171
+ export function rotate180(img) {
172
+ const data = new Uint8Array(img.data.length);
173
+ const total = img.width * img.height;
174
+ for (let i = 0; i < total; i++) {
175
+ const si = i * 4;
176
+ const di = (total - 1 - i) * 4;
177
+ data[di] = img.data[si];
178
+ data[di + 1] = img.data[si + 1];
179
+ data[di + 2] = img.data[si + 2];
180
+ data[di + 3] = img.data[si + 3];
181
+ }
182
+ return { data, width: img.width, height: img.height };
183
+ }
184
+ // Multiply every pixel's alpha channel by opacity (0–1). Mutates in place.
185
+ export function setOpacity(img, opacity) {
186
+ for (let i = 3; i < img.data.length; i += 4) {
187
+ img.data[i] = Math.round(img.data[i] * opacity);
188
+ }
189
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=imageUtils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageUtils.test.d.ts","sourceRoot":"","sources":["../../src/utils/imageUtils.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,277 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { encode } from 'fast-png';
3
+ import { decodeImage, encodeImage, toBase64Png, createCanvas, cropImage, compositeImage, resizeBilinear, rotate90CW, rotate90CCW, rotate180, setOpacity, } from './imageUtils.js';
4
+ // Build a minimal 2x2 RGBA PNG buffer for use as test input
5
+ function makePng(pixels) {
6
+ // pixels: [[r,g,b,a], ...] in row-major order, width = sqrt(pixels.length)
7
+ const side = Math.sqrt(pixels.length);
8
+ const data = new Uint8Array(pixels.length * 4);
9
+ pixels.forEach(([r, g, b, a], i) => {
10
+ data[i * 4] = r;
11
+ data[i * 4 + 1] = g;
12
+ data[i * 4 + 2] = b;
13
+ data[i * 4 + 3] = a;
14
+ });
15
+ return Buffer.from(encode({ data, width: side, height: side, channels: 4, depth: 8 }));
16
+ }
17
+ describe('decodeImage', () => {
18
+ it('decodes an RGBA PNG buffer into a RawImage', () => {
19
+ const buf = makePng([[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [255, 255, 0, 255]]);
20
+ const img = decodeImage(buf);
21
+ expect(img.width).toBe(2);
22
+ expect(img.height).toBe(2);
23
+ expect(img.data[0]).toBe(255); // R
24
+ expect(img.data[1]).toBe(0); // G
25
+ expect(img.data[2]).toBe(0); // B
26
+ expect(img.data[3]).toBe(255); // A
27
+ });
28
+ it('converts an RGB (3-channel) PNG to RGBA by adding full alpha', () => {
29
+ const data = new Uint8Array([100, 150, 200, 50, 60, 70]);
30
+ const buf = Buffer.from(encode({ data, width: 2, height: 1, channels: 3, depth: 8 }));
31
+ const img = decodeImage(buf);
32
+ expect(img.width).toBe(2);
33
+ expect(img.height).toBe(1);
34
+ expect(img.data[0]).toBe(100); // R
35
+ expect(img.data[1]).toBe(150); // G
36
+ expect(img.data[2]).toBe(200); // B
37
+ expect(img.data[3]).toBe(255); // A added
38
+ expect(img.data[4]).toBe(50);
39
+ expect(img.data[7]).toBe(255); // A added for second pixel
40
+ });
41
+ it('converts a grayscale (1-channel) PNG to RGBA', () => {
42
+ const data = new Uint8Array([128]);
43
+ const buf = Buffer.from(encode({ data, width: 1, height: 1, channels: 1, depth: 8 }));
44
+ const img = decodeImage(buf);
45
+ expect(img.width).toBe(1);
46
+ expect(img.data[0]).toBe(128); // R = gray value
47
+ expect(img.data[1]).toBe(128); // G = gray value
48
+ expect(img.data[2]).toBe(128); // B = gray value
49
+ expect(img.data[3]).toBe(255); // A = full
50
+ });
51
+ it('converts a grayscale+alpha (2-channel) PNG to RGBA', () => {
52
+ const data = new Uint8Array([200, 128]); // gray=200, alpha=128
53
+ const buf = Buffer.from(encode({ data, width: 1, height: 1, channels: 2, depth: 8 }));
54
+ const img = decodeImage(buf);
55
+ expect(img.data[0]).toBe(200); // R = gray
56
+ expect(img.data[1]).toBe(200); // G = gray
57
+ expect(img.data[2]).toBe(200); // B = gray
58
+ expect(img.data[3]).toBe(128); // A preserved
59
+ });
60
+ it('converts a 16-bit RGBA PNG by downsampling to 8-bit', () => {
61
+ // 16-bit value 0xFF00 >> 8 = 255, 0x8000 >> 8 = 128
62
+ const data = new Uint16Array([0xFF00, 0x8000, 0x0000, 0xFF00]);
63
+ const buf = Buffer.from(encode({ data, width: 1, height: 1, channels: 4, depth: 16 }));
64
+ const img = decodeImage(buf);
65
+ expect(img.data[0]).toBe(255); // R: 0xFF00 >> 8
66
+ expect(img.data[1]).toBe(128); // G: 0x8000 >> 8
67
+ expect(img.data[2]).toBe(0); // B: 0x0000 >> 8
68
+ expect(img.data[3]).toBe(255); // A: 0xFF00 >> 8
69
+ });
70
+ });
71
+ describe('encodeImage / toBase64Png', () => {
72
+ it('round-trips a RawImage through encode → decode', () => {
73
+ const original = createCanvas(2, 2, 100, 150, 200, 255);
74
+ const buf = encodeImage(original);
75
+ const decoded = decodeImage(buf);
76
+ expect(decoded.width).toBe(2);
77
+ expect(decoded.height).toBe(2);
78
+ expect(decoded.data[0]).toBe(100);
79
+ expect(decoded.data[1]).toBe(150);
80
+ expect(decoded.data[2]).toBe(200);
81
+ expect(decoded.data[3]).toBe(255);
82
+ });
83
+ it('toBase64Png returns a valid base64 string that decodes back correctly', () => {
84
+ const img = createCanvas(1, 1, 10, 20, 30, 255);
85
+ const b64 = toBase64Png(img);
86
+ expect(typeof b64).toBe('string');
87
+ const decoded = decodeImage(Buffer.from(b64, 'base64'));
88
+ expect(decoded.data[0]).toBe(10);
89
+ expect(decoded.data[1]).toBe(20);
90
+ expect(decoded.data[2]).toBe(30);
91
+ });
92
+ });
93
+ describe('createCanvas', () => {
94
+ it('creates a zero-filled canvas by default', () => {
95
+ const img = createCanvas(3, 3);
96
+ expect(img.width).toBe(3);
97
+ expect(img.height).toBe(3);
98
+ expect(img.data.every(v => v === 0)).toBe(true);
99
+ });
100
+ it('fills all pixels with the provided RGBA color', () => {
101
+ const img = createCanvas(2, 2, 57, 170, 86, 255);
102
+ for (let i = 0; i < 4; i++) {
103
+ expect(img.data[i * 4]).toBe(57);
104
+ expect(img.data[i * 4 + 1]).toBe(170);
105
+ expect(img.data[i * 4 + 2]).toBe(86);
106
+ expect(img.data[i * 4 + 3]).toBe(255);
107
+ }
108
+ });
109
+ });
110
+ describe('cropImage', () => {
111
+ it('extracts the correct rectangular region', () => {
112
+ // 4x1 image: red, green, blue, yellow
113
+ const img = {
114
+ data: new Uint8Array([
115
+ 255, 0, 0, 255, // red
116
+ 0, 255, 0, 255, // green
117
+ 0, 0, 255, 255, // blue
118
+ 255, 255, 0, 255, // yellow
119
+ ]),
120
+ width: 4,
121
+ height: 1,
122
+ };
123
+ const cropped = cropImage(img, 1, 0, 2, 1);
124
+ expect(cropped.width).toBe(2);
125
+ expect(cropped.height).toBe(1);
126
+ // First pixel = green
127
+ expect(cropped.data[0]).toBe(0);
128
+ expect(cropped.data[1]).toBe(255);
129
+ expect(cropped.data[2]).toBe(0);
130
+ // Second pixel = blue
131
+ expect(cropped.data[4]).toBe(0);
132
+ expect(cropped.data[5]).toBe(0);
133
+ expect(cropped.data[6]).toBe(255);
134
+ });
135
+ });
136
+ describe('compositeImage', () => {
137
+ it('copies an opaque overlay exactly onto the base', () => {
138
+ const base = createCanvas(2, 2, 0, 0, 0, 255);
139
+ const overlay = createCanvas(1, 1, 255, 0, 0, 255);
140
+ compositeImage(base, overlay, 1, 1);
141
+ const di = (1 * 2 + 1) * 4;
142
+ expect(base.data[di]).toBe(255);
143
+ expect(base.data[di + 1]).toBe(0);
144
+ expect(base.data[di + 2]).toBe(0);
145
+ expect(base.data[di + 3]).toBe(255);
146
+ // Top-left should be untouched
147
+ expect(base.data[0]).toBe(0);
148
+ });
149
+ it('blends a semi-transparent overlay with opacity', () => {
150
+ const base = createCanvas(1, 1, 0, 0, 0, 255); // black opaque
151
+ const overlay = createCanvas(1, 1, 255, 0, 0, 255); // red opaque
152
+ compositeImage(base, overlay, 0, 0, 0.5);
153
+ // src_a = 0.5, dst_a = 1, out_a = 1
154
+ // R = round((255 * 0.5 + 0 * 1 * 0.5) / 1) = 128 (approximately)
155
+ expect(base.data[0]).toBeCloseTo(128, 0);
156
+ expect(base.data[3]).toBe(255);
157
+ });
158
+ it('skips blending when both src and dst alpha are zero', () => {
159
+ const base = createCanvas(1, 1, 255, 0, 0, 0); // red but fully transparent
160
+ const overlay = createCanvas(1, 1, 0, 255, 0, 0); // green but fully transparent
161
+ compositeImage(base, overlay, 0, 0);
162
+ // outA = 0 → pixel unchanged: base stays (255, 0, 0, 0)
163
+ expect(base.data[0]).toBe(255);
164
+ expect(base.data[3]).toBe(0);
165
+ });
166
+ it('skips pixels outside the base bounds', () => {
167
+ const base = createCanvas(2, 2, 0, 0, 0, 255);
168
+ const overlay = createCanvas(3, 3, 255, 0, 0, 255);
169
+ // Offset so overlay extends beyond base - should not throw
170
+ expect(() => compositeImage(base, overlay, 1, 1)).not.toThrow();
171
+ });
172
+ });
173
+ describe('resizeBilinear', () => {
174
+ it('doubles a 1x1 image to 2x2 with the same color', () => {
175
+ const img = createCanvas(1, 1, 100, 200, 50, 255);
176
+ const resized = resizeBilinear(img, 2, 2);
177
+ expect(resized.width).toBe(2);
178
+ expect(resized.height).toBe(2);
179
+ expect(resized.data[0]).toBe(100);
180
+ expect(resized.data[1]).toBe(200);
181
+ expect(resized.data[2]).toBe(50);
182
+ });
183
+ it('halves a 4x4 image to 2x2', () => {
184
+ const img = createCanvas(4, 4, 200, 100, 50, 255);
185
+ const resized = resizeBilinear(img, 2, 2);
186
+ expect(resized.width).toBe(2);
187
+ expect(resized.height).toBe(2);
188
+ });
189
+ });
190
+ describe('rotate90CW', () => {
191
+ it('swaps width and height', () => {
192
+ const img = createCanvas(4, 2, 0, 0, 0, 255);
193
+ const rotated = rotate90CW(img);
194
+ expect(rotated.width).toBe(2);
195
+ expect(rotated.height).toBe(4);
196
+ });
197
+ it('moves top-left pixel to top-right', () => {
198
+ // 2x1: [red | green]
199
+ const img = {
200
+ data: new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]),
201
+ width: 2,
202
+ height: 1,
203
+ };
204
+ const rotated = rotate90CW(img);
205
+ // 90° CW: new 1x2 - top pixel comes from bottom-left of src (only 1 row, so left=red)
206
+ expect(rotated.width).toBe(1);
207
+ expect(rotated.height).toBe(2);
208
+ // top of rotated should be red (was left in src)
209
+ expect(rotated.data[0]).toBe(255); // R
210
+ expect(rotated.data[1]).toBe(0); // G
211
+ });
212
+ });
213
+ describe('rotate90CCW', () => {
214
+ it('swaps width and height', () => {
215
+ const img = createCanvas(4, 2, 0, 0, 0, 255);
216
+ const rotated = rotate90CCW(img);
217
+ expect(rotated.width).toBe(2);
218
+ expect(rotated.height).toBe(4);
219
+ });
220
+ it('is the inverse of rotate90CW', () => {
221
+ const img = createCanvas(3, 2, 0, 0, 0, 255);
222
+ img.data[0] = 42; // mark top-left
223
+ const cw = rotate90CW(img);
224
+ const back = rotate90CCW(cw);
225
+ expect(back.width).toBe(3);
226
+ expect(back.height).toBe(2);
227
+ expect(back.data[0]).toBe(42);
228
+ });
229
+ });
230
+ describe('rotate180', () => {
231
+ it('preserves dimensions', () => {
232
+ const img = createCanvas(3, 4, 0, 0, 0, 255);
233
+ const rotated = rotate180(img);
234
+ expect(rotated.width).toBe(3);
235
+ expect(rotated.height).toBe(4);
236
+ });
237
+ it('flips the pixel order', () => {
238
+ const img = {
239
+ data: new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]),
240
+ width: 2,
241
+ height: 1,
242
+ };
243
+ const rotated = rotate180(img);
244
+ // First pixel should now be green (was last)
245
+ expect(rotated.data[0]).toBe(0);
246
+ expect(rotated.data[1]).toBe(255);
247
+ expect(rotated.data[2]).toBe(0);
248
+ // Second pixel should be red
249
+ expect(rotated.data[4]).toBe(255);
250
+ expect(rotated.data[5]).toBe(0);
251
+ });
252
+ it('is its own inverse', () => {
253
+ const img = createCanvas(2, 2, 0, 0, 0, 255);
254
+ img.data[0] = 77;
255
+ const twice = rotate180(rotate180(img));
256
+ expect(twice.data[0]).toBe(77);
257
+ });
258
+ });
259
+ describe('setOpacity', () => {
260
+ it('halves the alpha channel of each pixel', () => {
261
+ const img = createCanvas(2, 2, 57, 170, 86, 255);
262
+ setOpacity(img, 0.5);
263
+ for (let i = 0; i < 4; i++) {
264
+ expect(img.data[i * 4 + 3]).toBe(128);
265
+ }
266
+ });
267
+ it('sets full opacity to 255', () => {
268
+ const img = createCanvas(1, 1, 0, 0, 0, 128);
269
+ setOpacity(img, 1);
270
+ expect(img.data[3]).toBe(128); // unchanged - 128 * 1 = 128
271
+ });
272
+ it('sets zero opacity to 0', () => {
273
+ const img = createCanvas(1, 1, 0, 0, 0, 200);
274
+ setOpacity(img, 0);
275
+ expect(img.data[3]).toBe(0);
276
+ });
277
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdio/image-comparison-core",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "author": "Wim Selles - wswebcreation",
5
5
  "description": "Image comparison core module for @wdio/visual-service - WebdriverIO visual testing framework",
6
6
  "keywords": [
@@ -26,7 +26,8 @@
26
26
  "url": "https://github.com/webdriverio/visual-testing/issues"
27
27
  },
28
28
  "dependencies": {
29
- "jimp": "^1.6.1",
29
+ "fast-png": "^8.0.0",
30
+ "pixelmatch": "^7.2.0",
30
31
  "@wdio/logger": "^9.29.1",
31
32
  "@wdio/types": "^9.29.1"
32
33
  },
@@ -1,136 +0,0 @@
1
- import type { BaseBoundingBox, BaseCoordinates, BaseDimensions } from '../base.interfaces.js';
2
- export interface CompareData {
3
- /** The mismatch percentage like 0.12345567 */
4
- rawMisMatchPercentage: number;
5
- /** The mismatch percentage like 0.12 */
6
- misMatchPercentage: number;
7
- /** The image buffer */
8
- getBuffer: () => Buffer;
9
- /** The bounds of the diff area */
10
- diffBounds: BaseBoundingBox;
11
- /** The analysis time in milliseconds */
12
- analysisTime: number;
13
- /** The diff pixels location(s) and color(s) */
14
- diffPixels: BaseCoordinates[];
15
- }
16
- /**
17
- * Src: @types/resemblejs
18
- */
19
- type OutputSettings = {
20
- /** Color to use for highlighting errors */
21
- errorColor?: {
22
- /** Red color component (0-255) */
23
- red: number;
24
- /** Green color component (0-255) */
25
- green: number;
26
- /** Blue color component (0-255) */
27
- blue: number;
28
- } | undefined;
29
- /** Type of error highlighting to use */
30
- errorType?: OutputErrorType | undefined;
31
- /** Custom error pixel processing function */
32
- errorPixel?: ((px: number[], offset: number, d1: Color, d2: Color) => void) | undefined;
33
- /** Transparency level for the output image */
34
- transparency?: number | undefined;
35
- /** Threshold for large image processing */
36
- largeImageThreshold?: number | undefined;
37
- /** Whether to use cross-origin for image loading */
38
- useCrossOrigin?: boolean | undefined;
39
- /** Bounding box to focus comparison on */
40
- boundingBox?: Box | undefined;
41
- /** Box area to ignore during comparison */
42
- ignoredBox?: Box | undefined;
43
- /** Multiple bounding boxes to focus comparison on */
44
- boundingBoxes?: Box[] | undefined;
45
- /** Multiple box areas to ignore during comparison */
46
- ignoredBoxes?: Box[] | undefined;
47
- /** Color to ignore during comparison */
48
- ignoreAreasColoredWith?: Color | undefined;
49
- };
50
- type Box = {
51
- /** Left boundary of the box */
52
- left: number;
53
- /** Top boundary of the box */
54
- top: number;
55
- /** Right boundary of the box */
56
- right: number;
57
- /** Bottom boundary of the box */
58
- bottom: number;
59
- };
60
- type Color = {
61
- /** Red color component (0-255) */
62
- r: number;
63
- /** Green color component (0-255) */
64
- g: number;
65
- /** Blue color component (0-255) */
66
- b: number;
67
- /** Alpha transparency component (0-255) */
68
- a: number;
69
- };
70
- type Tolerance = {
71
- /** Tolerance for red color component */
72
- red?: number;
73
- /** Tolerance for green color component */
74
- green?: number;
75
- /** Tolerance for blue color component */
76
- blue?: number;
77
- /** Tolerance for alpha transparency component */
78
- alpha?: number;
79
- /** Minimum brightness tolerance */
80
- minBrightness?: number;
81
- /** Maximum brightness tolerance */
82
- maxBrightness?: number;
83
- };
84
- type OutputErrorType = 'flat' | 'movement' | 'flatDifferenceIntensity' | 'movementDifferenceIntensity' | 'diffOnly';
85
- export type ComparisonIgnoreOption = 'nothing' | 'less' | 'antialiasing' | 'colors' | 'alpha';
86
- export interface ComparisonOptions {
87
- /** Output settings for the comparison */
88
- output?: OutputSettings | undefined;
89
- /** Threshold to return early if mismatch exceeds this value */
90
- returnEarlyThreshold?: number | undefined;
91
- /** Whether to scale images to the same size before comparison */
92
- scaleToSameSize?: boolean | undefined;
93
- /** What aspects to ignore during comparison */
94
- ignore?: ComparisonIgnoreOption | ComparisonIgnoreOption[] | undefined;
95
- /** Tolerance settings for color differences */
96
- tolerance?: Tolerance | undefined;
97
- }
98
- export interface ComparisonResult {
99
- /**
100
- * Error information if error encountered
101
- *
102
- * Note: If error encountered, other properties will be undefined
103
- */
104
- error?: unknown | undefined;
105
- /**
106
- * Time consumed by the comparison (in milliseconds)
107
- */
108
- analysisTime: number;
109
- /**
110
- * Do the two images have the same dimensions?
111
- */
112
- isSameDimensions: boolean;
113
- /**
114
- * The difference in width and height between the dimensions of the two compared images
115
- */
116
- dimensionDifference: BaseDimensions;
117
- /**
118
- * The percentage of pixels which do not match between the images
119
- */
120
- rawMisMatchPercentage: number;
121
- /**
122
- * Same as `rawMisMatchPercentage` but fixed to 2-digit after the decimal point
123
- */
124
- misMatchPercentage: number;
125
- diffBounds: Box;
126
- /**
127
- * Get a data URL for the comparison image
128
- */
129
- getImageDataUrl(): string;
130
- /**
131
- * Get data buffer
132
- */
133
- getBuffer?: (includeOriginal: boolean) => Buffer;
134
- }
135
- export {};
136
- //# sourceMappingURL=compare.interfaces.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compare.interfaces.d.ts","sourceRoot":"","sources":["../../src/resemble/compare.interfaces.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"compareImages.d.ts","sourceRoot":"","sources":["../../src/resemble/compareImages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAE7E,wBAA8B,aAAa,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,iBAAiB,GAC3B,OAAO,CAAC,WAAW,CAAC,CAmBtB"}
@@ -1,21 +0,0 @@
1
- // @ts-ignore: Ignoring type checking for this module import
2
- import * as resembleJimp from './resemble.jimp.cjs';
3
- export default async function compareImages(image1, image2, options) {
4
- /**
5
- * Resemble.js implemented in the way that scales 2nd images to the size of 1st.
6
- * Experimentally proven that downscaling images produces more accurate result than upscaling
7
- */
8
- const { imageToCompare1, imageToCompare2 } = options.scaleToSameSize && image1.length > image2.length
9
- ? {
10
- imageToCompare1: image2,
11
- imageToCompare2: image1,
12
- }
13
- : { imageToCompare1: image1, imageToCompare2: image2 };
14
- try {
15
- const data = await resembleJimp.default.compare(imageToCompare1, imageToCompare2, options);
16
- return data;
17
- }
18
- catch (err) {
19
- throw err;
20
- }
21
- }