From 26c4406401c6232f4bfda9ec5376a9e3c4e0c8cc Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 17 Jan 2021 20:41:51 +0100 Subject: [PATCH] Add basic barcode scanner --- .../AddProductToMealDialog/index.js | 2 +- src/components/BarcodeScanner/index.js | 17 - src/containers/BarcodeScanner/index.js | 312 ++++++++++++++++++ src/containers/BarcodeScanner/styles.css | 22 ++ src/pages/Home/index.js | 2 + src/pages/Login/index.js | 204 ++++++------ 6 files changed, 443 insertions(+), 116 deletions(-) delete mode 100644 src/components/BarcodeScanner/index.js create mode 100644 src/containers/BarcodeScanner/index.js create mode 100644 src/containers/BarcodeScanner/styles.css diff --git a/src/components/AddProductToMealDialog/index.js b/src/components/AddProductToMealDialog/index.js index 447241c..e7833e1 100644 --- a/src/components/AddProductToMealDialog/index.js +++ b/src/components/AddProductToMealDialog/index.js @@ -13,7 +13,7 @@ import { import { useDispatch } from 'react-redux' import {Add as AddIcon} from "@material-ui/icons"; import SearchInput from 'components/SearchInput' -import BarcodeScanner from 'components/BarcodeScanner' +import BarcodeScanner from 'containers/BarcodeScanner' import { createStructuredSelector } from 'reselect' import { useSelector } from 'react-redux' import { makeSelectProducts } from 'pages/Home/selectors' diff --git a/src/components/BarcodeScanner/index.js b/src/components/BarcodeScanner/index.js deleted file mode 100644 index 00d7a72..0000000 --- a/src/components/BarcodeScanner/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import {IconButton, Grid} from '@material-ui/core' -import {CropFree as CropFreeIcon} from '@material-ui/icons' - -const BarcodeScanner = () => { - return ( - - - - - - - - ); -} - -export default BarcodeScanner; diff --git a/src/containers/BarcodeScanner/index.js b/src/containers/BarcodeScanner/index.js new file mode 100644 index 0000000..da9d8b4 --- /dev/null +++ b/src/containers/BarcodeScanner/index.js @@ -0,0 +1,312 @@ +import React, { useState, useEffect } from 'react'; +import {IconButton, Grid} from '@material-ui/core' +import {CropFree as CropFreeIcon} from '@material-ui/icons' +import './styles.css' + +const BarcodeScanner = () => { + const [decodecBarcode, setDecodedBarcode] = useState(''); + + const dimensions = { + height: 0, + width: 0, + start: 0, + end: 0 + } + + const elements = { + video: null, + canvas: null, + ctx: null, + canvasg: null, + ctxg: null + } + + const upc = { + '0': [3, 2, 1, 1], + '1': [2, 2, 2, 1], + '2': [2, 1, 2, 2], + '3': [1, 4, 1, 1], + '4': [1, 1, 3, 2], + '5': [1, 2, 3, 1], + '6': [1, 1, 1, 4], + '7': [1, 3, 1, 2], + '8': [1, 2, 1, 3], + '9': [3, 1, 1, 2] + }; + + const check = { + 'oooooo': '0', + 'ooeoee': '1', + 'ooeeoe': '2', + 'ooeeeo': '3', + 'oeooee': '4', + 'oeeooe': '5', + 'oeeeoo': '6', + 'oeoeoe': '7', + 'oeoeeo': '8', + 'oeeoeo': '9' + } + + const config = { + strokeColor: '#f00', + start: 0.1, + end: 0.9, + threshold: 160, + quality: 0.45, + delay: 100, + video: '#barcodevideo', + canvas: '#barcodecanvas', + canvasg: '#barcodecanvasg' + } + + const init = () => { + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; + + elements.video = document.querySelector(config.video); + elements.canvas = document.querySelector(config.canvas); + elements.ctx = elements.canvas.getContext('2d'); + elements.canvasg = document.querySelector(config.canvasg); + elements.ctxg = elements.canvasg.getContext('2d'); + + if (navigator.getUserMedia) { + navigator.getUserMedia({audio: false, video: true}, function(stream) { + elements.video.srcObject = stream; + }, function(error) { + console.log(error); + }); + } + + elements.video.addEventListener('canplay', () => { + + dimensions.height = elements.video.videoHeight; + dimensions.width = elements.video.videoWidth; + + dimensions.start = dimensions.width * config.start; + dimensions.end = dimensions.width * config.end; + + elements.canvas.width = dimensions.width; + elements.canvas.height = dimensions.height; + elements.canvasg.width = dimensions.width; + elements.canvasg.height = dimensions.height; + + drawLine(elements.ctxg); + + setInterval(() => { + const snapshot = takeSnapshot(elements.ctx, elements.video) + processSnapshot(snapshot) + }, config.delay); + }, false); + } + + const takeSnapshot = (canvasElement, videoElement) => { + canvasElement.drawImage(videoElement, 0, 0, dimensions.width, dimensions.height); + return canvasElement.getImageData(dimensions.start, dimensions.height * 0.5, dimensions.end - dimensions.start, 1).data; + } + + const processSnapshot = (snapshot) => { + + const bars = []; + + const pixels = []; + let pixelBars = []; + + // convert to grayscale + for (let i = 0; i < snapshot.length; i += 4) { + pixels.push(Math.round(snapshot[i] * 0.2126 + snapshot[i + 1] * 0.7152 + snapshot[ i + 2] * 0.0722)); + } + + // normalize and convert to binary + const minPixel = Math.min(...pixels); + const maxPixel = Math.max(...pixels); + + const binary = pixels.reduce((arr, val) => { + const binaryValue = Math.round((val - minPixel) / (maxPixel - minPixel) * 255) > config.threshold ? 1 : 0 + return [...arr, binaryValue] + }, []); + + // determine bar widths + let current = binary[0]; + let count = 0; + + for (let i = 0; i < binary.length; i++) { + if (binary[i] === current) { + count++; + } else { + pixelBars.push(count); + count = 1; + current = binary[i] + } + } + pixelBars.push(count); + + // quality check + if (pixelBars.length < (3 + 24 + 5 + 24 + 3 + 1)) { + return; + } + + // find starting sequence + let startIndex = 0; + const minFactor = 0.5; + const maxFactor = 1.5; + + for (let i = 3; i < pixelBars.length; i++) { + const refLength = (pixelBars[i] + pixelBars[i-1] + pixelBars[i-2]) / 3; + if ( + (pixelBars[i] > (minFactor * refLength) || pixelBars[i] < (maxFactor * refLength)) + && (pixelBars[i-1] > (minFactor * refLength) || pixelBars[i-1] < (maxFactor * refLength)) + && (pixelBars[i-2] > (minFactor * refLength) || pixelBars[i-2] < (maxFactor * refLength)) + && (pixelBars[i-3] > 3 * refLength) + ) { + startIndex = i - 2; + break; + } + } + + console.log("startIndex: " + startIndex ); + + // return if no starting sequence found + if (startIndex === 0) { + return; + } + + // discard leading and trailing patterns + + pixelBars = pixelBars.slice(startIndex, startIndex + 3 + 24 + 5 + 24 + 3); + + console.log("pixelBars: " + pixelBars ); + + // calculate relative widths + const ref = (pixelBars[0] + pixelBars[1] + pixelBars[2]) / 3; + + for (let i = 0; i < pixelBars.length; i++) { + bars.push(Math.round(pixelBars[i] / ref * 100) / 100); + } + + // analyze pattern + analyzePattern(bars); + } + + const analyzePattern = (bars) => { + + console.clear(); + + console.log("analyzing"); + + // determine parity first digit and reverse sequence if necessary + + const first = normalize(bars.slice(3, 3 + 4), 7); + if (!isOdd(Math.round(first[1] + first[3]))) { + bars = bars.reverse(); + } + + // split into digits + + const digits = [ + normalize(bars.slice(3, 3 + 4), 7), + normalize(bars.slice(7, 7 + 4), 7), + normalize(bars.slice(11, 11 + 4), 7), + normalize(bars.slice(15, 15 + 4), 7), + normalize(bars.slice(19, 19 + 4), 7), + normalize(bars.slice(23, 23 + 4), 7), + normalize(bars.slice(32, 32 + 4), 7), + normalize(bars.slice(36, 36 + 4), 7), + normalize(bars.slice(40, 40 + 4), 7), + normalize(bars.slice(44, 44 + 4), 7), + normalize(bars.slice(48, 48 + 4), 7), + normalize(bars.slice(52, 52 + 4), 7) + ] + + console.log("digits: " + digits); + + // determine parity and reverse if necessary + const parities = []; + + for (let i = 0; i < 6; i++) { + if (parity(digits[i])) { + parities.push('o'); + } else { + parities.push('e'); + digits[i] = digits[i].reverse(); + } + } + + // identify digits + const result = []; + let quality = 0; + + for (let i = 0; i < digits.length; i++) { + + let distance = 9; + let bestKey = ''; + + for (let key in upc) { + if (maxDistance(digits[i], upc[key]) < distance) { + distance = maxDistance(digits[i], upc[key]); + bestKey = key; + } + } + + result.push(bestKey); + if (distance > quality) { + quality = distance; + } + + } + + console.log("result: " + result); + + // check digit + const checkDigit = check[parities.join('')]; + + // output + console.log("quality: " + quality); + + if(quality < config.quality) { + setDecodedBarcode(checkDigit + result.join('')) + } + + } + + const normalize = (input, total) => { + const sum = input.reduce((acc, val) => acc + val, 0); + return input.reduce((acc, val) => [...acc, val / sum * total], []); + } + + const isOdd = (num) => num % 2; + + const maxDistance = (a, b) => + a.reduce((max, value, index) => { + const current = Math.abs(value - b[index]) + return current > max ? current : max + }, 0) + + const parity = (digit) => isOdd(Math.round(digit[1] + digit[3])) + + const drawLine = (canvasContext) => { + canvasContext.strokeStyle = config.strokeColor; + canvasContext.lineWidth = 3; + canvasContext.beginPath(); + canvasContext.moveTo(dimensions.start, dimensions.height * 0.5); + canvasContext.lineTo(dimensions.end, dimensions.height * 0.5); + canvasContext.stroke(); + } + + useEffect(() => { + init() + }, []) + + return ( + +
+
+ + {decodecBarcode === "5449000136350"} + {decodecBarcode} +
+ ); +} + + +export default BarcodeScanner; diff --git a/src/containers/BarcodeScanner/styles.css b/src/containers/BarcodeScanner/styles.css new file mode 100644 index 0000000..cbf60f7 --- /dev/null +++ b/src/containers/BarcodeScanner/styles.css @@ -0,0 +1,22 @@ +#barcodevideo, #barcodecanvas, #barcodecanvasg { + height: 400px; +} + +#barcodecanvasg { + position: absolute; + top: 0px; + left: 0px; +} + +#result { + font-family: verdana; + font-size: 1.5em; +} + +#barcode { + position: relative; +} + +#barcodecanvas { + display: none; +} diff --git a/src/pages/Home/index.js b/src/pages/Home/index.js index 0362b17..e277a05 100644 --- a/src/pages/Home/index.js +++ b/src/pages/Home/index.js @@ -11,6 +11,8 @@ import { getMealsAction } from './actions' import MealCard from "components/MealCard"; import { createStructuredSelector } from 'reselect' +import BarcodeScanner from 'containers/BarcodeScanner' + const stateSelector = createStructuredSelector({ meals: makeSelectMeals() }) diff --git a/src/pages/Login/index.js b/src/pages/Login/index.js index 1d45425..a332150 100644 --- a/src/pages/Login/index.js +++ b/src/pages/Login/index.js @@ -1,100 +1,108 @@ -import React from 'react'; -import { useInjectReducer, useInjectSaga } from 'redux-injectors'; -import { useDispatch } from 'react-redux'; -import { - Container, - Grid, - Button, - Typography, - FormControlLabel, - Checkbox, - Link, -} from '@material-ui/core' -import {routes} from "utils"; -import InputField from 'components/InputField' -import { Formik, Form } from 'formik'; -import { loginAction } from './actions' -import useStyles from './styles' -import validationSchema from './FormModel/validationSchema' -import formInitialValues from './FormModel/formInitialValues' -import loginFormModel from './FormModel/loginFormModel' -import saga from "./saga"; +// import React from 'react'; +// import { useInjectReducer, useInjectSaga } from 'redux-injectors'; +// import { useDispatch } from 'react-redux'; +// import { +// Container, +// Grid, +// Button, +// Typography, +// FormControlLabel, +// Checkbox, +// Link, +// } from '@material-ui/core' +// import {routes} from "utils"; +// import InputField from 'components/InputField' +// import { Formik, Form } from 'formik'; +// import { loginAction } from './actions' +// import useStyles from './styles' +// import validationSchema from './FormModel/validationSchema' +// import formInitialValues from './FormModel/formInitialValues' +// import loginFormModel from './FormModel/loginFormModel' +// import saga from "./saga"; +// +// const { +// formId, +// formField: { +// email, +// password +// } +// } = loginFormModel; +// +// const key = 'loginPage' +// +// const Login = () => { +// useInjectSaga({ key, saga }); +// const classes = useStyles() +// const dispatch = useDispatch(); +// +// const handleSubmit = (values, actions) => { +// dispatch(loginAction(values)) +// } +// +// return ( +// +//
+// +// Login to Account +// +// +// {({ isSubmitting }) => ( +//
+// +// +// } +// label="Remember me" +// /> +// +// +// )} +//
+// +// +// +// Don't have an account? Sign Up +// +// +// +//
+//
+// ); +// }; +// +// export default Login; -const { - formId, - formField: { - email, - password - } -} = loginFormModel; +import BarcodeScanner from "../../containers/BarcodeScanner"; +import {Container} from "@material-ui/core"; +import React from "react"; -const key = 'loginPage' - -const Login = () => { - useInjectSaga({ key, saga }); - const classes = useStyles() - const dispatch = useDispatch(); - - const handleSubmit = (values, actions) => { - dispatch(loginAction(values)) - } - - return ( - -
- - Login to Account - - - {({ isSubmitting }) => ( -
- - - } - label="Remember me" - /> - - - )} -
- - - - Don't have an account? Sign Up - - - -
-
- ); -}; - -export default Login; +export default () => ( + +)