@wdio/image-comparison-core 1.3.0 → 2.0.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +65 -3
  2. package/dist/base.interfaces.d.ts +2 -2
  3. package/dist/helpers/constants.js +1 -1
  4. package/dist/helpers/options.d.ts +1 -4
  5. package/dist/helpers/options.d.ts.map +1 -1
  6. package/dist/helpers/options.interfaces.d.ts +4 -2
  7. package/dist/helpers/options.interfaces.d.ts.map +1 -1
  8. package/dist/helpers/options.js +2 -11
  9. package/dist/helpers/options.test.js +16 -1
  10. package/dist/methods/compareReport.interfaces.d.ts +1 -1
  11. package/dist/methods/compareReport.interfaces.d.ts.map +1 -1
  12. package/dist/methods/createCompareReport.test.js +4 -2
  13. package/dist/methods/images.d.ts +2 -1
  14. package/dist/methods/images.d.ts.map +1 -1
  15. package/dist/methods/images.executeImageCompare.test.js +60 -44
  16. package/dist/methods/images.js +31 -31
  17. package/dist/methods/images.test.js +80 -220
  18. package/dist/methods/processDiffPixels.d.ts +1 -1
  19. package/dist/methods/processDiffPixels.d.ts.map +1 -1
  20. package/dist/methods/processDiffPixels.js +11 -2
  21. package/dist/methods/rectangles.d.ts.map +1 -1
  22. package/dist/methods/rectangles.interfaces.d.ts +1 -1
  23. package/dist/methods/rectangles.interfaces.d.ts.map +1 -1
  24. package/dist/methods/rectangles.js +12 -9
  25. package/dist/pixelmatch/compare.interfaces.d.ts +43 -0
  26. package/dist/pixelmatch/compare.interfaces.d.ts.map +1 -0
  27. package/dist/pixelmatch/compareImages.d.ts.map +1 -0
  28. package/dist/pixelmatch/compareImages.js +178 -0
  29. package/dist/pixelmatch/compareImages.test.d.ts +2 -0
  30. package/dist/pixelmatch/compareImages.test.d.ts.map +1 -0
  31. package/dist/pixelmatch/compareImages.test.js +267 -0
  32. package/dist/utils/imageUtils.d.ts +17 -0
  33. package/dist/utils/imageUtils.d.ts.map +1 -0
  34. package/dist/utils/imageUtils.js +189 -0
  35. package/dist/utils/imageUtils.test.d.ts +2 -0
  36. package/dist/utils/imageUtils.test.d.ts.map +1 -0
  37. package/dist/utils/imageUtils.test.js +277 -0
  38. package/package.json +3 -2
  39. package/dist/resemble/compare.interfaces.d.ts +0 -136
  40. package/dist/resemble/compare.interfaces.d.ts.map +0 -1
  41. package/dist/resemble/compareImages.d.ts.map +0 -1
  42. package/dist/resemble/compareImages.js +0 -21
  43. package/dist/resemble/resemble.jimp.cjs +0 -981
  44. package/dist/resemble/resemble.jimp.d.cts +0 -46
  45. package/dist/resemble/resemble.jimp.d.cts.map +0 -1
  46. /package/dist/{resemble → pixelmatch}/compare.interfaces.js +0 -0
  47. /package/dist/{resemble → pixelmatch}/compareImages.d.ts +0 -0
@@ -1,9 +1,9 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import { readFileSync, writeFileSync, promises as fsPromises, constants } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
- import { Jimp, JimpMime } from 'jimp';
4
+ import { decodeImage, toBase64Png, createCanvas, cropImage, compositeImage, setOpacity, rotate90CW, rotate90CCW, rotate180 } from '../utils/imageUtils.js';
5
5
  import logger from '@wdio/logger';
6
- import compareImages from '../resemble/compareImages.js';
6
+ import compareImagesPixelmatch from '../pixelmatch/compareImages.js';
7
7
  import { calculateDprData, getIosBezelImageNames, getBase64ScreenshotSize, prepareComparisonFilePaths, updateVisualBaseline } from '../helpers/utils.js';
8
8
  import { prepareIgnoreOptions } from '../helpers/options.js';
9
9
  import { DEFAULT_RESIZE_DIMENSIONS, supportedIosBezelDevices } from '../helpers/constants.js';
@@ -92,7 +92,7 @@ ${filePathMessage}
92
92
  export async function getRotatedImageIfNeeded({ isWebDriverElementScreenshot, isLandscape, base64Image }) {
93
93
  const { height: screenshotHeight, width: screenshotWidth } = getBase64ScreenshotSize(base64Image);
94
94
  const isRotated = !isWebDriverElementScreenshot && isLandscape && screenshotHeight > screenshotWidth;
95
- return isRotated ? await rotateBase64Image({ base64Image, degrees: 90 }) : base64Image;
95
+ return isRotated ? rotateBase64Image({ base64Image, degrees: 90 }) : base64Image;
96
96
  }
