Skip to content

Commit ccec132

Browse files
chore: sharp -> magick-wasm
1 parent ea3dc47 commit ccec132

9 files changed

Lines changed: 184 additions & 975 deletions

File tree

index.js

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const AntialiasingComparator = require('./lib/antialiasing-comparator');
88
const IgnoreCaretComparator = require('./lib/ignore-caret-comparator');
99
const DiffArea = require('./lib/diff-area');
1010
const utils = require('./lib/utils');
11-
const {JND} = require('./lib/constants');
11+
const {JND, RGB_IMAGE_CHANNELS} = require('./lib/constants');
1212

1313
const makeAntialiasingComparator = (comparator, img1, img2, opts) => {
1414
const antialiasingComparator = new AntialiasingComparator(comparator, img1, img2, opts);
@@ -24,13 +24,42 @@ function makeCIEDE2000Comparator(tolerance) {
2424
const upperBound = tolerance * 6.2; // cie76 <= 6.2 * ciede2000
2525
const lowerBound = tolerance * 0.695; // cie76 >= 0.695 * ciede2000
2626

27+
let rgbColor1 = {};
28+
let rgbColor2 = {};
29+
let labColor1 = {};
30+
let labColor2 = {};
31+
2732
return function doColorsLookSame(data) {
2833
if (areColorsSame(data)) {
2934
return true;
3035
}
36+
37+
let lab1, lab2;
38+
3139
/*jshint camelcase:false*/
32-
const lab1 = colorDiff.rgb_to_lab(data.color1);
33-
const lab2 = colorDiff.rgb_to_lab(data.color2);
40+
if (areColorsSame({color1: data.color1, color2: rgbColor1})) {
41+
lab1 = labColor1;
42+
} else if (areColorsSame({color1: data.color1, color2: rgbColor2})) {
43+
lab1 = labColor2;
44+
}
45+
46+
if (areColorsSame({color1: data.color2, color2: rgbColor1})) {
47+
lab2 = labColor1;
48+
} else if (areColorsSame({color1: data.color2, color2: rgbColor2})) {
49+
lab2 = labColor2;
50+
}
51+
52+
if (!lab1) {
53+
lab1 = colorDiff.rgb_to_lab(data.color1);
54+
rgbColor1 = data.color1;
55+
labColor1 = lab1;
56+
}
57+
58+
if (!lab2) {
59+
lab2 = colorDiff.rgb_to_lab(data.color2);
60+
rgbColor2 = data.color2;
61+
labColor2 = lab2;
62+
}
3463

3564
const cie76 = Math.sqrt(
3665
(lab1.L - lab2.L) * (lab1.L - lab2.L) +
@@ -93,7 +122,7 @@ const buildDiffImage = async (img1, img2, options) => {
93122
const minHeight = Math.min(img1.height, img2.height);
94123

95124
const highlightColor = options.highlightColor;
96-
const resultBuffer = Buffer.alloc(width * height * 3);
125+
const resultBuffer = Buffer.allocUnsafe(width * height * RGB_IMAGE_CHANNELS);
97126

98127
const setPixel = (buf, x, y, {R, G, B}) => {
99128
const pixelInd = (y * width + x) * 3;
@@ -118,7 +147,7 @@ const buildDiffImage = async (img1, img2, options) => {
118147
}
119148
});
120149

121-
return img.fromBuffer(resultBuffer, {raw: {width, height, channels: 3}});
150+
return img.fromBuffer(resultBuffer, {rgb: {width, height}});
122151
};
123152

124153
const getToleranceFromOpts = (opts) => {

lib/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ module.exports = {
55
REQUIRED_IMAGE_FIELDS: ['source', 'boundingBox'],
66
REQUIRED_BOUNDING_BOX_FIELDS: ['left', 'top', 'right', 'bottom'],
77
CLUSTERS_SIZE: 10,
8-
DIFF_IMAGE_CHANNELS: 3
8+
RGB_IMAGE_CHANNELS: 3,
9+
BITS_IN_BYTE: 8
910
};

lib/image/bounded-image.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
const Image = require('./image');
44

55
module.exports = class BoundedImage extends Image {
6-
constructor(img, boundingBox) {
7-
super(img);
6+
constructor(buffer, rgb, boundingBox) {
7+
super(buffer, rgb);
88

99
this._boundingBox = boundingBox;
1010
}

lib/image/image.js

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,91 @@
11
'use strict';
22

3+
const magick = require('@imagemagick/magick-wasm');
4+
const fs = require('fs');
35
const ImageBase = require('../image-base');
6+
const {RGB_IMAGE_CHANNELS, BITS_IN_BYTE} = require('../constants');
7+
8+
const supportedFormats = ['JPEG', 'PNG', 'WEBP', 'GIF', 'AVIF', 'TIFF', 'SVG'];
49

510
module.exports = class Image extends ImageBase {
6-
constructor(img) {
11+
constructor(buffer, rgb) {
712
super();
813

9-
this._img = img;
10-
}
14+
let settings;
15+
16+
if (rgb) {
17+
settings = new magick.MagickReadSettings({format: 'RGB', width: rgb.width, height: rgb.height});
18+
}
1119

12-
async init() {
13-
const {data, info} = await this._img.raw().toBuffer({resolveWithObject: true});
20+
this._img = magick.MagickImage.create(buffer, settings);
1421

15-
this._buffer = data;
16-
this._width = info.width;
17-
this._height = info.height;
18-
this._channels = info.channels;
22+
this._width = this._img.width;
23+
this._height = this._img.height;
1924
}
2025

21-
async initMeta() {
22-
const {width, height, channels} = await this._img.metadata();
26+
/**
27+
* @returns 8 bit depth UInt8Array of rgb colors
28+
*/
29+
_getRgbBuffer() {
30+
if (this._rgbBuffer) {
31+
return this._rgbBuffer;
32+
}
33+
34+
const imageDepth = this._img.depth;
35+
36+
this._img.depth = BITS_IN_BYTE;
37+
this._rgbBuffer = this._img.write('RGB', Buffer.from);
38+
this._img.depth = imageDepth;
2339

24-
this._width = width;
25-
this._height = height;
26-
this._channels = channels;
40+
return this._rgbBuffer;
2741
}
2842

2943
getPixel(x, y) {
30-
const idx = this._getIdx(x, y);
44+
const rgbBuffer = this._getRgbBuffer();
45+
const idx = (this._width * y + x) * RGB_IMAGE_CHANNELS;
46+
3147
return {
32-
R: this._buffer[idx],
33-
G: this._buffer[idx + 1],
34-
B: this._buffer[idx + 2]
48+
R: rgbBuffer[idx],
49+
G: rgbBuffer[idx + 1],
50+
B: rgbBuffer[idx + 2]
3551
};
3652
}
3753

38-
_getIdx(x, y) {
39-
return (this._width * y + x) * this._channels;
40-
}
41-
4254
async save(path) {
43-
return this._img.toFile(path);
55+
const dotIndex = path.lastIndexOf('.');
56+
57+
let imageExtension = 'PNG';
58+
59+
if (dotIndex !== -1) {
60+
const fileExtension = path.slice(dotIndex + 1).toUpperCase();
61+
62+
if (supportedFormats.includes(fileExtension)) {
63+
imageExtension = fileExtension;
64+
}
65+
}
66+
67+
return this._img.write(imageExtension, buffer => fs.promises.writeFile(path, buffer));
4468
}
4569

4670
async createBuffer(extension) {
47-
return this._img.toFormat(extension).toBuffer();
71+
if (extension === 'raw') {
72+
return this._getRgbBuffer();
73+
}
74+
75+
return this._img.write('PNG', Buffer.from);
4876
}
4977
};
78+
79+
let initMagickPromise;
80+
81+
module.exports.initMagick = () => {
82+
if (initMagickPromise) {
83+
return initMagickPromise;
84+
}
85+
86+
return initMagickPromise = new Promise(resolve => {
87+
const magickWasmBytesLocation = require.resolve('@imagemagick/magick-wasm/magick.wasm');
88+
89+
return fs.promises.readFile(magickWasmBytesLocation).then(magick.initializeImageMagick).then(resolve);
90+
});
91+
};

lib/image/index.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22

33
const fs = require('fs-extra');
44
const NestedError = require('nested-error-stacks');
5-
const sharp = require('sharp');
5+
const {initMagick} = require('./image');
66
const OriginalIMG = require('./original-image');
77
const BoundedIMG = require('./bounded-image');
88

9-
const createimage = async (img, {boundingBox} = {}) => {
9+
const initMagickPromise = initMagick();
10+
11+
const createimage = async (buffer, {boundingBox, rgb} = {}) => {
12+
await initMagickPromise;
13+
1014
return boundingBox
11-
? BoundedIMG.create(img, boundingBox)
12-
: OriginalIMG.create(img);
15+
? BoundedIMG.create(buffer, rgb, boundingBox)
16+
: OriginalIMG.create(buffer, rgb);
1317
};
1418

15-
exports.fromBuffer = async (buffer, opts) => {
16-
const img = sharp(buffer, opts);
17-
return createimage(img, opts);
18-
};
19+
exports.fromBuffer = (buffer, opts) => createimage(buffer, opts);
1920

2021
exports.fromFile = async (filePath, opts = {}) => {
2122
try {

lib/utils.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ const DiffArea = require('./diff-area');
88
const DiffClusters = require('./diff-clusters');
99
const validators = require('./validators');
1010
const areColorsSame = require('./same-colors');
11-
const {DIFF_IMAGE_CHANNELS} = require('./constants');
11+
const {RGB_IMAGE_CHANNELS} = require('./constants');
1212

1313
exports.readImgCb = async ({source, ...opts}) => {
1414
const readFunc = Buffer.isBuffer(source) ? img.fromBuffer : img.fromFile;
1515
const image = await readFunc(source, opts);
1616

17-
await image.init();
18-
1917
return image;
2018
};
2119

@@ -122,7 +120,7 @@ exports.calcDiffImage = async (img1, img2, comparator, {highlightColor, shouldCl
122120
const totalPixels = maxHeight * maxWidth;
123121
const metaInfo = {refImg: {size: {width: img1.width, height: img1.height}}};
124122

125-
const diffBuffer = Buffer.alloc(maxHeight * maxWidth * DIFF_IMAGE_CHANNELS);
123+
const diffBuffer = Buffer.allocUnsafe(maxHeight * maxWidth * RGB_IMAGE_CHANNELS);
126124
const diffArea = new DiffArea();
127125
const diffClusters = new DiffClusters(clustersSize);
128126

@@ -182,8 +180,7 @@ exports.calcDiffImage = async (img1, img2, comparator, {highlightColor, shouldCl
182180
let diffImage = null;
183181

184182
if (differentPixels) {
185-
diffImage = await img.fromBuffer(diffBuffer, {raw: {width: maxWidth, height: maxHeight, channels: DIFF_IMAGE_CHANNELS}});
186-
await diffImage.initMeta();
183+
diffImage = await img.fromBuffer(diffBuffer, {rgb: {width: maxWidth, height: maxHeight}});
187184
}
188185

189186
return {

0 commit comments

Comments
 (0)