Image Hashing in the Browser
// Original source: | |
// https://github.com/commonsmachinery/blockhash-js | |
// Update for local file reading | |
// Update (loosely) for ES6 | |
// Removes dependencies from origina project | |
class Blockhash { | |
constructor() { | |
this.canvas = document.createElement( 'canvas' ); | |
this.canvas.style.visibility = 'hidden'; | |
this.canvas.style.position = 'absolute'; | |
document.body.appendChild( this.canvas ); | |
this.image = document.createElement( 'img' ); | |
this.image.style.visibility = 'hidden'; | |
this.image.style.position = 'absolute'; | |
this.image.addEventListener( 'load', evt => this.doLoad( evt ) ); | |
document.body.appendChild( this.image ); | |
this.reader = new FileReader(); | |
this.reader.addEventListener( 'load', evt => this.doRead( evt ) ); | |
this.bits = null; | |
this.method = null; | |
this.callback = null; | |
} | |
blockhash( file, bits, method, callback ) { | |
this.bits = bits; | |
this.method = method; | |
this.callback = callback; | |
this.reader.readAsDataURL( file ); | |
} | |
doLoad( evt ) { | |
this.canvas.width = this.image.width; | |
this.canvas.height = this.image.height; | |
let context = this.canvas.getContext( '2d' ); | |
context.drawImage( this.image, 0, 0, this.image.width, this.image.height ); | |
let data = context.getImageData( 0, 0, this.image.width, this.image.height ); | |
let hash = this.blockhashData( data, this.bits, this.method ); | |
this.callback( hash ); | |
} | |
doRead( evt ) { | |
this.image.src = evt.target.result; | |
} | |
/* | |
* From original | |
*/ | |
hammingDistance( hash1, hash2 ) { | |
let d = 0; | |
let i; | |
for( i = 0; i < hash1.length; i++ ) { | |
let n1 = parseInt( hash1[i], 16 ); | |
let n2 = parseInt( hash2[i], 16 ); | |
d += one_bits[n1 ^ n2]; | |
} | |
return d; | |
} | |
median( data ) { | |
let mdarr = data.slice( 0 ); | |
mdarr.sort( function( a, b ) { | |
return a - b; | |
} ); | |
if( mdarr.length % 2 === 0 ) { | |
return ( mdarr[mdarr.length / 2] + mdarr[mdarr.length / 2 + 1] ) / 2.0; | |
} | |
return mdarr[Math.floor( mdarr.length / 2 )]; | |
} | |
translate_blocks_to_bits( blocks, pixels_per_block ) { | |
let half_block_value = pixels_per_block * 256 * 3 / 2; | |
let bandsize = blocks.length / 4; | |
// Compare medians across four horizontal bands | |
for( let i = 0; i < 4; i++ ) { | |
let m = this.median( blocks.slice( i * bandsize, ( i + 1 ) * bandsize ) ); | |
for( let j = i * bandsize; j < ( i + 1 ) * bandsize; j++ ) { | |
let v = blocks[j]; | |
// Output a 1 if the block is brighter than the median. | |
// With images dominated by black or white, the median may | |
// end up being 0 or the max value, and thus having a lot | |
// of blocks of value equal to the median. To avoid | |
// generating hashes of all zeros or ones, in that case output | |
// 0 if the median is in the lower value space, 1 otherwise | |
blocks[j] = Number( v > m || ( Math.abs( v - m ) < 1 && m > half_block_value ) ); | |
} | |
} | |
} | |
bits_to_hexhash( bits ) { | |
let hex = []; | |
for( let i = 0; i < bits.length; i += 4 ) { | |
let nibble = bits.slice( i, i + 4 ); | |
hex.push( parseInt( nibble.join( '' ), 2 ).toString( 16 ) ); | |
} | |
return hex.join( '' ); | |
} | |
bmvbhash_even( data, bits ) { | |
let blocksize_x = Math.floor( data.width / bits ); | |
let blocksize_y = Math.floor( data.height / bits ); | |
let result = []; | |
for( let y = 0; y < bits; y++ ) { | |
for( let x = 0; x < bits; x++ ) { | |
let total = 0; | |
for( let iy = 0; iy < blocksize_y; iy++ ) { | |
for( let ix = 0; ix < blocksize_x; ix++ ) { | |
let cx = x * blocksize_x + ix; | |
let cy = y * blocksize_y + iy; | |
let ii = ( cy * data.width + cx ) * 4; | |
let alpha = data.data[ii+3]; | |
if( alpha === 0 ) { | |
total += 765; | |
} else { | |
total += data.data[ii] + data.data[ii+1] + data.data[ii+2]; | |
} | |
} | |
} | |
result.push( total ); | |
} | |
} | |
this.translate_blocks_to_bits( result, blocksize_x * blocksize_y ); | |
return this.bits_to_hexhash( result ); | |
} | |
bmvbhash( data, bits ) { | |
let result = []; | |
let i, j, x, y; | |
let block_width, block_height; | |
let weight_top, weight_bottom, weight_left, weight_right; | |
let block_top, block_bottom, block_left, block_right; | |
let y_mod, y_frac, y_int; | |
let x_mod, x_frac, x_int; | |
let blocks = []; | |
let even_x = data.width % bits === 0; | |
let even_y = data.height % bits === 0; | |
if( even_x && even_y ) { | |
return this.bmvbhash_even( data, bits ); | |
} | |
// Initialize blocks array with 0s | |
for( i = 0; i < bits; i++ ) { | |
blocks.push( [] ); | |
for( j = 0; j < bits; j++ ) { | |
blocks[i].push( 0 ); | |
} | |
} | |
block_width = data.width / bits; | |
block_height = data.height / bits; | |
for( y = 0; y < data.height; y++ ) { | |
if( even_y ) { | |
// don't bother dividing y, if the size evenly divides by bits | |
block_top = block_bottom = Math.floor( y / block_height ); | |
weight_top = 1; | |
weight_bottom = 0; | |
} else { | |
y_mod = ( y + 1 ) % block_height; | |
y_frac = y_mod - Math.floor( y_mod ); | |
y_int = y_mod - y_frac; | |
weight_top = ( 1 - y_frac ); | |
weight_bottom = ( y_frac ); | |
// y_int will be 0 on bottom/right borders and on block boundaries | |
if( y_int > 0 || ( y + 1 ) === data.height ) { | |
block_top = block_bottom = Math.floor( y / block_height ); | |
} else { | |
block_top = Math.floor( y / block_height ); | |
block_bottom = Math.ceil( y / block_height ); | |
} | |
} | |
for( x = 0; x < data.width; x++ ) { | |
let ii = ( y * data.width + x ) * 4; | |
let avgvalue, alpha = data.data[ii+3]; | |
if( alpha === 0 ) { | |
avgvalue = 765; | |
} else { | |
avgvalue = data.data[ii] + data.data[ii+1] + data.data[ii+2]; | |
} | |
if( even_x ) { | |
block_left = block_right = Math.floor( x / block_width ); | |
weight_left = 1; | |
weight_right = 0; | |
} else { | |
x_mod = ( x + 1 ) % block_width; | |
x_frac = x_mod - Math.floor(x_mod); | |
x_int = x_mod - x_frac; | |
weight_left = ( 1 - x_frac ); | |
weight_right = x_frac; | |
// x_int will be 0 on bottom/right borders and on block boundaries | |
if( x_int > 0 || ( x + 1 ) === data.width ) { | |
block_left = block_right = Math.floor( x / block_width ); | |
} else { | |
block_left = Math.floor( x / block_width ); | |
block_right = Math.ceil( x / block_width ); | |
} | |
} | |
// add weighted pixel value to relevant blocks | |
blocks[block_top][block_left] += avgvalue * weight_top * weight_left; | |
blocks[block_top][block_right] += avgvalue * weight_top * weight_right; | |
blocks[block_bottom][block_left] += avgvalue * weight_bottom * weight_left; | |
blocks[block_bottom][block_right] += avgvalue * weight_bottom * weight_right; | |
} | |
} | |
for( i = 0; i < bits; i++ ) { | |
for( j = 0; j < bits; j++ ) { | |
result.push( blocks[i][j] ); | |
} | |
} | |
this.translate_blocks_to_bits( result, block_width * block_height ); | |
return this.bits_to_hexhash( result ); | |
} | |
blockhashData( data, bits, method ) { | |
var hash; | |
if( method === 1 ) { | |
hash = this.bmvbhash_even( data, bits ); | |
} else if( method === 2 ) { | |
hash = this.bmvbhash( data, bits ); | |
} else { | |
throw new Error( 'Bad hashing method.' ); | |
} | |
return hash; | |
} | |
} |
body { | |
color: rgba( 0, 0, 0, 0.87 ); | |
font-family: 'Roboto', sans-serif; | |
margin: 0; | |
overflow: hidden; | |
padding: 0; | |
} | |
.holder { | |
align-items: center; | |
display: flex; | |
flex-direction: column; | |
height: 100%; | |
justify-content: center; | |
} | |
.full { | |
position: absolute; | |
visibility: hidden; | |
} | |
.render { | |
box-shadow: 0 2px 5px rgba( 0, 0, 0, 0.26 ); | |
display: none; | |
} |
class Hashing { | |
constructor() { | |
this.help = document.querySelector( '.help' ); | |
// Drag and drop | |
this.holder = document.querySelector( '.holder' ); | |
this.holder.addEventListener( 'dragover', evt => this.doDragOver( evt ) ); | |
this.holder.addEventListener( 'drop', evt => this.doDragDrop( evt ) ); | |
// Image element | |
// Used for measuring original image | |
this.full = document.querySelector( '.full' ); | |
this.full.addEventListener( 'load', evt => this.doImageLoad( evt ) ); | |
// Canvas element | |
// Surface to display the image | |
this.render = document.querySelector( '.render' ); | |
// Multiple handlers for XHR | |
// Need references for removal | |
this.doPreflightLoad = this.doPreflightLoad.bind( this ); | |
this.doUpload = this.doUpload.bind( this ); | |
// Hashing routines | |
// File reference | |
this.blockhash = new Blockhash(); | |
this.file = null; | |
this.xhr = new XMLHttpRequest(); | |
} | |
// Load image element | |
// Triggers sizing and display | |
show( path ) { | |
this.full.src = path; | |
} | |
// File dropped | |
// Perform hash | |
doDragDrop( evt ) { | |
evt.preventDefault(); | |
console.log( 'File drop.' ); | |
this.file = evt.dataTransfer.files[0]; | |
this.blockhash.blockhash( this.file, 8, 2, hash => this.doHash( hash ) ); | |
} | |
// Enable drag and drop | |
doDragOver( evt ) { | |
evt.preventDefault(); | |
evt.dataTransfer.dropEffect = 'copy'; | |
} | |
// Image hashed | |
// See if it exists on the server | |
doHash( hash ) { | |
console.log( 'Hash: ' + hash ); | |
this.xhr.addEventListener( 'load', this.doPreflightLoad ); | |
this.xhr.open( 'POST', '/api/image/preflight', true ); | |
this.xhr.setRequestHeader( 'Content-Type', 'application/json' ); | |
this.xhr.send( JSON.stringify( { | |
key: Hashing.API_KEY, | |
hash: hash | |
} ) ); | |
} | |
// Image element loaded | |
// Size and display on canvas | |
doImageLoad( evt ) { | |
// Hide instructions | |
this.help.style.display = 'none'; | |
// Landscape orientation | |
if( this.full.width > this.full.height ) { | |
this.render.width = Math.round( window.innerWidth * 0.75 ); | |
this.render.height = Math.round( this.render.width / ( this.full.width / this.full.height ) ); | |
} else { | |
// Portrait orientation | |
this.render.height = Math.round( window.innerHeight * 0.75 ); | |
this.render.width = Math.round( this.render.height * ( this.full.width / this.full.height ) ); | |
} | |
// Show canvas where image will go | |
this.render.style.display = 'block'; | |
// Put image into canvas | |
let context = this.render.getContext( '2d' ); | |
context.drawImage( this.full, 0, 0, this.render.width, this.render.height ); | |
} | |
// File check completed | |
// Populate if exists | |
// Upload file if it does not | |
doPreflightLoad( evt ) { | |
var data = JSON.parse( this.xhr.responseText ); | |
console.log( data ); | |
// Clean up | |
this.xhr.removeEventListener( 'load', this.doPreflightLoad ); | |
// Exists | |
// Populate image element | |
if( data.exists ) { | |
console.log( 'Found.' ); | |
this.show( '/api/image/original/' + data.hash + '.jpg?key=' + Hashing.API_KEY ); | |
} else { | |
// Does not exist | |
// Upload file to server | |
console.log( 'Not found.' ); | |
// Form | |
let data = new FormData(); | |
data.append( 'image', this.file ); | |
data.append( 'key', Hashing.API_KEY ); | |
// Upload | |
this.xhr.addEventListener( 'load', this.doUpload ); | |
this.xhr.open( 'POST', '/api/image/original', true ); | |
this.xhr.send( data ); | |
} | |
} | |
// Upload completed | |
doUpload( evt ) { | |
let data = JSON.parse( this.xhr.responseText ); | |
console.log( 'Upload complete.' ); | |
console.log( data ); | |
// Load image element | |
this.show( '/api/image/original/' + data.name + '.jpg?key=' + Hashing.API_KEY ); | |
// Clean up | |
this.xhr.removeEventListener( 'load', this.doUpload ); | |
} | |
} | |
Hashing.API_KEY = '159b191cdc1d7456e36af7a13fe764e5746989a8'; | |
let app = new Hashing(); |
var express = require( 'express' ); | |
var fs = require( 'fs' ); | |
var imghash = require( 'imghash' ); | |
var multer = require( 'multer' ); | |
var mv = require( 'mv' ); | |
var path = require( 'path' ); | |
var randomstring = require( 'randomstring' ); | |
var request = require( 'request' ); | |
// Router | |
var router = express.Router(); | |
// Upload storage options | |
// Unique name with extension | |
var storage = multer.diskStorage( { | |
destination: 'uploads', | |
filename: function( req, file, cb ) { | |
cb( null, randomstring.generate() + '.jpg' ); | |
} | |
} ); | |
// Upload handler | |
var upload = multer( { | |
storage: storage | |
} ); | |
// Get specific image | |
router.get( '/original/:path', function( req, res ) { | |
res.sendFile( path.resolve( __dirname + '/../originals/' + req.query.key + '/' + req.params.path ) ); | |
} ); | |
// Image upload | |
router.post( '/original', upload.single( 'image' ), function( req, res ) { | |
var destination = path.resolve( __dirname + '/../originals/' + req.body.key ); | |
// Does user have directory | |
fs.access( destination, function( err ) { | |
// Make directory if neeeded | |
if( err ) { | |
fs.mkdirSync( destination ) | |
} | |
// Perceptual image hash | |
imghash.hash( req.file.path ).then( ( hash ) => { | |
// Does file already exist | |
fs.access( path.resolve( destination + '/' + hash + '.jpg' ), function( err ) { | |
if( err ) { | |
// File not previously uploaded | |
// Move upload to user directory | |
// Use hash as destination name | |
mv( req.file.path, destination + '/' + hash + '.jpg', function( err ) { | |
// Tell user where to find file | |
res.json( { | |
name: hash | |
} ); | |
} ); | |
} else { | |
// File already uploaded | |
// Tell user where to find file | |
res.json( { | |
name: hash | |
} ); | |
} | |
} ); | |
} ); | |
} ); | |
} ); | |
// Check exists before uploading | |
router.post( '/preflight', function( req, res ) { | |
var destination = path.resolve( __dirname + '/../originals/' + req.body.key ); | |
fs.access( path.resolve( destination + '/' + req.body.hash + '.jpg' ), function( err ) { | |
var found = true; | |
if( err ) { | |
found = false; | |
} | |
res.json( { | |
exists: found, | |
hash: req.body.hash | |
} ); | |
} ); | |
} ); | |
// Test | |
router.get( '/test', function( req, res ) { | |
res.json( {facial: 'Image management.'} ); | |
} ); | |
// Export | |
module.exports = router; |
<html> | |
<head> | |
<title>Hashing</title> | |
<!-- Google Fonts --> | |
<!-- Roboto --> | |
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"> | |
<!-- Styles --> | |
<link href="hashing.css" rel="stylesheet"> | |
</head> | |
<body> | |
<!-- Center content --> | |
<div class="holder"> | |
<!-- Instructions --> | |
<p class="help">Drag and drop file here.</p> | |
<!-- Display image --> | |
<canvas class="render"></canvas> | |
<!-- Measure image --> | |
<img class="full"> | |
</div> | |
<!-- Blockhash --> | |
<!-- http://blockhash.io --> | |
<script src="blockhash.js"></script> | |
<!-- Application --> | |
<script src="hashing.js"></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment