looks-same
Advanced tools
Comparing version 8.1.0 to 8.2.0
@@ -5,2 +5,9 @@ # Changelog | ||
## [8.2.0](https://github.com/gemini-testing/looks-same/compare/v8.1.0...v8.2.0) (2023-08-02) | ||
### Features | ||
* calc equality and build diff simultaneously ([abbe9ed](https://github.com/gemini-testing/looks-same/commit/abbe9ed29f18d656317097053e74afe11bacda44)) | ||
## [8.1.0](https://github.com/gemini-testing/looks-same/compare/v8.0.0...v8.1.0) (2022-11-21) | ||
@@ -7,0 +14,0 @@ |
42
index.js
'use strict'; | ||
const _ = require('lodash'); | ||
const parseColor = require('parse-color'); | ||
const colorDiff = require('color-diff'); | ||
@@ -25,2 +24,5 @@ const img = require('./lib/image'); | ||
function makeCIEDE2000Comparator(tolerance) { | ||
const upperBound = tolerance * 6.2; // cie76 <= 6.2 * ciede2000 | ||
const lowerBound = tolerance * 0.695; // cie76 >= 0.695 * ciede2000 | ||
return function doColorsLookSame(data) { | ||
@@ -34,2 +36,16 @@ if (areColorsSame(data)) { | ||
const cie76 = Math.sqrt( | ||
(lab1.L - lab2.L) * (lab1.L - lab2.L) + | ||
(lab1.a - lab2.a) * (lab1.a - lab2.a) + | ||
(lab1.b - lab2.b) * (lab1.b - lab2.b) | ||
); | ||
if (cie76 >= upperBound) { | ||
return false; | ||
} | ||
if (cie76 <= lowerBound) { | ||
return true; | ||
} | ||
return colorDiff.diff(lab1, lab2) < tolerance; | ||
@@ -100,3 +116,3 @@ }; | ||
if (!options.comparator({color1, color2, img1, img2, x, y, width, height})) { | ||
if (!options.comparator({color1, color2, img1, img2, x, y, width, height, minWidth, minHeight})) { | ||
setPixel(resultBuffer, x, y, highlightColor); | ||
@@ -111,12 +127,2 @@ } else { | ||
const parseColorString = (str) => { | ||
const parsed = parseColor(str || '#ff00ff'); | ||
return { | ||
R: parsed.rgb[0], | ||
G: parsed.rgb[1], | ||
B: parsed.rgb[2] | ||
}; | ||
}; | ||
const getToleranceFromOpts = (opts) => { | ||
@@ -169,6 +175,6 @@ if (!_.hasIn(opts, 'tolerance')) { | ||
return {equal: true, metaInfo, diffBounds, diffClusters: [diffBounds]}; | ||
return {equal: true, metaInfo, diffBounds, diffClusters: [diffBounds], diffImage: null}; | ||
} | ||
if (first.width !== second.width || first.height !== second.height) { | ||
if (!opts.createDiffImage && (first.width !== second.width || first.height !== second.height)) { | ||
const diffBounds = getMaxDiffBounds(first, second); | ||
@@ -186,4 +192,8 @@ | ||
const comparator = createComparator(img1, img2, opts); | ||
const {stopOnFirstFail, shouldCluster, clustersSize} = opts; | ||
const {stopOnFirstFail, shouldCluster, clustersSize, createDiffImage, highlightColor} = opts; | ||
if (createDiffImage) { | ||
return utils.calcDiffImage(img1, img2, comparator, {highlightColor, shouldCluster, clustersSize}); | ||
} | ||
const {diffArea, diffClusters} = await utils.getDiffPixelsCoords(img1, img2, comparator, {stopOnFirstFail, shouldCluster, clustersSize}); | ||
@@ -224,3 +234,3 @@ const diffBounds = diffArea.area; | ||
const diffImage = await buildDiffImage(first, second, { | ||
highlightColor: parseColorString(opts.highlightColor), | ||
highlightColor: utils.parseColorString(opts.highlightColor), | ||
comparator: createComparator(first, second, opts) | ||
@@ -227,0 +237,0 @@ }); |
@@ -7,3 +7,4 @@ 'use strict'; | ||
REQUIRED_BOUNDING_BOX_FIELDS: ['left', 'top', 'right', 'bottom'], | ||
CLUSTERS_SIZE: 10 | ||
CLUSTERS_SIZE: 10, | ||
DIFF_IMAGE_CHANNELS: 3 | ||
}; |
'use strict'; | ||
const _ = require('lodash'); | ||
const STATES = { | ||
@@ -34,3 +32,3 @@ InitState: require('./states/init'), | ||
_checkIsCaret(data) { | ||
return this._state.validate(_.pick(data, ['x', 'y']), _.pick(data, ['img1', 'img2'])); | ||
return this._state.validate(data); | ||
} | ||
@@ -37,0 +35,0 @@ |
'use strict'; | ||
const _ = require('lodash'); | ||
const State = require('./state'); | ||
@@ -8,10 +7,10 @@ const areColorsSame = require('../../same-colors'); | ||
module.exports = class InitState extends State { | ||
validate(firstCaretPoint, imgs) { | ||
const lastCaretPoint = this._getLastCaretPoint(firstCaretPoint, imgs); | ||
validate(data) { | ||
const lastCaretPoint = this._getLastCaretPoint(data); | ||
if (!this._looksLikeCaret(firstCaretPoint, lastCaretPoint)) { | ||
if (!this._looksLikeCaret(data, lastCaretPoint)) { | ||
return false; | ||
} | ||
this.caretTopLeft = firstCaretPoint; | ||
this.caretTopLeft = data; | ||
this.caretBottomRight = lastCaretPoint; | ||
@@ -24,10 +23,10 @@ | ||
_getLastCaretPoint(firstCaretPoint, imgs) { | ||
let currPoint = firstCaretPoint; | ||
_getLastCaretPoint(data) { | ||
let currPoint = data; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
const nextPoint = this._getNextCaretPoint(firstCaretPoint, currPoint); | ||
const nextPoint = this._getNextCaretPoint(data, currPoint); | ||
if (this._isPointOutsideImages(nextPoint, imgs) || this._areColorsSame(nextPoint, imgs)) { | ||
if (this._isPointOutsideImages(nextPoint, data) || this._areColorsSame(nextPoint, data)) { | ||
return currPoint; | ||
@@ -39,9 +38,9 @@ } | ||
_isPointOutsideImages(point, imgs) { | ||
return _.some(imgs, (img) => point.x >= img.width || point.y >= img.height); | ||
_isPointOutsideImages(point, data) { | ||
return point.x >= data.minWidth || point.y >= data.minHeight; | ||
} | ||
_areColorsSame(point, imgs) { | ||
const color1 = imgs.img1.getPixel(point.x, point.y); | ||
const color2 = imgs.img2.getPixel(point.x, point.y); | ||
_areColorsSame(point, data) { | ||
const color1 = data.img1.getPixel(point.x, point.y); | ||
const color2 = data.img2.getPixel(point.x, point.y); | ||
@@ -48,0 +47,0 @@ return areColorsSame({color1, color2}); |
@@ -21,2 +21,10 @@ 'use strict'; | ||
async initMeta() { | ||
const {width, height, channels} = await this._img.metadata(); | ||
this._width = width; | ||
this._height = height; | ||
this._channels = channels; | ||
} | ||
getPixel(x, y) { | ||
@@ -23,0 +31,0 @@ const idx = this._getIdx(x, y); |
100
lib/utils.js
'use strict'; | ||
const _ = require('lodash'); | ||
const parseColor = require('parse-color'); | ||
const img = require('./image'); | ||
@@ -9,2 +10,4 @@ const buffer = require('./img-buffer'); | ||
const validators = require('./validators'); | ||
const areColorsSame = require('./same-colors'); | ||
const {DIFF_IMAGE_CHANNELS} = require('./constants'); | ||
@@ -100,1 +103,98 @@ exports.readImgCb = async ({source, ...opts}) => { | ||
}; | ||
exports.parseColorString = (str) => { | ||
const parsed = parseColor(str || '#ff00ff'); | ||
return { | ||
R: parsed.rgb[0], | ||
G: parsed.rgb[1], | ||
B: parsed.rgb[2] | ||
}; | ||
}; | ||
exports.calcDiffImage = async (img1, img2, comparator, {highlightColor, shouldCluster, clustersSize}) => { | ||
const diffColor = exports.parseColorString(highlightColor); | ||
const minHeight = Math.min(img1.height, img2.height); | ||
const minWidth = Math.min(img1.width, img2.width); | ||
const maxHeight = Math.max(img1.height, img2.height); | ||
const maxWidth = Math.max(img1.width, img2.width); | ||
const totalPixels = maxHeight * maxWidth; | ||
const metaInfo = {refImg: {size: {width: img1.width, height: img1.height}}}; | ||
const diffBuffer = Buffer.alloc(maxHeight * maxWidth * DIFF_IMAGE_CHANNELS); | ||
const diffArea = new DiffArea(); | ||
const diffClusters = new DiffClusters(clustersSize); | ||
let differentPixels = 0; | ||
let diffBufferPos = 0; | ||
const markDiff = (x, y) => { | ||
diffBuffer[diffBufferPos++] = diffColor.R; | ||
diffBuffer[diffBufferPos++] = diffColor.G; | ||
diffBuffer[diffBufferPos++] = diffColor.B; | ||
differentPixels++; | ||
diffArea.update(x, y); | ||
if (shouldCluster) { | ||
diffClusters.update(x, y); | ||
} | ||
}; | ||
for (let y = 0; y < maxHeight; y++) { | ||
for (let x = 0; x < maxWidth; x++) { | ||
if (y > minHeight || x > minWidth) { | ||
markDiff(x, y); // Out of bounds pixels considered as diff | ||
continue; | ||
} | ||
const color1 = img1.getPixel(x, y); | ||
const color2 = img2.getPixel(x, y); | ||
const areSame = areColorsSame({color1, color2}) || comparator({ | ||
img1, | ||
img2, | ||
x, | ||
y, | ||
color1, | ||
color2, | ||
width: maxWidth, | ||
height: maxHeight, | ||
minWidth, | ||
minHeight | ||
}); | ||
if (areSame) { | ||
diffBuffer[diffBufferPos++] = color2.R; | ||
diffBuffer[diffBufferPos++] = color2.G; | ||
diffBuffer[diffBufferPos++] = color2.B; | ||
} else { | ||
markDiff(x, y); | ||
} | ||
} | ||
// eslint-disable-next-line no-bitwise | ||
if (!(y & 0xff)) { // Release event queue every 256 rows | ||
await new Promise(setImmediate); | ||
} | ||
} | ||
let diffImage = null; | ||
if (differentPixels) { | ||
diffImage = await img.fromBuffer(diffBuffer, {raw: {width: maxWidth, height: maxHeight, channels: DIFF_IMAGE_CHANNELS}}); | ||
await diffImage.initMeta(); | ||
} | ||
return { | ||
equal: !differentPixels, | ||
metaInfo, | ||
diffImage, | ||
differentPixels, | ||
totalPixels, | ||
diffBounds: diffArea.area, | ||
diffClusters: diffClusters.clusters | ||
}; | ||
}; |
{ | ||
"name": "looks-same", | ||
"version": "8.1.0", | ||
"version": "8.2.0", | ||
"description": "Pure node.js library for comparing PNG-images, taking into account human color perception.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -130,2 +130,22 @@ # LooksSame | ||
## Comparing images and creating diff image simultaneously | ||
If you need both co compare images and create diff image, you can pass option `createDiffImage: true`, | ||
it would work faster than two separate function calls: | ||
```javascript | ||
const { | ||
equal, | ||
diffImage, | ||
differentPixels, | ||
totalPixels, | ||
diffBounds, | ||
diffClusters | ||
} = await looksSame('image1.png', 'image2.png', {createDiffImage: true}); | ||
if (!equal) { | ||
await diffImage.save('diffImage.png'); | ||
} | ||
``` | ||
## Comparing colors | ||
@@ -132,0 +152,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
53265
1134
161
0