UNPKG

isomorphic-image-hash

Version:

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

224 lines (223 loc) 8.67 kB
/*! isomorphic-image-hash v0.2.0 @license MIT */ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); let fs_promises = require("fs/promises"); let path = require("path"); let jpeg_js = require("jpeg-js"); let omggif = require("omggif"); let pngjs = require("pngjs"); //#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/image-data/node.ts const getImageDataForNode = async (path$1) => { if (path$1.match(/^(http|ftp)s?:\/\/./)) { const res = await fetch(path$1); const body = await res.arrayBuffer(); const data = Buffer.from(body); switch (res.headers.get("Content-Type")) { case "image/gif": return readGif(data); case "image/png": return readPng(data); default: return readJpeg(data); } } const data = await (0, fs_promises.readFile)(path$1); switch ((0, path.extname)(path$1)) { case ".gif": return readGif(data); case ".png": return readPng(data); default: return readJpeg(data); } }; const readGif = (data) => { const image = new omggif.GifReader(data); const gifData = new Uint8ClampedArray(image.width * image.height * 4); image.decodeAndBlitFrameRGBA(0, gifData); return { width: image.width, height: image.height, data: gifData }; }; const readJpeg = (data) => { return (0, jpeg_js.decode)(data, { useTArray: true }); }; const readPng = (data) => { return pngjs.PNG.sync.read(data); }; //#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) => { if (typeof document === "object") return getImageDataForBrowser(path); return getImageDataForNode(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;