summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2019-04-25 19:46:20 +0200
committerJules Laplace <julescarbon@gmail.com>2019-04-25 19:46:20 +0200
commitc9c72cdc3128fe272edeb6ec20959b2248f33877 (patch)
treebb04bf8a90ab11417b113675426ce58ca5595289 /client
parent4d5c3d59f32b80638d82373d33a476652520e260 (diff)
frontend demo
Diffstat (limited to 'client')
-rw-r--r--client/app.js102
-rw-r--r--client/index.js8
-rw-r--r--client/lib/upload.helpers.js156
-rw-r--r--client/lib/uploadImage.component.js46
-rw-r--r--client/util.js55
5 files changed, 367 insertions, 0 deletions
diff --git a/client/app.js b/client/app.js
new file mode 100644
index 0000000..96c426c
--- /dev/null
+++ b/client/app.js
@@ -0,0 +1,102 @@
+import React, { Component } from 'react'
+
+import UploadImage from './lib/uploadImage.component'
+import { post } from './util'
+
+const initialState = {
+ 'image': null,
+ 'res': null,
+ 'loading': false,
+}
+
+export default class PhashApp extends Component {
+ state = { ...initialState }
+
+ upload(blob) {
+ if (this.state.image) {
+ URL.revokeObjectURL(this.state.image)
+ }
+ const url = URL.createObjectURL(blob)
+ this.setState({ image: url, loading: true })
+
+ const fd = new FormData()
+ fd.append('q', blob)
+ post('/api/v1/match', fd)
+ .then(res => {
+ console.log(res)
+ this.setState({ res, loading: false })
+ })
+ .catch(err => {
+ console.log(err)
+ this.setState({ loading: false })
+ })
+ }
+
+ render() {
+ return (
+ <div className='app'>
+ <h1>Perceptual Hash Demo</h1>
+ {this.renderQuery()}
+ {this.renderResults()}
+ </div>
+ )
+ }
+
+ renderQuery() {
+ const { image } = this.state
+ const style = {}
+ if (image) {
+ style.backgroundImage = 'url(' + image + ')'
+ style.backgroundSize = 'cover'
+ style.opacity = 1
+ }
+ return (
+ <div className='query'>
+ <UploadImage onUpload={this.upload.bind(this)} />
+ {image && <div style={style} />}
+ </div>
+ )
+ }
+ renderResults() {
+ const { loading, res } = this.state
+ if (!res) {
+ return (
+ <div className='results'>
+ </div>
+ )
+ }
+ if (loading) {
+ return (
+ <div className='results'>
+ <i>Loading...</i>
+ </div>
+ )
+ }
+
+ const { success, error, match, closest_match } = res
+ if (!success) {
+ return (
+ <div className='results'>
+ <b>Error: {error}</b>
+ </div>
+ )
+ }
+
+ if (!match) {
+ return (
+ <div className='results'>
+ No match, image added to database
+ </div>
+ )
+ }
+ console.log(closest_match)
+ const { ext, phash, score, sha256 } = closest_match
+ return (
+ <div className='results'>
+ Closest match: {sha256}{'.'}{ext}<br />
+ Score: {score}<br />
+ Phash: {phash.toString(16)}
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/client/index.js b/client/index.js
new file mode 100644
index 0000000..1a71e55
--- /dev/null
+++ b/client/index.js
@@ -0,0 +1,8 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+
+import App from './app'
+
+ReactDOM.render(
+ <App />, document.querySelector('#container')
+)
diff --git a/client/lib/upload.helpers.js b/client/lib/upload.helpers.js
new file mode 100644
index 0000000..d0baab4
--- /dev/null
+++ b/client/lib/upload.helpers.js
@@ -0,0 +1,156 @@
+import ExifReader from 'exifreader'
+
+export const MAX_SIDE = 256
+
+function base64ToUint8Array(string, start, finish) {
+ start = start || 0
+ finish = finish || string.length
+ // atob that shit
+ const binary = atob(string)
+ const buffer = new Uint8Array(binary.length)
+ for (let i = start; i < finish; i++) {
+ buffer[i] = binary.charCodeAt(i)
+ }
+ return buffer
+}
+
+function getOrientation(uri) {
+ // Split off the base64 data
+ const base64String = uri.split(',')[1]
+ // Read off first 128KB, which is all we need to
+ // get the EXIF data
+ const arr = base64ToUint8Array(base64String, 0, 2 ** 17)
+ try {
+ const tags = ExifReader.load(arr.buffer)
+ // console.log(tags)
+ return tags.Orientation
+ } catch (err) {
+ return 1
+ }
+}
+
+function applyRotation(canvas, ctx, deg) {
+ const radians = deg * (Math.PI / 180)
+ if (deg === 90) {
+ ctx.translate(canvas.width, 0)
+ } else if (deg === 180) {
+ ctx.translate(canvas.width, canvas.height)
+ } else if (deg === 270) {
+ ctx.translate(0, canvas.height)
+ }
+ ctx.rotate(radians)
+}
+
+/**
+ * Mapping from EXIF orientation values to data
+ * regarding the rotation and mirroring necessary to
+ * render the canvas correctly
+ * Derived from:
+ * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
+ */
+const orientationToTransform = {
+ 1: { rotation: 0, mirror: false },
+ 2: { rotation: 0, mirror: true },
+ 3: { rotation: 180, mirror: false },
+ 4: { rotation: 180, mirror: true },
+ 5: { rotation: 90, mirror: true },
+ 6: { rotation: 90, mirror: false },
+ 7: { rotation: 270, mirror: true },
+ 8: { rotation: 270, mirror: false }
+}
+
+function applyOrientationCorrection(canvas, ctx, uri) {
+ const orientation = getOrientation(uri)
+ // Only apply transform if there is some non-normal orientation
+ if (orientation && orientation !== 1) {
+ const transform = orientationToTransform[orientation]
+ const { rotation } = transform
+ const flipAspect = rotation === 90 || rotation === 270
+ if (flipAspect) {
+ // Fancy schmancy swap algo
+ canvas.width = canvas.height + canvas.width
+ canvas.height = canvas.width - canvas.height
+ canvas.width -= canvas.height
+ }
+ if (rotation > 0) {
+ applyRotation(canvas, ctx, rotation)
+ }
+ }
+}
+
+function getScale(width, height, viewportWidth, viewportHeight, fillViewport) {
+ function fitHorizontal() {
+ return viewportWidth / width
+ }
+ function fitVertical() {
+ return viewportHeight / height
+ }
+ fillViewport = !!fillViewport
+ const landscape = (width / height) > (viewportWidth / viewportHeight)
+ if (landscape) {
+ if (fillViewport) {
+ return fitVertical()
+ }
+ if (width > viewportWidth) {
+ return fitHorizontal()
+ }
+ } else {
+ if (fillViewport) {
+ return fitHorizontal()
+ }
+ if (height > viewportHeight) {
+ return fitVertical()
+ }
+ }
+ return 1
+}
+
+export function renderToCanvas(img, options) {
+ if (!img) return null
+ options = options || {}
+
+ // Canvas max size for any side
+ const maxSide = MAX_SIDE
+ const canvas = document.createElement('canvas')
+ const ctx = canvas.getContext('2d')
+ const initialScale = options.scale || 1
+ // Scale to needed to constrain canvas to max size
+ let scale = getScale(img.naturalWidth * initialScale, img.naturalHeight * initialScale, maxSide, maxSide, true)
+ // console.log(scale)
+ // Still need to apply the user defined scale
+ scale *= initialScale
+ canvas.width = Math.round(img.naturalWidth * scale)
+ canvas.height = Math.round(img.naturalHeight * scale)
+ const { correctOrientation } = options
+ const jpeg = !!img.src.match(/data:image\/jpeg|\.jpeg$|\.jpg$/i)
+ const hasDataURI = !!img.src.match(/^data:/)
+
+ ctx.save()
+
+ // Can only correct orientation on JPEGs represented as dataURIs
+ // for the time being
+ if (correctOrientation && jpeg && hasDataURI) {
+ applyOrientationCorrection(canvas, ctx, img.src)
+ }
+ // Resize image if too large
+ if (scale !== 1) {
+ ctx.scale(scale, scale)
+ }
+
+ ctx.drawImage(img, 0, 0)
+ ctx.restore()
+
+ return canvas
+}
+
+export function renderThumbnail(img) {
+ const resized = renderToCanvas(img, { correctOrientation: true })
+ // const canvas = document.createElement('canvas') // document.querySelector('#user_photo_canvas')
+ // const ctx = canvas.getContext('2d')
+ // ctx.fillStyle = 'black'
+ // ctx.fillRect(0, 0, MAX_SIDE, MAX_SIDE)
+ // const xOffset = (MAX_SIDE - resized.width) / 2
+ // const yOffset = (MAX_SIDE - resized.height) / 2
+ // ctx.drawImage(resized, xOffset, yOffset, resized.width, resized.height)
+ return resized
+}
diff --git a/client/lib/uploadImage.component.js b/client/lib/uploadImage.component.js
new file mode 100644
index 0000000..690c0dc
--- /dev/null
+++ b/client/lib/uploadImage.component.js
@@ -0,0 +1,46 @@
+import React, { Component } from 'react'
+
+import { renderThumbnail } from './upload.helpers'
+
+export default class UploadImageComponent extends Component {
+ upload(e) {
+ const files = e.dataTransfer ? e.dataTransfer.files : e.target.files
+ let i
+ let file
+ for (i = 0; i < files.length; i++) {
+ file = files[i]
+ if (file && file.type.match('image.*')) break
+ }
+ if (!file) return
+ const fr = new FileReader()
+ fr.onload = fileReaderEvent => {
+ fr.onload = null
+ const img = new Image()
+ img.onload = () => {
+ img.onload = null
+ this.resizeAndUpload(img)
+ }
+ img.src = fileReaderEvent.target.result
+ }
+ fr.readAsDataURL(files[0])
+ }
+
+ resizeAndUpload(img) {
+ const canvas = renderThumbnail(img)
+ canvas.toBlob(blob => {
+ this.props.onUpload(blob)
+ }, 'image/jpeg', 80)
+ }
+
+ render() {
+ return (
+ <input
+ type="file"
+ name="img"
+ accept="image/*"
+ onChange={this.upload.bind(this)}
+ required
+ />
+ )
+ }
+}
diff --git a/client/util.js b/client/util.js
new file mode 100644
index 0000000..5b20c2a
--- /dev/null
+++ b/client/util.js
@@ -0,0 +1,55 @@
+/* Mobile check */
+
+export const isiPhone = !!((navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i)))
+export const isiPad = !!(navigator.userAgent.match(/iPad/i))
+export const isAndroid = !!(navigator.userAgent.match(/Android/i))
+export const isMobile = isiPhone || isiPad || isAndroid
+export const isDesktop = !isMobile
+export const isFirefox = typeof InstallTrigger !== 'undefined'
+
+const htmlClassList = document.body.parentNode.classList
+htmlClassList.add(isDesktop ? 'desktop' : 'mobile')
+if (isFirefox) {
+ htmlClassList.add('firefox')
+}
+
+/* AJAX */
+
+export const get = (uri, data) => {
+ let headers = {
+ Accept: 'application/json, application/xml, text/play, text/html, *.*',
+ }
+ let opt = {
+ method: 'GET',
+ body: data,
+ headers,
+ // credentials: 'include',
+ }
+ // console.log(headers)
+ // headers['X-CSRFToken'] = csrftoken
+ return fetch(uri, opt).then(res => res.json())
+}
+
+export const post = (uri, data) => {
+ let headers
+ if (data instanceof FormData) {
+ headers = {
+ Accept: 'application/json, application/xml, text/play, text/html, *.*',
+ }
+ } else {
+ headers = {
+ Accept: 'application/json, application/xml, text/play, text/html, *.*',
+ 'Content-Type': 'application/json; charset=utf-8',
+ }
+ data = JSON.stringify(data)
+ }
+ let opt = {
+ method: 'POST',
+ body: data,
+ headers,
+ // credentials: 'include',
+ }
+ // console.log(headers)
+ // headers['X-CSRFToken'] = csrftoken
+ return fetch(uri, opt).then(res => res.json())
+}