UNPKG

isomorphic-image-hash

Version:

A dhash library that works in both browsers and Node.js.

178 lines (177 loc) 7.24 kB
/*! isomorphic-image-hash v0.2.0 @license MIT */ //#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 export { dhash, dhashFromImageData, distance, getImageData };