diff --git a/.gitignore b/.gitignore index 7e864c7..cb69f7f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules .nyc_output coverage tmp +index.d.ts diff --git a/LICENSE b/LICENSE index 21b4891..3194b3c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2024, Mapbox +Copyright (c) 2025, Mapbox Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice diff --git a/index.js b/index.js index 15094a2..1521d4d 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,30 @@ - +/** + * Compare two equally sized images, pixel by pixel. + * + * @param {Uint8Array | Uint8ClampedArray} img1 First image data. + * @param {Uint8Array | Uint8ClampedArray} img2 Second image data. + * @param {Uint8Array | Uint8ClampedArray | void} output Image data to write the diff to, if provided. + * @param {number} width Input images width. + * @param {number} height Input images height. + * + * @param {Object} [options] + * @param {number} [options.threshold=0.1] Matching threshold (0 to 1); smaller is more sensitive. + * @param {boolean} [options.includeAA=false] Whether to skip anti-aliasing detection. + * @param {number} [options.alpha=0.1] Opacity of original image in diff output. + * @param {[number, number, number]} [options.aaColor=[255, 255, 0]] Color of anti-aliased pixels in diff output. + * @param {[number, number, number]} [options.diffColor=[255, 0, 0]] Color of different pixels in diff output. + * @param {[number, number, number]} [options.diffColorAlt=options.diffColor] Whether to detect dark on light differences between img1 and img2 and set an alternative color to differentiate between the two. + * @param {boolean} [options.diffMask=false] Draw the diff over a transparent background (a mask). + * + * @return {number} The number of mismatched pixels. + */ export default function pixelmatch(img1, img2, output, width, height, options = {}) { const { - threshold = 0.1, // matching threshold (0 to 1); smaller is more sensitive - includeAA = false, // whether to skip anti-aliasing detection - alpha = 0.1, // opacity of original image in diff output - aaColor = [255, 255, 0], // color of anti-aliased pixels in diff output - diffColor = [255, 0, 0], // color of different pixels in diff output - diffColorAlt = null, // whether to detect dark on light differences between img1 and img2 and set an alternative color to differentiate between the two - diffMask = false // draw the diff over a transparent background (a mask) + threshold = 0.1, + alpha = 0.1, + aaColor = [255, 255, 0], + diffColor = [255, 0, 0], + includeAA, diffColorAlt, diffMask } = options; if (!isPixelData(img1) || !isPixelData(img2) || (output && !isPixelData(output))) @@ -54,8 +71,7 @@ export default function pixelmatch(img1, img2, output, width, height, options = // the color difference is above the threshold if (Math.abs(delta) > maxDelta) { // check it's a real rendering difference or just anti-aliasing - if (!includeAA && (antialiased(img1, x, y, width, height, img2) || - antialiased(img2, x, y, width, height, img1))) { + if (!includeAA && (antialiased(img1, x, y, width, height, img2) || antialiased(img2, x, y, width, height, img1))) { // one of the pixels is anti-aliasing; draw as yellow and do not count as difference // note that we do not include such pixels in a mask if (output && !diffMask) drawPixel(output, pos, aaR, aaG, aaB); @@ -83,14 +99,22 @@ export default function pixelmatch(img1, img2, output, width, height, options = return diff; } +/** @param {Uint8Array | Uint8ClampedArray} arr */ function isPixelData(arr) { // work around instanceof Uint8Array not working properly in some Jest environments - return ArrayBuffer.isView(arr) && arr.constructor.BYTES_PER_ELEMENT === 1; + return ArrayBuffer.isView(arr) && arr.BYTES_PER_ELEMENT === 1; } -// check if a pixel is likely a part of anti-aliasing; -// based on "Anti-aliased Pixel and Intensity Slope Detector" paper by V. Vysniauskas, 2009 - +/** + * Check if a pixel is likely a part of anti-aliasing; + * based on "Anti-aliased Pixel and Intensity Slope Detector" paper by V. Vysniauskas, 2009 + * @param {Uint8Array | Uint8ClampedArray} img + * @param {number} x1 + * @param {number} y1 + * @param {number} width + * @param {number} height + * @param {Uint8Array | Uint8ClampedArray} img2 + */ function antialiased(img, x1, y1, width, height, img2) { const x0 = Math.max(x1 - 1, 0); const y0 = Math.max(y1 - 1, 0); @@ -100,7 +124,10 @@ function antialiased(img, x1, y1, width, height, img2) { let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0; let min = 0; let max = 0; - let minX, minY, maxX, maxY; + let minX = 0; + let minY = 0; + let maxX = 0; + let maxY = 0; // go through 8 adjacent pixels for (let x = x0; x <= x2; x++) { @@ -140,7 +167,14 @@ function antialiased(img, x1, y1, width, height, img2) { (hasManySiblings(img, maxX, maxY, width, height) && hasManySiblings(img2, maxX, maxY, width, height)); } -// check if a pixel has 3+ adjacent pixels of the same color. +/** + * Check if a pixel has 3+ adjacent pixels of the same color. + * @param {Uint8Array | Uint8ClampedArray} img + * @param {number} x1 + * @param {number} y1 + * @param {number} width + * @param {number} height + */ function hasManySiblings(img, x1, y1, width, height) { const x0 = Math.max(x1 - 1, 0); const y0 = Math.max(y1 - 1, 0); @@ -167,9 +201,15 @@ function hasManySiblings(img, x1, y1, width, height) { return false; } -// calculate color difference according to the paper "Measuring perceived color difference -// using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos - +/** + * Calculate color difference according to the paper "Measuring perceived color difference + * using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos + * @param {Uint8Array | Uint8ClampedArray} img1 + * @param {Uint8Array | Uint8ClampedArray} img2 + * @param {number} k + * @param {number} m + * @param {boolean} yOnly + */ function colorDelta(img1, img2, k, m, yOnly) { const r1 = img1[k]; const g1 = img1[k + 1]; @@ -209,6 +249,13 @@ function colorDelta(img1, img2, k, m, yOnly) { return y > 0 ? -delta : delta; } +/** + * @param {Uint8Array | Uint8ClampedArray} output + * @param {number} pos + * @param {number} r + * @param {number} g + * @param {number} b + */ function drawPixel(output, pos, r, g, b) { output[pos + 0] = r; output[pos + 1] = g; @@ -216,10 +263,13 @@ function drawPixel(output, pos, r, g, b) { output[pos + 3] = 255; } +/** + * @param {Uint8Array | Uint8ClampedArray} img + * @param {number} i + * @param {number} alpha + * @param {Uint8Array | Uint8ClampedArray} output + */ function drawGrayPixel(img, i, alpha, output) { - const r = img[i + 0]; - const g = img[i + 1]; - const b = img[i + 2]; - const val = 255 + (r * 0.29889531 + g * 0.58662247 + b * 0.11448223 - 255) * alpha * img[i + 3] / 255; + const val = 255 + (img[i] * 0.29889531 + img[i + 1] * 0.58662247 + img[i + 2] * 0.11448223 - 255) * alpha * img[i + 3] / 255; drawPixel(output, i, val, val, val); } diff --git a/package-lock.json b/package-lock.json index 9661345..bd2a273 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ }, "devDependencies": { "eslint": "^9.20.1", - "eslint-config-mourner": "^4.0.2" + "eslint-config-mourner": "^4.0.2", + "typescript": "^5.7.3" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1091,6 +1092,20 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index f80bd2f..f26762e 100644 --- a/package.json +++ b/package.json @@ -4,22 +4,25 @@ "type": "module", "description": "The smallest and fastest pixel-level image comparison library.", "main": "index.js", + "types": "index.d.ts", "bin": { "pixelmatch": "bin/pixelmatch" }, "files": [ - "bin/pixelmatch" + "bin/pixelmatch", + "index.d.ts" ], "dependencies": { "pngjs": "^7.0.0" }, "devDependencies": { "eslint": "^9.20.1", - "eslint-config-mourner": "^4.0.2" + "eslint-config-mourner": "^4.0.2", + "typescript": "^5.7.3" }, "scripts": { "pretest": "eslint", - "test": "node --test" + "test": "tsc && node --test" }, "repository": { "type": "git", @@ -30,7 +33,7 @@ "comparison", "diff" ], - "author": "Vladimir Agafonkin", + "author": "Volodymyr Agafonkin", "license": "ISC", "bugs": { "url": "https://github.com/mapbox/pixelmatch/issues" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5a01506 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "strict": true, + "emitDeclarationOnly": true, + "declaration": true, + "target": "es2017", + "module": "nodenext", + "moduleResolution": "nodenext" + }, + "files": [ + "index.js" + ] +}