Added QR Scan with device's camera

- Design changes regarding file selection / camera
This commit is contained in:
Hauke Tönjes 2021-06-30 03:13:01 +02:00
parent 98ca5b59bf
commit 57e08bacaf
No known key found for this signature in database
GPG Key ID: 0BF2BC96C9FAAE9E
5 changed files with 376 additions and 195 deletions

View File

@ -1,9 +1,10 @@
export default Card
function Card({ heading, content, step }) {
function Card({heading, content, step}) {
return (
<div className="rounded-md p-6 bg-white dark:bg-gray-800 space-y-4">
{step &&
{
step &&
<div className="flex flex-row items-center">
<div className="rounded-full p-4 bg-green-600 h-5 w-5 flex items-center justify-center">
<p className="text-white text-lg font-bold">
@ -13,7 +14,8 @@ function Card({ heading, content, step }) {
<div className="ml-3 font-bold text-xl">
{heading}
</div>
</div>}
</div>
}
<div className="text-lg">
{content}
</div>

View File

@ -1,11 +1,12 @@
import jsQR from "jsqr"
import {saveAs} from 'file-saver'
import {BrowserQRCodeReader} from '@zxing/browser'
import React, {useEffect, useRef, useState} from "react"
import {decodeData} from "../src/decode"
import { processJpeg, processPng, processPdf } from "../src/process"
import {processPdf, processPng} from "../src/process"
import {createPass} from "../src/pass"
import Card from "../components/Card"
import Alert from "../components/Alert"
import jsQR from "jsqr";
export default Form
@ -25,7 +26,7 @@ function Form() {
})
}
const error = function(heading, message) {
const error = function (heading, message) {
const alert = document.getElementById('alert')
alert.setAttribute('style', null)
@ -35,23 +36,27 @@ function Form() {
document.getElementById('spin').style.display = 'none'
}
const processFile = async function() {
const processFile = async function () {
console.log(qrCode)
console.log(file)
if (!qrCode && !file) {
error("Error", "Please capture a QR Code or select a file to scan");
return;
}
document.getElementById('spin').style.display = 'block'
const file = document.getElementById('file').files[0]
const fileBuffer = await readFileAsync(file)
let rawData;
if (file) {
let imageData
const fileBuffer = await readFileAsync(file)
switch (file.type) {
case 'application/pdf':
console.log('pdf')
imageData = await processPdf(fileBuffer)
break
case 'image/jpeg':
console.log('jpeg')
imageData = await processJpeg(fileBuffer)
break
case 'image/png':
console.log('png')
imageData = await processPng(fileBuffer)
@ -65,31 +70,37 @@ function Form() {
inversionAttempts: 'dontInvert',
})
if (code) {
const rawData = code.data
rawData = code.data;
} else {
rawData = qrCode.getText()
}
if (rawData) {
let decoded
try {
decoded = decodeData(rawData)
} catch (error) {
error('Invalid QR code found', 'Make sure that you picked the correct PDF')
} catch (e) {
error('Invalid QR code found', 'Try another method to select your certificate')
return;
}
return {decoded: decoded, raw: rawData}
} else {
error('No QR code found', 'Try scanning the PDF again')
error('No QR code found', 'Try another method to select your certificate')
}
}
const addToWallet = async function(event) {
const addToWallet = async function (event) {
event.preventDefault()
let result
try {
result = await processFile()
} catch {
error('Error:', 'Could not extract QR code data from PDF')
} catch (e) {
error('Error:', 'Could not extract QR code data from certificate')
}
if (typeof result === 'undefined') {
@ -120,30 +131,123 @@ function Form() {
}
}
const [isCameraOpen, setIsCameraOpen] = useState(false);
const [globalControls, setGlobalControls] = useState(undefined);
const [qrCode, setQrCode] = useState(undefined);
const [file, setFile] = useState(undefined);
const inputFile = useRef(undefined)
useEffect(() => {
if (inputFile && inputFile.current) {
inputFile.current.addEventListener('input', () => {
let selectedFile = inputFile.current.files[0];
if (selectedFile !== undefined) {
setQrCode(undefined);
setFile(selectedFile);
}
});
}
}, [inputFile])
async function showFileDialog() {
inputFile.current.click();
}
async function hideCameraView() {
if (globalControls !== undefined) {
globalControls.stop();
}
setIsCameraOpen(false);
}
async function showCameraView() {
const codeReader = new BrowserQRCodeReader();
// Needs to be called before any camera can be accessed
await BrowserQRCodeReader.listVideoInputDevices();
// Get preview Element to show camera stream
const previewElem = document.querySelector('#cameraPreview');
setGlobalControls(await codeReader.decodeFromVideoDevice(undefined, previewElem, (result, error, controls) => {
if (result !== undefined) {
setQrCode(result);
setFile(undefined);
controls.stop();
// Reset
setGlobalControls(undefined);
setIsCameraOpen(false);
}
}));
setIsCameraOpen(true);
}
return (
<div>
<form className="space-y-5" id="form" onSubmit={(e) => addToWallet(e)}>
<Card step={1} heading="Select Certificate" content={
<div className="space-y-5">
<p>
Please select the certificate screenshot or (scanned) PDF page, which you received from your doctor, pharmacy, vaccination centre or online. Note that taking a picture does not work on most devices yet.
Please select the certificate screenshot or (scanned) PDF page, which you received from your
doctor, pharmacy, vaccination centre or online. Note that taking a picture does not work on
most devices yet.
</p>
<input
className="w-full"
type="file"
id="file"
accept="application/pdf,image/jpeg,image/png"
required
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<button
type="button"
onClick={isCameraOpen ? hideCameraView : showCameraView}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
{isCameraOpen ? "Stop Camera" : "Start Camera"}
</button>
<button
type="button"
onClick={showFileDialog}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
Open File (PDF, PNG)
</button>
</div>
} />
<video id="cameraPreview" className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
<input type='file'
id='file'
accept="application/pdf,image/png"
ref={inputFile}
style={{display: 'none'}}
/>
{(qrCode || file) &&
<div className="flex items-center space-x-1">
<svg className="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"/>
</svg>
<span className="w-full">
{
qrCode && 'Found QR Code!'
}
{
file && file.name
}
</span>
</div>}
</div>
}/>
<Card step={2} heading="Pick a Color" content={
<div className="space-y-5">
<p>
Pick a background color for your pass.
</p>
<div className="relative inline-block w-full">
<select name="color" id="color" className="bg-gray-200 dark:bg-gray-900 focus:outline-none w-full h-10 pl-3 pr-6 text-base rounded-md appearance-none cursor-pointer">
<select name="color" id="color"
className="bg-gray-200 dark:bg-gray-900 focus:outline-none w-full h-10 pl-3 pr-6 text-base rounded-md appearance-none cursor-pointer">
<option value="white">white</option>
<option value="black">black</option>
<option value="grey">grey</option>
@ -154,39 +258,47 @@ function Form() {
<option value="teal">teal</option>
</select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg className="w-5 h-5 fill-current" viewBox="0 0 20 20"><path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" fillRule="evenodd"/></svg>
<svg className="w-5 h-5 fill-current" viewBox="0 0 20 20">
<path
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd" fillRule="evenodd"/>
</svg>
</div>
</div>
</div>
} />
}/>
<Card step={3} heading="Add to Wallet" content={
<div className="space-y-5">
<p>
Data privacy is of special importance when processing health-related data.
In order for you to make an informed decision, please read the <a href="/privacy">Privacy Policy</a>.
In order for you to make an informed decision, please read the <a href="/privacy">Privacy
Policy</a>.
</p>
<label htmlFor="privacy" className="flex flex-row space-x-4 items-center">
<input type="checkbox" id="privacy" value="privacy" required className="h-4 w-4" />
<input type="checkbox" id="privacy" value="privacy" required className="h-4 w-4"/>
<p>
I accept the <a href="/privacy" className="underline">Privacy Policy</a>
</p>
</label>
<div className="flex flex-row items-center justify-start">
<button id="download" type="download" className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
<button id="download" type="download"
className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
Add to Wallet
</button>
<div id="spin" style={{ "display": "none" }}>
<div id="spin" style={{"display": "none"}}>
<svg className="animate-spin h-5 w-5 ml-2" viewBox="0 0 24 24">
<circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
<circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor"
strokeWidth="4"/>
<path className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
</div>
</div>
</div>
} />
}/>
</form>
<Alert />
<canvas id="canvas" style={{ display: "none" }} />
<Alert/>
<canvas id="canvas" style={{display: "none"}}/>
</div>
)
}

72
package-lock.json generated
View File

@ -7,6 +7,7 @@
"": {
"version": "0.1.0",
"dependencies": {
"@zxing/browser": "^0.0.9",
"base45-js": "^1.0.1",
"cbor-js": "^0.1.0",
"do-not-zip": "^1.0.0",
@ -435,6 +436,38 @@
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
},
"node_modules/@zxing/browser": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.9.tgz",
"integrity": "sha512-yqYz/FHQXbUnfSK+oeDXUa4ezC/qdXkVpqEFAnxM7LqIWvNWEQyUpdaTr0X6MGtIcP0Smakj5D8J7/l276plBw==",
"optionalDependencies": {
"@zxing/text-encoding": "^0.9.0"
},
"peerDependencies": {
"@zxing/library": "^0.18.5"
}
},
"node_modules/@zxing/library": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.18.6.tgz",
"integrity": "sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==",
"peer": true,
"dependencies": {
"ts-custom-error": "^3.0.0"
},
"engines": {
"node": ">= 10.4.0"
},
"optionalDependencies": {
"@zxing/text-encoding": "~0.9.0"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"optional": true
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@ -4012,6 +4045,15 @@
"punycode": "^2.1.0"
}
},
"node_modules/ts-custom-error": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz",
"integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ts-pnp": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
@ -4730,6 +4772,30 @@
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
},
"@zxing/browser": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.9.tgz",
"integrity": "sha512-yqYz/FHQXbUnfSK+oeDXUa4ezC/qdXkVpqEFAnxM7LqIWvNWEQyUpdaTr0X6MGtIcP0Smakj5D8J7/l276plBw==",
"requires": {
"@zxing/text-encoding": "^0.9.0"
}
},
"@zxing/library": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.18.6.tgz",
"integrity": "sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==",
"peer": true,
"requires": {
"@zxing/text-encoding": "~0.9.0",
"ts-custom-error": "^3.0.0"
}
},
"@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"optional": true
},
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@ -7485,6 +7551,12 @@
"punycode": "^2.1.0"
}
},
"ts-custom-error": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz",
"integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==",
"peer": true
},
"ts-pnp": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",

View File

@ -9,6 +9,7 @@
"start": "next start"
},
"dependencies": {
"@zxing/browser": "^0.0.9",
"base45-js": "^1.0.1",
"cbor-js": "^0.1.0",
"do-not-zip": "^1.0.0",

View File

@ -1,4 +1,3 @@
import jpeg from 'jpeg-js'
import {PNG} from 'pngjs'
import * as PdfJS from 'pdfjs-dist'
@ -42,11 +41,6 @@ export async function processPdf(file) {
return ctx.getImageData(0, 0, canvas.width, canvas.height)
}
// Processes a JPEG File and returns it as ImageData
export async function processJpeg(file) {
return jpeg.decode(file)
}
// Processes a PNG File and returns it as ImageData
export async function processPng(file) {
return new Promise(async (resolve, reject) => {