isomorphic-image-hash
Version:
A dhash library that works in both browsers and Node.js.
182 lines (181 loc) • 7.39 kB
JavaScript
/*! isomorphic-image-hash v0.2.0 @license MIT */
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
//#region src/utils/pixcel.ts
const getGrayscaleValue = (imageData, col, row) => {
const base = (row * imageData.width + col) * 4;
const r = imageData.data[base];
const g = imageData.data[base + 1];
const b = imageData.data[base + 2];
return .299 * r + .587 * g + .114 * b;
};
//#endregion
//#region src/utils/resize.ts
const colorChannels = 4;
const resize = (imageData, width, height) => {
return {
width,
height,
data: resizeHeight(resizeWidth(imageData.data, imageData.width, imageData.height, width), width, imageData.height, height)
};
};
const resizeWidth = (buffer, originalWidth, originalHeight, targetWidth) => {
if (originalWidth === targetWidth) return buffer;
const targetWidthMultipliedByChannels = targetWidth * colorChannels;
const originalWidthMultipliedByChannels = originalWidth * colorChannels;
const originalHeightMultipliedByChannels = originalHeight * colorChannels;
const widthPassResultSize = targetWidthMultipliedByChannels * originalHeight;
const nextLineOffsetOriginalWidth = originalWidthMultipliedByChannels - 3;
const nextLineOffsetTargetWidth = targetWidthMultipliedByChannels - 3;
const ratioWeight = originalWidth / targetWidth;
const ratioWeightDivisor = 1 / ratioWeight;
let weight = 0;
let amountToNext = 0;
let actualPosition = 0;
let currentPosition = 0;
let line = 0;
let pixelOffset = 0;
let outputOffset = 0;
const output = new Float32Array(originalHeightMultipliedByChannels);
const outputBuffer = new Float32Array(widthPassResultSize);
do {
for (line = 0; line < originalHeightMultipliedByChannels;) {
output[line++] = 0;
output[line++] = 0;
output[line++] = 0;
output[line++] = 0;
}
weight = ratioWeight;
do {
amountToNext = 1 + actualPosition - currentPosition;
if (weight >= amountToNext) {
for (line = 0, pixelOffset = actualPosition; line < originalHeightMultipliedByChannels; pixelOffset += nextLineOffsetOriginalWidth) {
output[line++] += buffer[pixelOffset++] * amountToNext;
output[line++] += buffer[pixelOffset++] * amountToNext;
output[line++] += buffer[pixelOffset++] * amountToNext;
output[line++] += buffer[pixelOffset] * amountToNext;
}
currentPosition = actualPosition = actualPosition + 4;
weight -= amountToNext;
} else {
for (line = 0, pixelOffset = actualPosition; line < originalHeightMultipliedByChannels; pixelOffset += nextLineOffsetOriginalWidth) {
output[line++] += buffer[pixelOffset++] * weight;
output[line++] += buffer[pixelOffset++] * weight;
output[line++] += buffer[pixelOffset++] * weight;
output[line++] += buffer[pixelOffset] * weight;
}
currentPosition += weight;
break;
}
} while (weight > 0 && actualPosition < originalWidthMultipliedByChannels);
for (line = 0, pixelOffset = outputOffset; line < originalHeightMultipliedByChannels; pixelOffset += nextLineOffsetTargetWidth) {
outputBuffer[pixelOffset++] = output[line++] * ratioWeightDivisor;
outputBuffer[pixelOffset++] = output[line++] * ratioWeightDivisor;
outputBuffer[pixelOffset++] = output[line++] * ratioWeightDivisor;
outputBuffer[pixelOffset] = output[line++] * ratioWeightDivisor;
}
outputOffset += 4;
} while (outputOffset < targetWidthMultipliedByChannels);
return outputBuffer;
};
const resizeHeight = (buffer, originalWidth, originalHeight, targetHeight) => {
if (originalHeight === targetHeight) return new Uint8ClampedArray(buffer);
const targetWidthMultipliedByChannels = originalWidth * colorChannels;
const widthPassResultSize = targetWidthMultipliedByChannels * originalHeight;
const finalResultSize = targetWidthMultipliedByChannels * targetHeight;
const ratioWeight = originalHeight / targetHeight;
const ratioWeightDivisor = 1 / ratioWeight;
let weight = 0;
let amountToNext = 0;
let actualPosition = 0;
let currentPosition = 0;
let pixelOffset = 0;
let outputOffset = 0;
const output = new Float32Array(targetWidthMultipliedByChannels);
const outputBuffer = new Uint8ClampedArray(finalResultSize);
do {
for (pixelOffset = 0; pixelOffset < targetWidthMultipliedByChannels;) {
output[pixelOffset++] = 0;
output[pixelOffset++] = 0;
output[pixelOffset++] = 0;
output[pixelOffset++] = 0;
}
weight = ratioWeight;
do {
amountToNext = 1 + actualPosition - currentPosition;
if (weight >= amountToNext) {
for (pixelOffset = 0; pixelOffset < targetWidthMultipliedByChannels;) {
output[pixelOffset++] += buffer[actualPosition++] * amountToNext;
output[pixelOffset++] += buffer[actualPosition++] * amountToNext;
output[pixelOffset++] += buffer[actualPosition++] * amountToNext;
output[pixelOffset++] += buffer[actualPosition++] * amountToNext;
}
currentPosition = actualPosition;
weight -= amountToNext;
} else {
for (pixelOffset = 0, amountToNext = actualPosition; pixelOffset < targetWidthMultipliedByChannels;) {
output[pixelOffset++] += buffer[amountToNext++] * weight;
output[pixelOffset++] += buffer[amountToNext++] * weight;
output[pixelOffset++] += buffer[amountToNext++] * weight;
output[pixelOffset++] += buffer[amountToNext++] * weight;
}
currentPosition += weight;
break;
}
} while (weight > 0 && actualPosition < widthPassResultSize);
for (pixelOffset = 0; pixelOffset < targetWidthMultipliedByChannels;) {
outputBuffer[outputOffset++] = Math.round(output[pixelOffset++] * ratioWeightDivisor);
outputBuffer[outputOffset++] = Math.round(output[pixelOffset++] * ratioWeightDivisor);
outputBuffer[outputOffset++] = Math.round(output[pixelOffset++] * ratioWeightDivisor);
outputBuffer[outputOffset++] = Math.round(output[pixelOffset++] * ratioWeightDivisor);
}
} while (outputOffset < finalResultSize);
return outputBuffer;
};
//#endregion
//#region src/hash/dhash.ts
const size = 8;
const dhashFromImageData = (imageData) => {
const resizedImageData = resize(imageData, size + 1, size);
const list = Array(size * size);
for (let row = 0; row < size; row++) for (let col = 0; col < size; col++) {
const left = getGrayscaleValue(resizedImageData, col, row);
const right = getGrayscaleValue(resizedImageData, col + 1, row);
list[row * (size + 1) + col] = left < right ? 1 : 0;
}
return list.join("");
};
//#endregion
//#region src/image-data/browser.ts
const getImageDataForBrowser = async (path) => {
const image = new Image();
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
await new Promise((resolve) => {
image.onload = resolve;
image.src = path;
});
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
return ctx.getImageData(0, 0, canvas.width, canvas.height);
};
//#endregion
//#region src/utils/distance.ts
const distance = (a, b) => {
let count = 0;
for (let i = a.length; i--;) if (a[i] !== b[i]) ++count;
return count;
};
//#endregion
//#region src/index.ts
const getImageData = (path) => {
return getImageDataForBrowser(path);
};
const dhash = async (image) => {
return dhashFromImageData(typeof image === "string" ? await getImageData(image) : image);
};
//#endregion
exports.dhash = dhash;
exports.dhashFromImageData = dhashFromImageData;
exports.distance = distance;
exports.getImageData = getImageData;