├── .babelrc ├── .circleci └── config.yml ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __tests__ └── main.test.ts ├── example ├── Example ├── Example.jpg ├── Example.png ├── Example.webp ├── Example2.PNG ├── Example2.webp ├── ExamplePNG ├── Examplejpg ├── _103508526_evans_new.jpg ├── _95695590_tv039055678.jpg ├── _95695591_tv039055678.jpeg ├── _95695592_tv039055678jpeg ├── giphygif ├── jpgpretendingtobeapng.png ├── local-file-js ├── local-file-png.js ├── local-file-webp.js ├── local-file.js ├── piggy.png ├── remote-file.js └── remote-object.js ├── package-lock.json ├── package.json ├── src ├── block-hash.ts └── index.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", {"targets": {"node": "current"}}], 4 | "@babel/preset-typescript" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-transform-modules-commonjs" 8 | ] 9 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | test: 8 | working_directory: ~/repo 9 | docker: 10 | - image: circleci/node:latest 11 | steps: 12 | - checkout 13 | - run: npm ci 14 | - run: npm test 15 | deploy: 16 | working_directory: ~/repo 17 | docker: 18 | - image: circleci/node:latest 19 | steps: 20 | - checkout 21 | - run: git config --global user.email "ci@circleci.com" 22 | - run: git config --global user.name "CircleCI" 23 | - run: npm ci 24 | - run: npm test 25 | - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 26 | - run: npm version patch 27 | - run: npm run build 28 | - run: npm publish 29 | workflows: 30 | version: 2 31 | test: 32 | jobs: 33 | - test: 34 | filters: # using regex filters requires the entire branch to match 35 | branches: 36 | ignore: # only branches matching the below regex filters will run 37 | - master 38 | test-build-deploy: 39 | jobs: 40 | - deploy: 41 | filters: # using regex filters requires the entire branch to match 42 | branches: 43 | only: # only branches matching the below regex filters will run 44 | - master -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb-base', 'airbnb-typescript/base'], 3 | parserOptions: { 4 | project: './tsconfig.json' 5 | } 6 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | lib 39 | .DS_Store 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "eslint.enable": true, 4 | "eslint.validate": [ 5 | "javascript", 6 | "javascriptreact", 7 | "typescript", 8 | "typescriptreact" 9 | ] 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Daniel Morrison 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # image-hash 2 | 3 | A wrapper around [block-hash](https://github.com/commonsmachinery/blockhash-js) to easily hash a local or remote file with Node. 4 | 5 | Supports JPG, PNG and WebP 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm i -S image-hash 11 | ``` 12 | 13 | ## Use 14 | 15 | ```javascript 16 | const { imageHash }= require('image-hash'); 17 | 18 | // remote file simple 19 | imageHash('https://ichef-1.bbci.co.uk/news/660/cpsprodpb/7F76/production/_95703623_mediaitem95703620.jpg', 16, true, (error, data) => { 20 | if (error) throw error; 21 | console.log(data); 22 | // 0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0 23 | }); 24 | 25 | // remote file with requestjs config object 26 | const config = { 27 | uri: 'https://ichef-1.bbci.co.uk/news/660/cpsprodpb/7F76/production/_95703623_mediaitem95703620.jpg' 28 | }; 29 | 30 | imageHash(config, 16, true, (error, data) => { 31 | if (error) throw error; 32 | console.log(data); 33 | // 0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0 34 | }); 35 | 36 | //local file 37 | imageHash('./_95695590_tv039055678.jpg', 16, true, (error, data) => { 38 | if (error) throw error; 39 | console.log(data); 40 | // 0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0 41 | }); 42 | 43 | //Buffer 44 | const fBuffer = fs.readFileSync(__dirname + '/example/_95695591_tv039055678.jpeg'); 45 | imageHash({ 46 | ext: 'image/jpeg', 47 | data: fBuffer 48 | }, 16, true, (error, data) => { 49 | if(error) throw error; 50 | console.log(data); 51 | // 0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0 52 | }); 53 | 54 | //Buffer, without ext arg 55 | const fBuffer = fs.readFileSync(__dirname + '/example/_95695591_tv039055678.jpeg'); 56 | imageHash({ 57 | data: fBuffer 58 | }, 16, true, (error, data) => { 59 | if(error) throw error; 60 | console.log(data); 61 | // 0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0 62 | }); 63 | ``` 64 | 65 | ## API 66 | 67 | ```typescript 68 | // name 69 | imageHash(location, bits, precise, callback); 70 | 71 | // types 72 | imageHash(string|object, int, bool, function); 73 | ``` 74 | 75 | ## SETTINGS 76 | Image hash will log out warnings if environment variable `VERBOSE` is set to true. 77 | 78 | 79 | ### Image-Hash Arguments 80 | 81 | | Argument | Type | Description | Mandatory | Example | 82 | | -------- | ---- | ----------- | --------- | ------- | 83 | | location | `object` or `string` | A [RequestJS Object](https://github.com/request/request#requestoptions-callback), `Buffer` object (See input types below for more details), or `String` with a valid url or file location | Yes | see above | 84 | | bits | `int` | The number of bits in a row. The more bits, the more unique the hash. | Yes | 8 | 85 | | precise | `bool` | Whether a precision algorithm is used. `true` Precise but slower, non-overlapping blocks. `false` Quick and crude, non-overlapping blocks. Method 2 is recommended as a good tradeoff between speed and good matches on any image size. The quick ones are only advisable when the image width and height are an even multiple of the number of blocks used. | Yes | `true` | 86 | | callback | `function` | A function with `error` and `data` arguments - see below | 87 | 88 | #### Location Object Types 89 | 90 | ```typescript 91 | // Url Request Object 92 | interface UrlRequestObject { 93 | encoding?: string | null, 94 | url: string | null, 95 | }; 96 | 97 | // Buffer Object 98 | interface BufferObject { 99 | ext?: string, // mime type of buffered file 100 | data: Buffer, 101 | name?: string // file name for buffered file 102 | }; 103 | ``` 104 | 105 | ### Callback Arguments 106 | 107 | | Argument | Type | Description | 108 | | -------- | ------------------------ | ----------------------------------------------------------------------------------- | 109 | | error | `Error Object` or `null` | If a run time error is detected this will be an `Error Object`, otherwise `null` | 110 | | data | `string` or `null` | If there is no run time error, this be will be your hashed result, otherwise `null` | 111 | 112 | ## Development 113 | 114 | I have made this with Typescript, ESLint, Jest, Babel and VSCode. All config files and global binaries are included. For developers using VS Code, make sure you have ESLint extension installed. 115 | 116 | ## Testing 117 | 118 | `npm test` 119 | 120 | ## Credit 121 | 122 | The hard bit of this comes with thanks from [commonsmachinery](https://github.com/commonsmachinery) for [blockhash-js](https://github.com/commonsmachinery/blockhash-js) 123 | 124 | ## License 125 | 126 | Distributed under an MIT license 127 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | import { expect } from 'chai'; 3 | import fs from 'fs'; 4 | import { imageHash } from '../src/'; 5 | 6 | describe('hash images', () => { 7 | it('should hash a local jpg', (done) => { 8 | imageHash('example/_95695590_tv039055678.jpg', 16, true, (err, res) => { 9 | expect(res).to.equal('0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0'); 10 | done(); 11 | }); 12 | }); 13 | 14 | it('should hash a local jpg', (done) => { 15 | imageHash('example/_95695591_tv039055678.jpeg', 16, true, (err, res) => { 16 | expect(res).to.equal('0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0'); 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should hash a local png', (done) => { 22 | imageHash('example/Example.png', 16, true, (err, res) => { 23 | expect(res).to.equal('00007ffe7c3e780e601e603e7ffe7ffe47fe020642067ff66b066a567ffe7ffe'); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should hash a local PNG', (done) => { 29 | imageHash('example/Example2.PNG', 16, true, (err, res) => { 30 | expect(res).to.equal('00007ffe7c3e780e601e603e7ffe7ffe47fe020642067ff66b066a567ffe7ffe'); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('should throw error when there is a mime type mismatch', (done) => { 36 | imageHash('example/jpgpretendingtobeapng.png', 16, true, (err) => { 37 | expect(err).instanceOf(Error); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('should throw an error when there is no src', (done) => { 43 | const undef = {}; 44 | // @ts-ignore 45 | imageHash(undef.some, 16, true, (err) => { 46 | expect(err).instanceOf(Error); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('Should hash remote image', (done) => { 52 | imageHash('https://ichef.bbci.co.uk/news/800/cpsprodpb/145F4/production/_106744438_p077xzvx.jpg', 16, true, (err, res) => { 53 | if (err) { 54 | return done(err); 55 | } 56 | expect(res).to.equal('dfffbe3ff83fc03fc43ffc17bc07f803f00ff00ff00fe00ff05fe00fe00fe00f'); 57 | return done(); 58 | }); 59 | }); 60 | 61 | it('Should handle error when url is not found', (done) => { 62 | imageHash('https://ichef.bbo.co.uk/news/800/cpsprodpb/145F4/production/_106744438_p077xzvx.jpg', 16, true, (err) => { 63 | expect(err).instanceOf(Error); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('Should handle error when file is not found', (done) => { 69 | imageHash('example/jpgpreten.png', 16, true, (err) => { 70 | expect(err).instanceOf(Error); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('Should hash without jpg ext', (done) => { 76 | imageHash('example/Example', 16, true, (err, res) => { 77 | expect(res).to.equal('00007ffe7c3e780e601e603e7ffe7ffe47fe020642067ff66b066a567ffe7ffe'); 78 | done(); 79 | }); 80 | }); 81 | 82 | it('Should handle error when unreconised mime type', (done) => { 83 | imageHash('example/local-file-js', 16, true, (err) => { 84 | expect(err).instanceOf(Error); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('Should handle error when unreconised mime type', (done) => { 90 | imageHash('example/giphygif', 16, true, (err) => { 91 | expect(err).instanceOf(Error); 92 | done(); 93 | }); 94 | }); 95 | 96 | it('Should handle jpg with no file extension', (done) => { 97 | imageHash('example/_95695592_tv039055678jpeg', 16, true, (err, res) => { 98 | expect(res).to.equal('0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0'); 99 | done(); 100 | }); 101 | }); 102 | 103 | it('Should handle local jpg with file extension', (done) => { 104 | imageHash('example/_95695591_tv039055678.jpeg', 16, true, (err, res) => { 105 | expect(res).to.equal('0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0'); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('Should handle custom request object', (done) => { 111 | imageHash({ 112 | url: 'https://ichef.bbci.co.uk/news/800/cpsprodpb/145F4/production/_106744438_p077xzvx.jpg', 113 | }, 16, true, (err, res) => { 114 | if (err) { 115 | return done(err); 116 | } 117 | expect(res).to.equal('dfffbe3ff83fc03fc43ffc17bc07f803f00ff00ff00fe00ff05fe00fe00fe00f'); 118 | done(err); 119 | }); 120 | }); 121 | 122 | it('Should handle url when no extenion provided (#7)', (done) => { 123 | imageHash({ 124 | url: 'https://falabella.scene7.com/is/image/Falabella/prod11830022_6', 125 | }, 16, true, (err, res) => { 126 | expect(res).to.equal('80ff807f807f807fcc7fc007c067c077c8f3c183c013ccf7c823c8f3f8f7f8ff'); 127 | done(); 128 | }); 129 | }); 130 | 131 | it('Should handle local file buffer', (done) => { 132 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 133 | fs.readFile(`${__dirname}/../example/_95695591_tv039055678.jpeg`, (err, data) => { 134 | if (err) { 135 | return done(err); 136 | } 137 | imageHash({ 138 | ext: 'image/jpeg', 139 | data, 140 | }, 16, true, (error, res) => { 141 | if (error) { 142 | return done(error); 143 | } 144 | expect(res).equal('0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0').and.not.length(0); 145 | return done(); 146 | }); 147 | }); 148 | }); 149 | 150 | it('Should handle buffer with incorrect mime type', (done) => { 151 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 152 | fs.readFile(`${__dirname}/../example/_95695591_tv039055678.jpeg`, (err, data) => { 153 | if (err) { 154 | return done(err); 155 | } 156 | imageHash({ 157 | ext: 'image/jpg', 158 | data, 159 | }, 16, true, (error) => { 160 | expect(error).instanceOf(Error); 161 | return done(); 162 | }); 163 | }); 164 | }); 165 | 166 | it('Should handle local file buffer, without ext arg', (done) => { 167 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 168 | fs.readFile(`${__dirname}/../example/_95695591_tv039055678.jpeg`, (err, data) => { 169 | if (err) { 170 | return done(err); 171 | } 172 | imageHash({ 173 | data, 174 | }, 16, true, (error, res) => { 175 | if (error) { 176 | return done(error); 177 | } 178 | expect(res).equal('0773063f063f36070e070a070f378e7f1f000fff0fff020103f00ffb0f810ff0').and.not.length(0); 179 | return done(); 180 | }); 181 | }); 182 | }); 183 | 184 | it('Should handle remote file buffer', (done) => { 185 | try { 186 | const testUrl = 'https://ichef.bbci.co.uk/news/800/cpsprodpb/145F4/production/_106744438_p077xzvx.jpg'; 187 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 188 | request({ url: testUrl, encoding: null }, (err, _resp, buffer) => { 189 | if (err) { 190 | return done(err); 191 | } 192 | imageHash({ 193 | ext: 'image/jpeg', 194 | data: buffer, 195 | }, 16, true, (imgErr, res) => { 196 | if (imgErr) { 197 | return done(err); 198 | } 199 | expect(res).not.length(0).and.equal('dfffbe3ff83fc03fc43ffc17bc07f803f00ff00ff00fe00ff05fe00fe00fe00f'); 200 | return done(); 201 | }); 202 | }); 203 | } catch (err) { 204 | return done(err); 205 | } 206 | }); 207 | 208 | it('Should handle remote file buffer, without ext arg', (done) => { 209 | try { 210 | const testUrl = 'https://ichef.bbci.co.uk/news/800/cpsprodpb/145F4/production/_106744438_p077xzvx.jpg'; 211 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 212 | request({ url: testUrl, encoding: null }, (err, _resp, buffer) => { 213 | if (err) { 214 | return done(err); 215 | } 216 | imageHash({ 217 | data: buffer, 218 | }, 16, true, (imgErr, res) => { 219 | if (imgErr) { 220 | return done(err); 221 | } 222 | expect(res).not.length(0).and.equal('dfffbe3ff83fc03fc43ffc17bc07f803f00ff00ff00fe00ff05fe00fe00fe00f'); 223 | return done(); 224 | }); 225 | }); 226 | } catch (err) { 227 | return done(err); 228 | } 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /example/Example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/Example -------------------------------------------------------------------------------- /example/Example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/Example.jpg -------------------------------------------------------------------------------- /example/Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/Example.png -------------------------------------------------------------------------------- /example/Example.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/Example.webp -------------------------------------------------------------------------------- /example/Example2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/Example2.PNG -------------------------------------------------------------------------------- /example/Example2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/Example2.webp -------------------------------------------------------------------------------- /example/ExamplePNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/ExamplePNG -------------------------------------------------------------------------------- /example/Examplejpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/Examplejpg -------------------------------------------------------------------------------- /example/_103508526_evans_new.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/_103508526_evans_new.jpg -------------------------------------------------------------------------------- /example/_95695590_tv039055678.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/_95695590_tv039055678.jpg -------------------------------------------------------------------------------- /example/_95695591_tv039055678.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/_95695591_tv039055678.jpeg -------------------------------------------------------------------------------- /example/_95695592_tv039055678jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/_95695592_tv039055678jpeg -------------------------------------------------------------------------------- /example/giphygif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/giphygif -------------------------------------------------------------------------------- /example/jpgpretendingtobeapng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/jpgpretendingtobeapng.png -------------------------------------------------------------------------------- /example/local-file-js: -------------------------------------------------------------------------------- 1 | const imageHash = require('../index'); 2 | 3 | imageHash('./piggy.png', 16, true, (err, res) => { 4 | if (err) throw err; 5 | console.log(res); 6 | }); -------------------------------------------------------------------------------- /example/local-file-png.js: -------------------------------------------------------------------------------- 1 | const imageHash = require('../index'); 2 | 3 | imageHash('./piggy.png', 16, true, (err, res) => { 4 | if (err) throw err; 5 | console.log(res); 6 | }); -------------------------------------------------------------------------------- /example/local-file-webp.js: -------------------------------------------------------------------------------- 1 | const { imageHash } = require('../'); 2 | 3 | imageHash('./Example.webp', 16, true, (err, res) => { 4 | if (err) throw err; 5 | console.log(res); 6 | }); 7 | -------------------------------------------------------------------------------- /example/local-file.js: -------------------------------------------------------------------------------- 1 | const imageHash = require('../index'); 2 | 3 | imageHash('./_95695590_tv039055678.jpg', 16, true, (err, res) => { 4 | if (err) throw err; 5 | console.log(res); 6 | }); 7 | -------------------------------------------------------------------------------- /example/piggy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danm/image-hash/0c8694cf112b79398f0c6c2d70d7255b5be09ea1/example/piggy.png -------------------------------------------------------------------------------- /example/remote-file.js: -------------------------------------------------------------------------------- 1 | const imageHash = require('../index'); 2 | 3 | imageHash('https://ichef-1.bbci.co.uk/news/660/cpsprodpb/7F76/production/_95703623_mediaitem95703620.jpg', 16, false, (err, res) => { 4 | if (err) throw err; 5 | console.log(res); 6 | }); -------------------------------------------------------------------------------- /example/remote-object.js: -------------------------------------------------------------------------------- 1 | const imageHash = require('../index'); 2 | 3 | const config = { 4 | uri: 'https://ichef-1.bbci.co.uk/news/660/cpsprodpb/7F76/production/_95703623_mediaitem95703620.jpg' 5 | }; 6 | 7 | imageHash(config, 16, true, (err, res) => { 8 | if (err) throw err; 9 | console.log(res); 10 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-hash", 3 | "version": "5.3.1", 4 | "description": "Create a hash from an image", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "npx mocha -r ts-node/register __tests__/*.test.ts ", 9 | "build": "tsc", 10 | "lint": "eslint src/*.ts", 11 | "lint:fix": "eslint src/*.ts --fix" 12 | }, 13 | "files": [ 14 | "lib" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/danm/image-hash.git" 19 | }, 20 | "keywords": [ 21 | "phash", 22 | "hash", 23 | "image hash", 24 | "perceptual hash" 25 | ], 26 | "author": "Daniel Morrison ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/danm/image-hash/issues" 30 | }, 31 | "homepage": "https://github.com/danm/image-hash#readme", 32 | "dependencies": { 33 | "@cwasm/webp": "^0.1.5", 34 | "file-type": "^16.5.3", 35 | "jpeg-js": "^0.4.0", 36 | "pngjs": "^6.0.0", 37 | "request": "^2.81.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "^7.2.3", 41 | "@babel/core": "^7.2.2", 42 | "@babel/plugin-transform-modules-commonjs": "^7.4.4", 43 | "@babel/preset-env": "^7.3.1", 44 | "@babel/preset-typescript": "^7.1.0", 45 | "@babel/runtime": "^7.3.1", 46 | "@types/chai": "^4.2.12", 47 | "@types/mocha": "^9.0.0", 48 | "@types/node": "^11.13.8", 49 | "chai": "^4.2.0", 50 | "eslint": "^8.8.0", 51 | "eslint-config-airbnb-base": "^15.0.0", 52 | "eslint-config-airbnb-typescript": "^16.1.0", 53 | "eslint-plugin-import": "^2.25.4", 54 | "mocha": "^9.0.0", 55 | "ts-node": "^8.10.2", 56 | "typescript": "^3.3.1", 57 | "@types/pngjs": "^6.0.1" 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/block-hash.ts: -------------------------------------------------------------------------------- 1 | const median = (data) => { 2 | const mdarr = data.slice(0).sort((a, b) => a - b); 3 | if (mdarr.length % 2 === 0) { 4 | return (mdarr[mdarr.length / 2] + mdarr[mdarr.length / 2 + 1]) / 2.0; 5 | } 6 | return mdarr[Math.floor(mdarr.length / 2)]; 7 | }; 8 | 9 | const translateBlocksToBits = (blocks, pixelsPerBlock) => { 10 | const newblocks = blocks; 11 | const halfBlockValue = (pixelsPerBlock * 256 * 3) / 2; 12 | const bandsize = blocks.length / 4; 13 | 14 | // Compare medians across four hor zontal bands 15 | for (let i = 0; i < 4; i += 1) { 16 | const m = median(blocks.slice(i * bandsize, (i + 1) * bandsize)); 17 | for (let j = i * bandsize; j < (i + 1) * bandsize; j += 1) { 18 | const v = blocks[j]; 19 | // Output a 1 if the block is brighter than the median. 20 | // With images dominated by black or white, the median may 21 | // end up being 0 or the max value, and thus having a lot 22 | // of blocks of value equal to the median. To avoid 23 | // generating hashes of all zeros or ones, in that case output 24 | // 0 if the median is in the lower value space, 1 otherwise 25 | newblocks[j] = Number( 26 | v > m || (Math.abs(v - m) < 1 && m > halfBlockValue), 27 | ); 28 | } 29 | } 30 | }; 31 | 32 | const bitsToHexhash = (bitsArray) => { 33 | const hex = []; 34 | for (let i = 0; i < bitsArray.length; i += 4) { 35 | const nibble = bitsArray.slice(i, i + 4); 36 | hex.push(parseInt(nibble.join(''), 2).toString(16)); 37 | } 38 | 39 | return hex.join(''); 40 | }; 41 | 42 | const bmvbhashEven = (data, bits) => { 43 | const blocksizeX = Math.floor(data.width / bits); 44 | const blocksizeY = Math.floor(data.height / bits); 45 | 46 | const result = []; 47 | 48 | for (let y = 0; y < bits; y += 1) { 49 | for (let x = 0; x < bits; x += 1) { 50 | let total = 0; 51 | 52 | for (let iy = 0; iy < blocksizeY; iy += 1) { 53 | for (let ix = 0; ix < blocksizeX; ix += 1) { 54 | const cx = x * blocksizeX + ix; 55 | const cy = y * blocksizeY + iy; 56 | const ii = (cy * data.width + cx) * 4; 57 | 58 | const alpha = data.data[ii + 3]; 59 | if (alpha === 0) { 60 | total += 765; 61 | } else { 62 | total += data.data[ii] + data.data[ii + 1] + data.data[ii + 2]; 63 | } 64 | } 65 | } 66 | 67 | result.push(total); 68 | } 69 | } 70 | 71 | translateBlocksToBits(result, blocksizeX * blocksizeY); 72 | return bitsToHexhash(result); 73 | }; 74 | 75 | const bmvbhash = (data, bits) => { 76 | const result = []; 77 | let weightTop; 78 | let weightBottom; 79 | let weightLeft; 80 | let weightRight; 81 | let blockTop; 82 | let blockBottom; 83 | let blockLeft; 84 | let blockRight; 85 | let yMod; 86 | let yFrac; 87 | let yInt; 88 | let xMod; 89 | let xFrac; 90 | let xInt; 91 | const blocks = []; 92 | 93 | const evenX = data.width % bits === 0; 94 | const evenY = data.height % bits === 0; 95 | 96 | if (evenX && evenY) { 97 | return bmvbhashEven(data, bits); 98 | } 99 | 100 | // initialize blocks array with 0s 101 | for (let i = 0; i < bits; i += 1) { 102 | blocks.push([]); 103 | for (let j = 0; j < bits; j += 1) { 104 | blocks[i].push(0); 105 | } 106 | } 107 | 108 | const blockWidth = data.width / bits; 109 | const blockHeight = data.height / bits; 110 | 111 | for (let y = 0; y < data.height; y += 1) { 112 | if (evenY) { 113 | // don't bother dividing y, if the size evenly divides by bits 114 | blockBottom = Math.floor(y / blockHeight); 115 | blockTop = blockBottom; 116 | weightTop = 1; 117 | weightBottom = 0; 118 | } else { 119 | yMod = (y + 1) % blockHeight; 120 | yFrac = yMod - Math.floor(yMod); 121 | yInt = yMod - yFrac; 122 | 123 | weightTop = 1 - yFrac; 124 | weightBottom = yFrac; 125 | 126 | // y_int will be 0 on bottom/right borders and on block boundaries 127 | if (yInt > 0 || y + 1 === data.height) { 128 | blockBottom = Math.floor(y / blockHeight); 129 | blockTop = blockBottom; 130 | } else { 131 | blockTop = Math.floor(y / blockHeight); 132 | blockBottom = Math.ceil(y / blockHeight); 133 | } 134 | } 135 | 136 | for (let x = 0; x < data.width; x += 1) { 137 | let avgvalue; 138 | const ii = (y * data.width + x) * 4; 139 | const alpha = data.data[ii + 3]; 140 | if (alpha === 0) { 141 | avgvalue = 765; 142 | } else { 143 | avgvalue = data.data[ii] + data.data[ii + 1] + data.data[ii + 2]; 144 | } 145 | 146 | if (evenX) { 147 | blockRight = Math.floor(x / blockWidth); 148 | blockLeft = blockRight; 149 | weightLeft = 1; 150 | weightRight = 0; 151 | } else { 152 | xMod = (x + 1) % blockWidth; 153 | xFrac = xMod - Math.floor(xMod); 154 | xInt = xMod - xFrac; 155 | 156 | weightLeft = 1 - xFrac; 157 | weightRight = xFrac; 158 | 159 | // x_int will be 0 on bottom/right borders and on block boundaries 160 | if (xInt > 0 || x + 1 === data.width) { 161 | blockRight = Math.floor(x / blockWidth); 162 | blockLeft = blockRight; 163 | } else { 164 | blockLeft = Math.floor(x / blockWidth); 165 | blockRight = Math.ceil(x / blockWidth); 166 | } 167 | } 168 | 169 | // add weighted pixel value to relevant blocks 170 | blocks[blockTop][blockLeft] += avgvalue * weightTop * weightLeft; 171 | blocks[blockTop][blockRight] += avgvalue * weightTop * weightRight; 172 | blocks[blockBottom][blockLeft] += avgvalue * weightBottom * weightLeft; 173 | blocks[blockBottom][blockRight] += avgvalue * weightBottom * weightRight; 174 | } 175 | } 176 | 177 | for (let i = 0; i < bits; i += 1) { 178 | for (let j = 0; j < bits; j += 1) { 179 | result.push(blocks[i][j]); 180 | } 181 | } 182 | 183 | translateBlocksToBits(result, blockWidth * blockHeight); 184 | return bitsToHexhash(result); 185 | }; 186 | 187 | export default (imgData, bits, method) => { 188 | let hash; 189 | 190 | if (method === 1) { 191 | hash = bmvbhashEven(imgData, bits); 192 | } else if (method === 2) { 193 | hash = bmvbhash(imgData, bits); 194 | } else { 195 | throw new Error('Bad hashing method'); 196 | } 197 | 198 | return hash; 199 | }; 200 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { Buffer } from 'buffer'; 3 | import fileType from 'file-type'; 4 | import jpeg from 'jpeg-js'; 5 | import { PNG } from 'pngjs'; 6 | import request from 'request'; 7 | import { URL } from 'url'; 8 | import webp from '@cwasm/webp'; 9 | import blockhash from './block-hash'; 10 | 11 | export interface UrlRequestObject { 12 | encoding?: string | null, 13 | url: string | null, 14 | } 15 | 16 | export interface BufferObject { 17 | ext?: string, 18 | data: Buffer, 19 | name?: string 20 | } 21 | 22 | const processPNG = (data, bits, method, cb) => { 23 | try { 24 | const png = PNG.sync.read(data); 25 | const res = blockhash(png, bits, method ? 2 : 1); 26 | cb(null, res); 27 | } catch (e) { 28 | cb(e); 29 | } 30 | }; 31 | 32 | const processJPG = (data, bits, method, cb) => { 33 | try { 34 | const decoded = jpeg.decode(data); 35 | const res = blockhash(decoded, bits, method ? 2 : 1); 36 | cb(null, res); 37 | } catch (e) { 38 | cb(e); 39 | } 40 | }; 41 | 42 | const processWebp = (data, bits, method, cb) => { 43 | try { 44 | const decoded = webp.decode(data); 45 | const res = blockhash(decoded, bits, method ? 2 : 1); 46 | cb(null, res); 47 | } catch (e) { 48 | cb(e); 49 | } 50 | }; 51 | 52 | const isUrlRequestObject = (obj: UrlRequestObject | BufferObject): obj is UrlRequestObject => { 53 | const casted = (obj as UrlRequestObject); 54 | return casted.url && casted.url.length > 0; 55 | }; 56 | 57 | const isBufferObject = (obj: UrlRequestObject | BufferObject): obj is BufferObject => { 58 | const casted = (obj as BufferObject); 59 | return Buffer.isBuffer(casted.data) 60 | || (Buffer.isBuffer(casted.data) && (casted.ext && casted.ext.length > 0)); 61 | }; 62 | 63 | // eslint-disable-next-line 64 | export const imageHash = (oldSrc: string | UrlRequestObject | BufferObject, bits, method, cb) => { 65 | const src = oldSrc; 66 | 67 | const getFileType = async (data: Buffer | string) => { 68 | if (typeof src !== 'string' && isBufferObject(src) && src.ext) { 69 | return { 70 | mime: src.ext, 71 | }; 72 | } 73 | if (Buffer.isBuffer(data)) { 74 | return fileType.fromBuffer(data); 75 | } 76 | if (typeof src === 'string') { 77 | return fileType.fromFile(src); 78 | } 79 | return ''; 80 | }; 81 | 82 | const checkFileType = (name, data: Buffer | string) => { 83 | getFileType(data).then((type) => { 84 | // what is the image type 85 | if (!type) { 86 | cb(new Error('Mime type not found')); 87 | return; 88 | } 89 | if (name && name.lastIndexOf('.') > 0) { 90 | const ext = name 91 | .split('.') 92 | .pop() 93 | .toLowerCase(); 94 | if (ext === 'png' && type.mime === 'image/png') { 95 | processPNG(data, bits, method, cb); 96 | } else if ((ext === 'jpg' || ext === 'jpeg') && type.mime === 'image/jpeg') { 97 | processJPG(data, bits, method, cb); 98 | } else if (ext === 'webp' && type.mime === 'image/webp') { 99 | processWebp(data, bits, method, cb); 100 | } else { 101 | cb(new Error(`Unrecognized file extension, mime type or mismatch, ext: ${ext} / mime: ${type.mime}`)); 102 | } 103 | } else { 104 | if (process.env.verbose) console.warn('No file extension found, attempting mime typing.'); 105 | if (type.mime === 'image/png') { 106 | processPNG(data, bits, method, cb); 107 | } else if (type.mime === 'image/jpeg') { 108 | processJPG(data, bits, method, cb); 109 | } else if (type.mime === 'image/webp') { 110 | processWebp(data, bits, method, cb); 111 | } else { 112 | cb(new Error(`Unrecognized mime type: ${type.mime}`)); 113 | } 114 | } 115 | }).catch((err) => { 116 | cb(err); 117 | }); 118 | }; 119 | 120 | const handleRequest = (err, res) => { 121 | if (err) { 122 | cb(new Error(err)); 123 | } else { 124 | const url = new URL(res.request.uri.href); 125 | const name = url.pathname; 126 | checkFileType(name, res.body); 127 | } 128 | }; 129 | 130 | const handleReadFile = (err, res) => { 131 | if (err) { 132 | cb(new Error(err)); 133 | return; 134 | } 135 | checkFileType(src, res); 136 | }; 137 | 138 | // check source 139 | // is source assigned 140 | if (src === undefined) { 141 | cb(new Error('No image source provided')); 142 | return; 143 | } 144 | 145 | // is src url or file 146 | if (typeof src === 'string' && (src.indexOf('http') === 0 || src.indexOf('https') === 0)) { 147 | // url 148 | const req = { 149 | url: src, 150 | encoding: null, 151 | }; 152 | request(req, handleRequest); 153 | } else if (typeof src !== 'string' && isBufferObject(src)) { 154 | // image buffers 155 | checkFileType(src.name, src.data); 156 | } else if (typeof src !== 'string' && isUrlRequestObject(src)) { 157 | // Request Object 158 | src.encoding = null; 159 | request(src, handleRequest); 160 | } else { 161 | // file 162 | fs.readFile(src, handleReadFile); 163 | } 164 | }; 165 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es2015", 6 | "noEmitOnError": true, 7 | "declaration": true, 8 | "resolveJsonModule": true, 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "outDir": "lib", 12 | "baseUrl": ".", 13 | "paths": { 14 | "*": [ 15 | "node_modules/*", 16 | ] 17 | }, 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------