Skip to content

Instantly share code, notes, and snippets.

@korc
Last active May 20, 2020 14:39
Show Gist options
  • Save korc/96bb971c78ce274e464bfc0e4810a9ed to your computer and use it in GitHub Desktop.
Save korc/96bb971c78ce274e464bfc0e4810a9ed to your computer and use it in GitHub Desktop.
QR-Code tool
<html>
<head>
<title>QR code tool</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js"></script>
<style>
ul.foundCodes {
text-align: left;
}
.foundCodes li:hover {
background-color: antiquewhite;
}
.foundCodes li button {
float: right;
}
.foundCodes li button {
opacity: 0.3;
}
.foundCodes li:hover button {
opacity: 1;
}
.qrcode {
white-space: pre-wrap;
}
.qrcode.current {
font-weight: bold;
}
.qrcode.binary {
font-family: monospace;
}
.qrcode.binary::before {
content: "💻 ";
color: #ccc;
}
body {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
flex-wrap: wrap;
}
h1 {
border-bottom: 2px dashed gray;
margin-bottom: 33%;
}
.footer {
margin-top: 50%;
}
.app {
min-height: 90%;
}
@media print {
.noprint {
display: none;
}
}
.error {
color: red;
font-weight: bold;
}
.loadmsg {
white-space: pre-wrap;
border: 1px dashed gray;
margin: 1em;
padding: 0.5em;
}
</style>
</head>
<body>
<div id="app" class="app">
<h1>Just <tt>.js</tt> QR-Code tool</h1>
<div class="noprint">
<div v-if="!!loadingMessage" class="loadmsg">{{loadingMessage}}</div>
<span v-if="!scanRunning">
<button
@click="startDetection"
:disabled="isPasting || (scanSource==='file' && !scanFile)"
>
Scan code
</button>
using
<select
v-model="scanSource"
:disabled="isPasting"
@change="loadingMessage=''"
>
<option value="camera">camera</option>
<option value="display">screen</option>
<option value="file">file</option>
<option value="paste">image paste</option>
</select>
<div v-if="scanSource==='file'">
<input type="file" @change="e => (scanFile=e.target.files[0])" />
</div>
</span>
</div>
<canvas
ref="canvas"
v-show="showCanvas"
:width="canvasWidth"
:height="canvasHeight"
></canvas>
<div v-if="showCanvas">
<button @click="stopDetection">Stop scanning</button>
</div>
<div v-if="showCanvas && !!outputMessage">{{outputMessage}}</div>
<h4 v-if="foundCodes.length>0">Codes found:</h4>
<ul v-if="foundCodes.length>0" class="foundCodes">
<li v-for="code,i in foundCodes" :key="i">
<span
class="qrcode"
:class="{current:code.current,binary:code.binary}"
>{{code.data}}</span
>
<button @click="ev => copyOutput(code.data, ev)">Copy</button>
<button @click="textInput=code.data">Re-create</button>
<button
@click="if(confirm('Remove '+code.data.slice(0,10)+'..?')) foundCodes.splice(i,1)"
>
🗑
</button>
</li>
<li v-if="!!copyError" class="error">Copy error: {{copyError}}</li>
</ul>
<hr class="noprint" />
<textarea
class="noprint"
cols="40"
rows="2"
placeholder="Generate new code"
v-model="textInput"
@focus="textFocused=true"
@blur="textFocused=false"
></textarea>
<div v-if="!!textInput" class="noprint">
<select v-model="qrMode">
<option disabled>Data mode</option>
<option value="Numeric">Numeric</option>
<option value="Alphanumeric">Alphanumeric</option>
<option value="Byte">Byte</option>
<option :disabled="!haveKanji" value="Kanji"
>Kanji{{haveKanji?"":" (no support)"}}</option
>
</select>
<select v-model="qrEC">
<option disabled>Error correction</option>
<option value="L">Low (7%)</option>
<option value="M">Medium (15%)</option>
<option value="Q">Quartile (25%)</option>
<option value="H">High (30%)</option>
</select>
<select v-model="qrFmt">
<option disabled>Format</option>
<option value="createSvgTag">SVG</option>
<option value="createImgTag">Image</option>
<option value="createTableTag">HTML table</option>
<option value="createASCII">ASCII text</option>
</select>
<select v-model="qrCellSize">
<option disabled>Cell size</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="4">4</option>
<option value="8">8</option>
<option value="16">16</option>
</select>
<input
size="5"
:value="qrType?qrType:''"
@input="setQrType"
placeholder="type 0-40"
/>
</div>
<div v-if="!!qrMakeError" class="error">Error: {{qrMakeError}}</div>
<div v-show="!!lastTextInput">
<pre
class="qrcode"
ref="qrOutput"
@click="$refs.qrOutput.firstChild.requestFullscreen && $refs.qrOutput.firstChild.requestFullscreen()"
></pre>
<div class="noprint">
<button @click="lastTextInput=''">
Dismiss
</button>
</div>
<div class="qrcode">{{lastTextInput}}</div>
</div>
<div class="footer"></div>
</div>
<script>
"use strict";
var app = new Vue({
el: "#app",
data() {
return {
textInput: "",
textFocused: false,
lastTextInput: "",
outputMessage: null,
scanSource: "camera",
scanFile: null,
loadingMessage: null,
showCanvas: false,
scanRunning: false,
canvasWidth: null,
isPasting: false,
prevPasteHandler: null,
canvasHeight: null,
copyEvent: null,
copyError: null,
stream: null,
foundCodes: [],
video: document.createElement("video"),
qrType: 0,
qrMode: "Byte",
qrEC: "L",
qrFmt: "createSvgTag",
qrCellSize: 8,
qrMakeError: null,
};
},
computed: {
canvas() {
return this.$refs.canvas.getContext("2d");
},
outputContainer() {
return this.$refs.output;
},
haveKanji() {
return !!qrcode.stringToBytesFuncs["SJIS"];
},
},
watch: {
textInput() {
this.generateQR();
},
qrEC() {
this.generateQR();
},
qrFmt(value, oldValue) {
if (value === "createASCII" && this.qrCellSize > 2)
this.qrCellSize = 1;
else if (oldValue === "createASCII") this.qrCellSize *= 8;
else this.generateQR();
},
qrCellSize() {
this.generateQR();
},
qrType() {
this.generateQR();
},
qrMode() {
this.generateQR();
},
},
methods: {
setQrType(value) {
var i = parseInt(value.target.value);
if (i > 0 && i <= 40) this.qrType = i;
else this.qrType = 0;
},
generateQR() {
this.qrMakeError = null;
if (!this.textInput) return;
qrcode.stringToBytes = qrcode.stringToBytesFuncs["UTF-8"];
var qr = qrcode(this.qrType, this.qrEC);
try {
qr.addData(this.textInput, this.qrMode);
qr.make();
} catch (e) {
this.qrMakeError = e;
return;
}
this.$refs.qrOutput.innerHTML = qr[this.qrFmt](this.qrCellSize);
this.lastTextInput = this.textInput;
},
stopDetection() {
this.showCanvas = false;
this.scanRunning = false;
if (this.isPasting) this.stopPasting();
if (this.stream)
this.stream.getVideoTracks().forEach((track) => track.stop());
this.stream = null;
},
showVideoStream(stream) {
this.scanRunning = true;
this.video.srcObject = this.stream = stream;
this.video.setAttribute("playsinline", true);
this.video.play();
requestAnimationFrame(this.tick);
},
scanImageUrl(url) {
var img = new Image();
img.onload = () => {
this.canvasHeight = img.height;
this.canvasWidth = img.width;
this.$nextTick(() => {
this.canvas.drawImage(img, 0, 0);
this.scanCanvas();
});
};
img.onerror = (err) => {
this.loadingMessage = "Error loading image: " + err;
};
img.src = url;
return img;
},
startDetection() {
this.showCanvas = true;
this.loadingMessage = "";
switch (this.scanSource) {
case "camera":
navigator.mediaDevices
.getUserMedia({ video: { facingMode: "environment" } })
.then(this.showVideoStream)
.catch((err) => {
this.loadingMessage =
"🎥 Unable to access video stream (please make sure you have a webcam enabled): " +
err;
this.stopDetection();
});
break;
case "display":
navigator.mediaDevices
.getDisplayMedia({ video: true })
.then(this.showVideoStream)
.catch((err) => {
this.loadingMessage = "Cannot access user display: " + err;
this.stopDetection();
});
break;
case "file":
this.scanImageUrl(URL.createObjectURL(this.scanFile));
break;
case "paste":
this.isPasting = true;
this.loadingMessage = "Use CTRL-V to paste an image";
this.prevPasteHandler = document.onpaste;
document.onpaste = (ev) => {
var gotImage = false;
var nonImages = [];
Array.prototype.forEach.call(
ev.clipboardData.items,
(item) => {
if (
item.kind === "file" &&
item.type.startsWith("image/")
) {
this.scanImageUrl(
URL.createObjectURL(item.getAsFile())
);
gotImage = true;
} else nonImages.push(item.type);
}
);
if (gotImage)
this.loadingMessage =
"Image received. Paste more with CTRL-V.";
else
this.loadingMessage =
"No image data (" +
nonImages +
") pasted.\nPlease copy and paste an image with CTRL-V";
};
break;
}
},
stopPasting() {
document.onpaste = this.prevPasteHandler;
this.loadingMessage = "";
this.isPasting = false;
},
copyOutput(text, ev) {
try {
if (
navigator &&
navigator.clipboard &&
navigator.clipboard.writeText
)
return navigator.clipboard.writeText(text);
var el = ev.currentTarget.parentNode.querySelector(".qrcode");
el.contentEditable = true;
var range = document.createRange();
range.selectNodeContents(el);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
el.contentEditable = false;
} catch (e) {
this.copyError = e;
}
},
drawPath(points, color) {
var ctx = this.canvas;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i < points.length; i++)
ctx.lineTo(points[i].x, points[i].y);
ctx.lineWidth = 4;
ctx.strokeStyle = color;
ctx.stroke();
},
scanCanvas() {
var img = this.canvas.getImageData(
0,
0,
this.canvasWidth,
this.canvasHeight
);
var code = jsQR(img.data, img.width, img.height, {
inversionAttempts: "dontInvert",
});
this.foundCodes.forEach((c) => (c.current = false));
if (code) {
var loc = code.location;
this.drawPath(
[
loc.topLeftCorner,
loc.topRightCorner,
loc.bottomRightCorner,
loc.bottomLeftCorner,
loc.topLeftCorner,
],
"#FF3B58"
);
this.outputMessage = null;
var binary =
!code.data.length && code.binaryData && code.binaryData.length;
if (binary) code.data = String.fromCharCode(...code.binaryData);
var idx = this.foundCodes.findIndex((c) => c.data === code.data);
if (idx === -1)
this.foundCodes.push({
data: code.data,
current: true,
binary: true,
});
else this.foundCodes[idx].current = true;
} else this.outputMessage = "No QR code detected.";
},
tick() {
if (!this.scanRunning) return;
this.loadingMessage = "⌛ Loading video...";
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
this.loadingMessage = null;
this.canvasHeight = this.video.videoHeight;
this.canvasWidth = this.video.videoWidth;
this.canvas.drawImage(
this.video,
0,
0,
this.canvasWidth,
this.canvasHeight
);
this.scanCanvas();
}
requestAnimationFrame(this.tick);
},
},
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment