UNPKG

isomorphic-image-hash

Version:

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

236 lines (226 loc) 8.87 kB
/*! isomorphic-image-hash v0.1.0 @license MIT */ 'use strict'; var promises = require('fs/promises'); var path = require('path'); var jpegJs = require('jpeg-js'); var omggif = require('omggif'); var pngjs = require('pngjs'); 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 0.299 * r + 0.587 * g + 0.114 * b; }; const colorChannels = 4; const resize = (imageData, width, height) => { const resizedWidthData = resizeWidth(imageData.data, imageData.width, imageData.height, width); const data = resizeHeight(resizedWidthData, width, imageData.height, height); return { width, height, data }; }; 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; }; 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(""); }; 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); }; const getImageDataForNode = async (path$1) => { if (path$1.match(/^(http|ftp)s?:\/\/./)) { const res = await fetch(path$1); const body = await res.arrayBuffer(); const data2 = Buffer.from(body); switch (res.headers.get("Content-Type")) { case "image/gif": return readGif(data2); case "image/png": return readPng(data2); default: return readJpeg(data2); } } const data = await promises.readFile(path$1); switch (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 jpegJs.decode(data, { useTArray: true }); }; const readPng = (data) => { return pngjs.PNG.sync.read(data); }; const distance = (a, b) => { let count = 0; for (let i = a.length; i--; ) { if (a[i] !== b[i]) { ++count; } } return count; }; const getImageData = (path) => { if (typeof document === "object") { return getImageDataForBrowser(path); } return getImageDataForNode(path); }; const dhash = async (image) => { const imageData = typeof image === "string" ? await getImageData(image) : image; return dhashFromImageData(imageData); }; exports.dhash = dhash; exports.dhashFromImageData = dhashFromImageData; exports.distance = distance; exports.getImageData = getImageData;