Add modal for barcode scanner

This commit is contained in:
= 2021-01-19 18:39:12 +01:00
parent 52dfb0522f
commit 8191799760
5 changed files with 471 additions and 407 deletions

View File

@ -0,0 +1,322 @@
import React, {useEffect, useState} from 'react';
import {Paper, IconButton} from "@material-ui/core";
import { Close as CloseIcon } from '@material-ui/icons'
import useStyles from "./styles";
const Barcode = ({ isScannerOpen, handleClose }) => {
const [decodecBarcode, setDecodedBarcode] = useState('');
const classes = useStyles()
const onClose = () => {
stop()
handleClose()
}
let localMediaStream = null;
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.9,
delay: 100,
video: '#barcodevideo',
canvas: '#barcodecanvas',
canvasg: '#barcodecanvasg'
}
const play = () => {
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);
}
const stop = () => {
elements.video.pause();
localMediaStream.getTracks()[0].stop();
}
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}, (stream) => {
elements.video.srcObject = stream;
localMediaStream = stream;
}, (error) => {
console.log(error);
});
}
elements.video.addEventListener('canplay', play, 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 minPixelValue = Math.min(...pixels);
const maxPixelValue = Math.max(...pixels);
const binary = pixels.reduce((arr, val) => {
const binaryValue = Math.round((val - minPixelValue) / (maxPixelValue - minPixelValue) * 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
const minFactor = 0.5;
const maxFactor = 1.5;
let startIndex = 0;
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('')];
console.log("quality: " + quality);
// output
if(quality < config.quality) {
const barcode = checkDigit + result.join('')
setDecodedBarcode(barcode)
onClose()
}
}
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 () => elements.video.removeEventListener('canplay', play, false)
}, [])
return (
<Paper className={classes.container}>
<div className={classes.close}>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</div>
<div className={classes.inner}>
<video className={classes.video} id="barcodevideo" autoPlay />
<canvas className={classes.canvasg} id="barcodecanvasg" />
</div>
<canvas className={classes.canvas} id="barcodecanvas" />
</Paper>
);
}
export default Barcode;

View File

@ -1,293 +1,28 @@
import React, { useState, useEffect } from 'react'; import React, {useState} from 'react';
import {IconButton, Grid} from '@material-ui/core' import {IconButton} from "@material-ui/core";
import {CropFree as CropFreeIcon} from '@material-ui/icons' import {CropFree as CropFreeIcon} from '@material-ui/icons'
import './styles.css' import Barcode from './Barcode'
const BarcodeScanner = () => { const BarcodeScanner = () => {
const [decodecBarcode, setDecodedBarcode] = useState(''); const [isScannerOpen, setIsScannerOpen] = useState(false)
const dimensions = { const handleOpen = () => {
height: 0, setIsScannerOpen(true)
width: 0,
start: 0,
end: 0
} }
const elements = { const handleClose = () => {
video: null, setIsScannerOpen(false)
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 minPixelValue = Math.min(...pixels);
const maxPixelValue = Math.max(...pixels);
const binary = pixels.reduce((arr, val) => {
const binaryValue = Math.round((val - minPixelValue) / (maxPixelValue - minPixelValue) * 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
const minFactor = 0.5;
const maxFactor = 1.5;
let startIndex = 0;
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;
}
}
// 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);
// 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) => {
// 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)
]
// 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;
}
}
// check digit
const checkDigit = check[parities.join('')];
// output
if(quality < config.quality) {
const barcode = checkDigit + result.join('')
setDecodedBarcode(barcode)
}
}
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 ( return (
<React.Fragment> <React.Fragment>
<div id="barcode"> <IconButton onClick={handleOpen}>
<video id="barcodevideo" autoPlay /> <CropFreeIcon />
<canvas id="barcodecanvasg" /> </IconButton>
</div> {isScannerOpen ? (
<canvas id="barcodecanvas" /> <Barcode isScannerOpen={isScannerOpen} handleClose={handleClose} />
{decodecBarcode === "5449000136350"} ) : null}
{decodecBarcode}
</React.Fragment> </React.Fragment>
); );
} }
export default BarcodeScanner; export default BarcodeScanner;

View File

@ -1,22 +0,0 @@
#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;
}

View File

@ -0,0 +1,37 @@
import {makeStyles} from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
container: {
zIndex: theme.zIndex.modal,
width: `100%`,
height: `100%`,
position: `absolute`,
left: 0,
top: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inner: {
position: `relative`,
},
close: {
position: `absolute`,
top: 20,
right: 20,
},
video: {
height: 400,
},
canvas: {
display: "none",
},
canvasg: {
position: "absolute",
top: 0,
left: 0,
height: 400,
},
}))
export default useStyles

View File

@ -1,108 +1,100 @@
// import React from 'react'; import React from 'react';
// import { useInjectReducer, useInjectSaga } from 'redux-injectors'; import { useInjectReducer, useInjectSaga } from 'redux-injectors';
// import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
// import { import {
// Container, Container,
// Grid, Grid,
// Button, Button,
// Typography, Typography,
// FormControlLabel, FormControlLabel,
// Checkbox, Checkbox,
// Link, Link,
// } from '@material-ui/core' } from '@material-ui/core'
// import {routes} from "utils"; import {routes} from "utils";
// import InputField from 'components/InputField' import InputField from 'components/InputField'
// import { Formik, Form } from 'formik'; import { Formik, Form } from 'formik';
// import { loginAction } from './actions' import { loginAction } from './actions'
// import useStyles from './styles' import useStyles from './styles'
// import validationSchema from './FormModel/validationSchema' import validationSchema from './FormModel/validationSchema'
// import formInitialValues from './FormModel/formInitialValues' import formInitialValues from './FormModel/formInitialValues'
// import loginFormModel from './FormModel/loginFormModel' import loginFormModel from './FormModel/loginFormModel'
// import saga from "./saga"; 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;
import BarcodeScanner from "../../containers/BarcodeScanner"; const {
import {Container} from "@material-ui/core"; formId,
import React from "react"; formField: {
email,
password
}
} = loginFormModel;
export default () => ( const key = 'loginPage'
<BarcodeScanner />
) 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;