├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── images ├── face-high.jpg ├── face-low.jpg └── face-with-nose.jpg └── spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store* 3 | *.log 4 | *.gz 5 | 6 | node_modules 7 | coverage 8 | .nyc_output 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "10" 6 | sudo: false 7 | dist: trusty 8 | script: "npm run-script test-travis" 9 | after_script: "npm install coveralls && cat ./coverage/lcov.info | coveralls" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jonathan Ong me@jongleberry.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dhash (difference hash) 2 | ========= 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![Build status][travis-image]][travis-url] 6 | [![Test coverage][coveralls-image]][coveralls-url] 7 | [![Dependency Status][david-image]][david-url] 8 | [![License][license-image]][license-url] 9 | [![Downloads][downloads-image]][downloads-url] 10 | 11 | dHash - image hash implementation for node 12 | 13 | ### Install 14 | 15 | ``` 16 | npm install dhash-image 17 | ``` 18 | 19 | ### Usage 20 | 21 | ``` 22 | 23 | var dhash = require('dhash-image'); 24 | 25 | dhash('/path/to/image', function(err, hash){ 26 | // Do something with hash hex-string... 27 | }); 28 | 29 | // Optional last argument for hash size in bytes (Default: 8) 30 | 31 | ``` 32 | 33 | [gitter-image]: https://badges.gitter.im/mgmtio/dhash-image.png 34 | [gitter-url]: https://gitter.im/mgmtio/dhash-image 35 | [npm-image]: https://img.shields.io/npm/v/dhash-image.svg?style=flat-square 36 | [npm-url]: https://npmjs.org/package/dhash-image 37 | [github-tag]: http://img.shields.io/github/tag/mgmtio/dhash-image.svg?style=flat-square 38 | [github-url]: https://github.com/mgmtio/dhash-image/tags 39 | [travis-image]: https://img.shields.io/travis/mgmtio/dhash-image.svg?style=flat-square 40 | [travis-url]: https://travis-ci.org/mgmtio/dhash-image 41 | [coveralls-image]: https://img.shields.io/coveralls/mgmtio/dhash-image.svg?style=flat-square 42 | [coveralls-url]: https://coveralls.io/r/mgmtio/dhash-image 43 | [david-image]: http://img.shields.io/david/mgmtio/dhash-image.svg?style=flat-square 44 | [david-url]: https://david-dm.org/mgmtio/dhash-image 45 | [license-image]: http://img.shields.io/npm/l/dhash-image.svg?style=flat-square 46 | [license-url]: LICENSE 47 | [downloads-image]: http://img.shields.io/npm/dm/dhash-image.svg?style=flat-square 48 | [downloads-url]: https://npmjs.org/package/dhash-image 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var sharp = require('sharp'); 3 | 4 | var DEFAULT_HASH_SIZE = 8; 5 | 6 | sharp.cache(false); 7 | 8 | module.exports = function(path, callback, hashSize) { 9 | if (typeof callback === 'number') { 10 | hashSize = callback; 11 | callback = null; 12 | } 13 | 14 | var height = hashSize || DEFAULT_HASH_SIZE; 15 | var width = height + 1; 16 | 17 | // Covert to small gray image 18 | var promise = sharp(path) 19 | .grayscale() 20 | .resize(width, height) 21 | .ignoreAspectRatio() 22 | .raw() 23 | .toBuffer() 24 | .then(function(pixels) { 25 | // Compare adjacent pixels. 26 | var difference = ''; 27 | for (var row = 0; row < height; row++) { 28 | for (var col = 0; col < height; col++) { // height is not a mistake here... 29 | var left = px(pixels, width, col, row); 30 | var right = px(pixels, width, col + 1, row); 31 | difference += left < right ? 1 : 0; 32 | } 33 | } 34 | return binaryToHex(difference); 35 | }); 36 | 37 | if (typeof callback === 'function') { 38 | promise.then(function (res) { 39 | callback(null, res); 40 | }, callback); 41 | } 42 | 43 | return promise; 44 | }; 45 | 46 | // TODO: move to a separate module 47 | function binaryToHex(s) { 48 | var output = ''; 49 | for (var i = 0; i < s.length; i += 4) { 50 | var bytes = s.substr(i, 4); 51 | var decimal = parseInt(bytes, 2); 52 | var hex = decimal.toString(16); 53 | output += hex; 54 | } 55 | return new Buffer(output, 'hex'); 56 | } 57 | 58 | function px(pixels, width, x, y) { 59 | var pixel = width * y + x; 60 | assert(pixel < pixels.length); 61 | return pixels[pixel]; 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dhash-image", 3 | "version": "1.3.0", 4 | "description": "Compare image similarity with a dhash", 5 | "keywords": [ 6 | "dhash", 7 | "image", 8 | "compare" 9 | ], 10 | "author": "Richard van der Dys (http://richardvanderdys.com/)", 11 | "contributors": [ 12 | "Jonathan Ong ", 13 | "Lovell Fuller " 14 | ], 15 | "license": "MIT", 16 | "dependencies": { 17 | "sharp": "0.20.8" 18 | }, 19 | "devDependencies": { 20 | "mocha": "5", 21 | "nyc": "13", 22 | "should": "13" 23 | }, 24 | "scripts": { 25 | "test": "mocha --reporter spec", 26 | "test-cov": "nyc mocha --reporter dot", 27 | "test-travis": "nyc mocha --reporter lcovonly --reporter dot" 28 | }, 29 | "repository": "mgmtio/dhash-image", 30 | "files": [ 31 | "index.js" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/images/face-high.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jongleberry-bot/dhash-image/1c6c9b0937a269738c1137f6a3d66861d2412e9e/test/images/face-high.jpg -------------------------------------------------------------------------------- /test/images/face-low.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jongleberry-bot/dhash-image/1c6c9b0937a269738c1137f6a3d66861d2412e9e/test/images/face-low.jpg -------------------------------------------------------------------------------- /test/images/face-with-nose.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jongleberry-bot/dhash-image/1c6c9b0937a269738c1137f6a3d66861d2412e9e/test/images/face-with-nose.jpg -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | var dhash = require(__dirname + "/../index.js"); 3 | 4 | function hamming(a, b) { 5 | var distance = 0; 6 | for (var i = 0; i < a.length; i++) { 7 | if (a[i] != b[i]) distance++; 8 | } 9 | return distance; 10 | } 11 | 12 | describe('dhash', function() { 13 | 14 | it('should get 64 bit hash by default', function(done) { 15 | dhash(__dirname + '/images/face-high.jpg', function(err, hash) { 16 | if (err) throw err; 17 | hash.toString('hex').length.should.equal(64 / 4); // 4 bits to a byte 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should get 256 bit hash if asked for', function(done) { 23 | dhash(__dirname + '/images/face-high.jpg', function(err, hash) { 24 | if (err) throw err; 25 | hash.toString('hex').length.should.equal(256 / 4); 26 | done(); 27 | }, 16); 28 | }); 29 | 30 | it('should have similar hashes for low/high of same image', function(done) { 31 | dhash(__dirname + '/images/face-high.jpg', function(err, highHash) { 32 | if (err) throw err; 33 | dhash(__dirname + '/images/face-low.jpg', function(err, lowHash) { 34 | if (err) throw err; 35 | hamming(highHash, lowHash).should.be.below(4); 36 | done(); 37 | }); 38 | }); 39 | }); 40 | 41 | it('should have similar hashes for similar images', function(done) { 42 | dhash(__dirname + '/images/face-high.jpg', function(err, highHash) { 43 | if (err) throw err; 44 | dhash(__dirname + '/images/face-with-nose.jpg', function(err, lowHash) { 45 | if (err) throw err; 46 | hamming(highHash, lowHash).should.be.below(3); 47 | done(); 48 | }); 49 | }); 50 | }); 51 | 52 | it('should work as a promise', function (done) { 53 | dhash(__dirname + '/images/face-high.jpg' ,16).then(function (hash) { 54 | hash.toString('hex').length.should.equal(256 / 4); 55 | done(); 56 | }); 57 | }) 58 | }); 59 | --------------------------------------------------------------------------------