97
97
  /**
98
98
  * Log a warning when the crop goes out of the screen
@@ -150,10 +150,10 @@ export async function handleIOSBezelCorners({ addIOSBezelCorners, image, deviceN
150
150
  const __dirname = dirname(__filename);
151
151
  const topImage = readFileSync(join(__dirname, '..', '..', 'assets', 'ios', `${topImageName}.png`), { encoding: 'base64' });
152
152
  const bottomImage = readFileSync(join(__dirname, '..', '..', 'assets', 'ios', `${bottomImageName}.png`), { encoding: 'base64' });
153
- const topBase64Image = isLandscape ? await rotateBase64Image({ base64Image: topImage, degrees: 90 }) : topImage;
154
- const bottomBase64Image = isLandscape ? await rotateBase64Image({ base64Image: bottomImage, degrees: 90 }) : bottomImage;
155
- image.composite(await Jimp.read(Buffer.from(topBase64Image, 'base64')), 0, 0);
156
- image.composite(await Jimp.read(Buffer.from(bottomBase64Image, 'base64')), isLandscape ? width - getBase64ScreenshotSize(bottomImage).height : 0, isLandscape ? 0 : height - getBase64ScreenshotSize(bottomImage).height);
153
+ const topBase64Image = isLandscape ? rotateBase64Image({ base64Image: topImage, degrees: 270 }) : topImage;
154
+ const bottomBase64Image = isLandscape ? rotateBase64Image({ base64Image: bottomImage, degrees: 270 }) : bottomImage;
155
+ compositeImage(image, decodeImage(Buffer.from(topBase64Image, 'base64')), 0, 0);
156
+ compositeImage(image, decodeImage(Buffer.from(bottomBase64Image, 'base64')), isLandscape ? width - getBase64ScreenshotSize(bottomImage).height : 0, isLandscape ? 0 : height - getBase64ScreenshotSize(bottomImage).height);
157
157
  }
158
158
  else {
159
159
  isIosBezelError = true;
@@ -178,13 +178,12 @@ ${supportedIosBezelDevices.join(', ')}
178
178
  * Crop the image and convert it to a base64 image
179
179
  */
180
180
  export async function cropAndConvertToDataURL({ addIOSBezelCorners, base64Image, deviceName, devicePixelRatio, height, isIOS, isLandscape, sourceX, sourceY, width, }) {
181
- const image = await Jimp.read(Buffer.from(base64Image, 'base64'));
182
- const croppedImage = image.crop({ x: sourceX, y: sourceY, w: width, h: height });
181
+ const image = decodeImage(Buffer.from(base64Image, 'base64'));
182
+ const croppedImage = cropImage(image, sourceX, sourceY, width, height);
183
183
  if (isIOS) {
184
184
  await handleIOSBezelCorners({ addIOSBezelCorners, image: croppedImage, deviceName, devicePixelRatio, height, isLandscape, width });
185
185
  }
186
- const base64CroppedImage = await croppedImage.getBase64(JimpMime.png);
187
- return base64CroppedImage.replace(/^data:image\/png;base64,/, '');
186
+ return toBase64Png(croppedImage);
188
187
  }
