Add basic barcode scanner
This commit is contained in:
parent
1a611fbea1
commit
26c4406401
@ -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'
|
||||
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<Grid item xs={2}>
|
||||
<IconButton color="primary">
|
||||
<CropFreeIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default BarcodeScanner;
|
312
src/containers/BarcodeScanner/index.js
Normal file
312
src/containers/BarcodeScanner/index.js
Normal file
@ -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 (
|
||||
<React.Fragment>
|
||||
<div id="barcode">
|
||||
<video id="barcodevideo" autoPlay />
|
||||
<canvas id="barcodecanvasg" />
|
||||
</div>
|
||||
<canvas id="barcodecanvas" />
|
||||
{decodecBarcode === "5449000136350"}
|
||||
{decodecBarcode}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default BarcodeScanner;
|
22
src/containers/BarcodeScanner/styles.css
Normal file
22
src/containers/BarcodeScanner/styles.css
Normal file
@ -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;
|
||||
}
|
@ -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()
|
||||
})
|
||||
|
@ -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 (
|
||||
// <Container component="main" maxWidth="xs">
|
||||
// <div className={classes.paper}>
|
||||
// <Typography component="h1" variant="h5">
|
||||
// Login to Account
|
||||
// </Typography>
|
||||
// <Formik
|
||||
// initialValues={formInitialValues}
|
||||
// validationSchema={validationSchema}
|
||||
// onSubmit={handleSubmit}
|
||||
// >
|
||||
// {({ isSubmitting }) => (
|
||||
// <Form id={formId}>
|
||||
// <InputField
|
||||
// type="text"
|
||||
// label={email.label}
|
||||
// name={email.name}
|
||||
// variant="outlined"
|
||||
// fullWidth
|
||||
// className={classes.input}
|
||||
// />
|
||||
// <InputField
|
||||
// type="password"
|
||||
// label={password.label}
|
||||
// name={password.name}
|
||||
// variant="outlined"
|
||||
// fullWidth
|
||||
// className={classes.input}
|
||||
// />
|
||||
// <FormControlLabel
|
||||
// control={<Checkbox value="remember" color="primary" />}
|
||||
// label="Remember me"
|
||||
// />
|
||||
// <Button
|
||||
// type="submit"
|
||||
// fullWidth
|
||||
// variant="contained"
|
||||
// color="primary"
|
||||
// className={classes.submit}
|
||||
// disabled={isSubmitting}
|
||||
// >
|
||||
// Sign Up
|
||||
// </Button>
|
||||
// </Form>
|
||||
// )}
|
||||
// </Formik>
|
||||
// <Grid container alignItems="flex-end">
|
||||
// <Grid item>
|
||||
// <Link href={routes.register.path} color="secondary" variant="body2">
|
||||
// Don't have an account? Sign Up
|
||||
// </Link>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </div>
|
||||
// </Container>
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// 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 (
|
||||
<Container component="main" maxWidth="xs">
|
||||
<div className={classes.paper}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Login to Account
|
||||
</Typography>
|
||||
<Formik
|
||||
initialValues={formInitialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form id={formId}>
|
||||
<InputField
|
||||
type="text"
|
||||
label={email.label}
|
||||
name={email.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
/>
|
||||
<InputField
|
||||
type="password"
|
||||
label={password.label}
|
||||
name={password.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox value="remember" color="primary" />}
|
||||
label="Remember me"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Grid container alignItems="flex-end">
|
||||
<Grid item>
|
||||
<Link href={routes.register.path} color="secondary" variant="body2">
|
||||
Don't have an account? Sign Up
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default () => (
|
||||
<BarcodeScanner />
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user