189
188
  /**
190
189
  * Make a cropped image with Canvas
@@ -309,7 +308,7 @@ export async function executeImageCompare({ isViewPortScreenshot, isNativeContex
309
308
  scaleToSameSize: imageCompareOptions.scaleImagesToSameSize,
310
309
  };
311
310
  // 5. Execute the compare and retrieve the data
312
- const data = await compareImages(readFileSync(baselineFilePath), actualImageBuffer, compareOptions);
311
+ const data = await compareImagesPixelmatch(readFileSync(baselineFilePath), actualImageBuffer, compareOptions);
313
312
  const rawMisMatchPercentage = data.rawMisMatchPercentage;
314
313
  const reportMisMatchPercentage = imageCompareOptions.rawMisMatchPercentage
315
314
  ? rawMisMatchPercentage
@@ -372,19 +371,19 @@ export async function executeImageCompare({ isViewPortScreenshot, isNativeContex
372
371
  export async function makeFullPageBase64Image(screenshotsData, { devicePixelRatio, isLandscape }) {
373
372
  const amountOfScreenshots = screenshotsData.data.length;
374
373
  const { fullPageHeight: canvasHeight, fullPageWidth: canvasWidth } = screenshotsData;
375
- const canvas = await new Jimp({ width: canvasWidth, height: canvasHeight });
374
+ const canvas = createCanvas(canvasWidth, canvasHeight);
376
375
  // Load all the images
377
376
  for (let i = 0; i < amountOfScreenshots; i++) {
378
377
  const currentScreenshot = screenshotsData.data[i].screenshot;
379
378
  const { height: screenshotHeight, width: screenshotWidth } = getBase64ScreenshotSize(currentScreenshot, devicePixelRatio);
380
379
  const isRotated = isLandscape && screenshotHeight > screenshotWidth;
381
- const newBase64Image = isRotated ? await rotateBase64Image({ base64Image: currentScreenshot, degrees: 90 }) : currentScreenshot;
380
+ const newBase64Image = isRotated ? rotateBase64Image({ base64Image: currentScreenshot, degrees: 90 }) : currentScreenshot;
382
381
  const { canvasYPosition, imageHeight, imageXPosition, imageYPosition } = screenshotsData.data[i];
383
- const image = await Jimp.read(Buffer.from(newBase64Image, 'base64'));
382
+ const image = decodeImage(Buffer.from(newBase64Image, 'base64'));
384
383
  // Clamp crop dimensions to fit within the actual image bounds
385
384
  // This is especially important for the last image where the calculated height might exceed available pixels
386
- const actualImageWidth = image.bitmap.width;
387
- const actualImageHeight = image.bitmap.height;
385
+ const actualImageWidth = image.width;
386
+ const actualImageHeight = image.height;
388
387
  const clampedCropX = Math.max(0, Math.min(imageXPosition, actualImageWidth - 1));
389
388
  const clampedCropY = Math.max(0, Math.min(imageYPosition, actualImageHeight - 1));
390
389
  // Ensure the cropped width matches the canvas width to avoid 1px gaps due to rounding
@@ -392,10 +391,9 @@ export async function makeFullPageBase64Image(screenshotsData, { devicePixelRati
392
391
  const maxAvailableWidth = actualImageWidth - clampedCropX;
393
392
  const clampedCropWidth = Math.min(canvasWidth, maxAvailableWidth);
394
393
  const clampedCropHeight = Math.min(imageHeight, actualImageHeight - clampedCropY);
395
- canvas.composite(image.crop({ x: clampedCropX, y: clampedCropY, w: clampedCropWidth, h: clampedCropHeight }), 0, canvasYPosition);
394
+ compositeImage(canvas, cropImage(image, clampedCropX, clampedCropY, clampedCropWidth, clampedCropHeight), 0, canvasYPosition);
396
395
  }
397
- const base64FullPageImage = await canvas.getBase64(JimpMime.png);
398
- return base64FullPageImage.replace(/^data:image\/png;base64,/, '');
396
+ return toBase64Png(canvas);
399
397
  }
400
398
  /**
401
399
  * Save the base64 image to a file
@@ -404,30 +402,32 @@ export async function saveBase64Image(base64Image, filePath) {
404
402
  await fsPromises.mkdir(dirname(filePath), { recursive: true });
405
403
  await fsPromises.writeFile(filePath, Buffer.from(base64Image, 'base64'));
406
404
  }
405
+ export async function savePngBuffer(buffer, filePath) {
406
+ await fsPromises.mkdir(dirname(filePath), { recursive: true });
407
+ await fsPromises.writeFile(filePath, buffer);
408
+ }
407
409
  /**
408
410
  * Create a canvas with the ignore boxes if they are present
409
411
  */
410
412
  export async function addBlockOuts(screenshot, ignoredBoxes) {
411
- const image = await Jimp.read(Buffer.from(screenshot, 'base64'));
413
+ const image = decodeImage(Buffer.from(screenshot, 'base64'));
412
414
  // Loop over all ignored areas and add them to the current canvas
413
415
  for (const ignoredBox of ignoredBoxes) {
414
416
  const { right: ignoredBoxWidth, bottom: ignoredBoxHeight, left: x, top: y } = ignoredBox;
415
- const ignoreCanvas = new Jimp({ width: ignoredBoxWidth - x, height: ignoredBoxHeight - y, color: '#39aa56' });
416
- ignoreCanvas.opacity(0.5);
417
- image.composite(ignoreCanvas, x, y);
417
+ const ignoreCanvas = createCanvas(ignoredBoxWidth - x, ignoredBoxHeight - y, 57, 170, 86, 255);
418
+ setOpacity(ignoreCanvas, 0.5);
419
+ compositeImage(image, ignoreCanvas, x, y);
418
420
  }
419
- const base64ImageWithBlockOuts = await image.getBase64(JimpMime.png);
420
- return base64ImageWithBlockOuts.replace(/^data:image\/png;base64,/, '');
421
+ return toBase64Png(image);
421
422
  }
422
423
  /**
423
424
  * Rotate a base64 image
424
425
  * Tnx to https://gist.github.com/Zyndoras/6897abdf53adbedf02564808aaab94db
425
426
  */
426
- export async function rotateBase64Image({ base64Image, degrees }) {
427
- const image = await Jimp.read(Buffer.from(base64Image, 'base64'));
428
- const rotatedImage = image.rotate(degrees);
429
- const base64RotatedImage = await rotatedImage.getBase64(JimpMime.png);
430
- return base64RotatedImage.replace(/^data:image\/png;base64,/, '');
427
+ export function rotateBase64Image({ base64Image, degrees }) {
428
+ const image = decodeImage(Buffer.from(base64Image, 'base64'));
429
+ const rotated = degrees === 180 ? rotate180(image) : degrees === 270 ? rotate90CCW(image) : rotate90CW(image);
430
+ return toBase64Png(rotated);
431
431
  }
432
432
  /**
433
433
  * Take a based64 screenshot of an element and resize it