mirror of
https://fietkau.software/QRSVG.git
synced 2024-12-04 16:23:08 -06:00
Initial commit
This commit is contained in:
commit
92076c91fc
20
LICENSE
Normal file
20
LICENSE
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
12
README.md
Normal file
12
README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# QRSVG
|
||||||
|
|
||||||
|
This is a small JavaScript project to render a two-dimensional bitmask
|
||||||
|
(mostly assumed to be a QR code) with a fixed width and height to an SVG
|
||||||
|
element as a collection of SVG paths with defined purposes. The code
|
||||||
|
analyzes the bitmask geometrically and traces the contours of contiguous
|
||||||
|
shapes. It allows rendering QR codes in several stylized ways. Note that
|
||||||
|
this code does not contain an actual QR code creator – it expects to receive
|
||||||
|
the 2D QR code as a bitmask for its input. See the project website for a
|
||||||
|
demo and more information.
|
||||||
|
|
||||||
|
Version 1.0 – https://fietkau.software/qr
|
540
qrsvg-v1.0.js
Normal file
540
qrsvg-v1.0.js
Normal file
|
@ -0,0 +1,540 @@
|
||||||
|
/** SPDX-License-Identifier: MIT
|
||||||
|
******************************************************************************
|
||||||
|
* QRSVG
|
||||||
|
* Version 1.0
|
||||||
|
* https://fietkau.software/qr
|
||||||
|
* Copyright (c) Julian Fietkau
|
||||||
|
*
|
||||||
|
* This is a small JavaScript project to render a two-dimensional bitmask
|
||||||
|
* (mostly assumed to be a QR code) with a fixed width and height to an SVG
|
||||||
|
* element as a collection of SVG paths with defined purposes. The code
|
||||||
|
* analyzes the bitmask geometrically and traces the contours of contiguous
|
||||||
|
* shapes. It allows rendering QR codes in several stylized ways. Note that
|
||||||
|
* this code does not contain an actual QR code creator – it expects to receive
|
||||||
|
* the 2D QR code as a bitmask for its input. See the project website for a
|
||||||
|
* demo and more information.
|
||||||
|
******************************************************************************
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the “Software”), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
* DEALINGS IN THE SOFTWARE.
|
||||||
|
******************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
var qrsvg;
|
||||||
|
(function (qrsvg) {
|
||||||
|
|
||||||
|
// Data class holding four SVG pathspecs that make up a QR code pattern.
|
||||||
|
// There are separate properties for the inner and outer parts of the position
|
||||||
|
// detection pattern, for 1x1 blocks in the pattern and for all larger shapes.
|
||||||
|
// This separation is for later ease in distinct coloration.
|
||||||
|
// The pathspecs are held as arrays instead of strings so their component parts
|
||||||
|
// can be more easily iterated and manipulated. This is necessary for the
|
||||||
|
// application of shape styles.
|
||||||
|
class Contour {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.pdpOuter = [];
|
||||||
|
this.pdpInner = [];
|
||||||
|
this.dots = [];
|
||||||
|
this.shapes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data class holding a rectangular bitmask, accessible as x/y coordinates
|
||||||
|
// returning boolean values.
|
||||||
|
class Bitmask {
|
||||||
|
|
||||||
|
constructor(width, height) {
|
||||||
|
if(!Number.isInteger(width) || !Number.isInteger(height)) {
|
||||||
|
throw Error('Bitmask: width and height must be integers: ' + width + ', ' + height);
|
||||||
|
}
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this._array = new Array(width * height);
|
||||||
|
this.wipe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(x, y) {
|
||||||
|
if(!Number.isInteger(x) || !Number.isInteger(y)) {
|
||||||
|
throw Error('Bitmask: x and y must be integers: ' + x + ', ' + y);
|
||||||
|
}
|
||||||
|
if(x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this._array[y * this.width + x];
|
||||||
|
}
|
||||||
|
|
||||||
|
set(x, y, value) {
|
||||||
|
if(!Number.isInteger(x) || !Number.isInteger(y)) {
|
||||||
|
throw Error('Bitmask: x and y must be integers: ' + x + ', ' + y);
|
||||||
|
}
|
||||||
|
if(x < 0 || x >= this.width) {
|
||||||
|
throw Error('Bitmask: x must be at least 0 and less than width: ' + x);
|
||||||
|
}
|
||||||
|
if(y < 0 || y >= this.height) {
|
||||||
|
throw Error('Bitmask: y must be at least 0 and less than height: ' + y);
|
||||||
|
}
|
||||||
|
this._array[y * this.width + x] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fully overwrite the current data with a sequence of boolean
|
||||||
|
// values. In the simplest case, call wipe(false) to set all
|
||||||
|
// coordinates to false, or provide more values. They will be
|
||||||
|
// repeated in sequence as often as needed.
|
||||||
|
wipe(...pattern) {
|
||||||
|
for(let i = 0; i < this._array.length; i++) {
|
||||||
|
this._array[i] = pattern[i % pattern.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mini pseudo-random number generator. Used because rerendering
|
||||||
|
// jittered bitmasks is easier when we can do it deterministically.
|
||||||
|
class PRNG {
|
||||||
|
constructor(seed) {
|
||||||
|
// LCG using GCC's constants
|
||||||
|
this.m = 0x80000000;
|
||||||
|
this.a = 1103515245;
|
||||||
|
this.c = 12345;
|
||||||
|
this.state = seed ? seed : Math.floor(Math.random() * (this.m - 1));
|
||||||
|
}
|
||||||
|
next() {
|
||||||
|
this.state = (this.a * this.state + this.c) % this.m;
|
||||||
|
return this.state / (this.m - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take an existing segmented pathspec and round its corners.
|
||||||
|
// Used by `rounded` and `dots` styles.
|
||||||
|
function makePathSpecRound(oldPathSpec) {
|
||||||
|
let isInnerContour;
|
||||||
|
let newPathSpec = new Array();
|
||||||
|
for(let i = 0; i < oldPathSpec.length; i++) {
|
||||||
|
if(oldPathSpec[i].startsWith('M')) {
|
||||||
|
let coords = oldPathSpec[i].substring(1).split(' ').map(c => parseInt(c, 10));
|
||||||
|
if(oldPathSpec[i+1].startsWith('h')) {
|
||||||
|
coords[0] += 0.5;
|
||||||
|
isInnerContour = false;
|
||||||
|
} else if(oldPathSpec[i+1].startsWith('v')) {
|
||||||
|
coords[1] += 0.5;
|
||||||
|
isInnerContour = true;
|
||||||
|
}
|
||||||
|
newPathSpec.push('M' + coords[0] + ' ' + coords[1]);
|
||||||
|
i++; // Skip the first horizontal line segment
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'z') {
|
||||||
|
if(isInnerContour) {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 0 -0.5 0.5');
|
||||||
|
} else {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 -0.5');
|
||||||
|
}
|
||||||
|
newPathSpec.push('z');
|
||||||
|
// End this loop iteration here because (a) if this is the last path
|
||||||
|
// segment, trying to access i+1 further down would cause errors,
|
||||||
|
// and (b) because we might as well.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'h1' && oldPathSpec[i+1] == 'h1') {
|
||||||
|
newPathSpec.push('h1');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'h-1' && oldPathSpec[i+1] == 'h-1') {
|
||||||
|
newPathSpec.push('h-1');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'v1' && oldPathSpec[i+1] == 'v1') {
|
||||||
|
newPathSpec.push('v1');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'v-1' && oldPathSpec[i+1] == 'v-1') {
|
||||||
|
newPathSpec.push('v-1');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'h1' && oldPathSpec[i+1] == 'v1') {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 0.5');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'h1' && oldPathSpec[i+1] == 'v-1') {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 0 0.5 -0.5');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'h-1' && oldPathSpec[i+1] == 'v1') {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 0 -0.5 0.5');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'h-1' && oldPathSpec[i+1] == 'v-1') {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 -0.5 -0.5');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'v1' && oldPathSpec[i+1] == 'h1') {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 0 0.5 0.5');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'v1' && oldPathSpec[i+1] == 'h-1') {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 -0.5 0.5');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'v-1' && oldPathSpec[i+1] == 'h1') {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 -0.5');
|
||||||
|
}
|
||||||
|
if(oldPathSpec[i] == 'v-1' && oldPathSpec[i+1] == 'h-1') {
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 0 -0.5 -0.5');
|
||||||
|
}
|
||||||
|
let len = newPathSpec.length;
|
||||||
|
if(len >= 2 && newPathSpec[len-1][0] == newPathSpec[len-2][0] && ['h', 'v'].includes(newPathSpec[len-1][0])) {
|
||||||
|
let command = newPathSpec[len-1][0];
|
||||||
|
let delta1 = parseInt(newPathSpec.pop().slice(1), 10);
|
||||||
|
let delta2 = parseInt(newPathSpec.pop().slice(1), 10);
|
||||||
|
newPathSpec.push(command + (delta1 + delta2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newPathSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addJitterToPathSpec(oldPathSpec, jitterValue, prng) {
|
||||||
|
let newPathSpec = []
|
||||||
|
let currentPos = [null, null];
|
||||||
|
for(let step of oldPathSpec) {
|
||||||
|
if(step.startsWith('M')) {
|
||||||
|
currentPos = step.slice(1).split(' ').map(c => parseInt(c, 10));
|
||||||
|
newPathSpec.push(step);
|
||||||
|
} else if(step.startsWith('h') || step.startsWith('v')) {
|
||||||
|
let posIndex = 0; // default: h
|
||||||
|
if(step.startsWith('v')) {
|
||||||
|
posIndex = 1;
|
||||||
|
}
|
||||||
|
let distance = parseInt(step.slice(1), 10);
|
||||||
|
currentPos[posIndex] += distance;
|
||||||
|
let jitteredPos = currentPos.map(c => c + (prng.next() * 2 - 1) * jitterValue);
|
||||||
|
newPathSpec.push('L' + jitteredPos[0] + ' ' + jitteredPos[1]);
|
||||||
|
} else {
|
||||||
|
newPathSpec.push(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newPathSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactPathSpec(oldPathSpec) {
|
||||||
|
let newPathSpec = [];
|
||||||
|
if(oldPathSpec.length == 0) {
|
||||||
|
return newPathSpec;
|
||||||
|
}
|
||||||
|
newPathSpec.push(oldPathSpec[0]);
|
||||||
|
for(let step of oldPathSpec.slice(1)) {
|
||||||
|
let prev = newPathSpec[newPathSpec.length - 1];
|
||||||
|
if((step[0] == 'h' || step[0] == 'v') && step[0] == prev[0]) {
|
||||||
|
let distance = parseInt(prev.substring(1), 10) + parseInt(step.substring(1), 10);
|
||||||
|
newPathSpec[newPathSpec.length - 1] = step[0] + distance;
|
||||||
|
} else {
|
||||||
|
newPathSpec.push(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newPathSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case method to calculate contours for the two styles
|
||||||
|
// where shapes are not contiguous. This also skips the PDP.
|
||||||
|
function calculateDotsOrMosaicContour(bitmask, margin, style) {
|
||||||
|
if(style != 'dots' && style != 'mosaic') {
|
||||||
|
throw Error('Unsupported dots/mosaic render style: ' + style);
|
||||||
|
}
|
||||||
|
let contour = new Contour();
|
||||||
|
let prng = new PRNG(1);
|
||||||
|
for(let y = 0; y < bitmask.height; y++) {
|
||||||
|
for(let x = 0; x < bitmask.width; x++) {
|
||||||
|
if(bitmask.width > 16 && bitmask.height > 16) {
|
||||||
|
// Check if we are inside a PDP area, because they have already been handled separately.
|
||||||
|
if((x < 7 + margin && y < 7 + margin) ||
|
||||||
|
(x < 7 + margin && y > bitmask.height - margin - 7) ||
|
||||||
|
(x > bitmask.width - margin - 7 && y < 7 + margin)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(bitmask.get(x, y)) {
|
||||||
|
let newPathSpec = new Array();
|
||||||
|
if(style == 'dots') {
|
||||||
|
newPathSpec.push('M' + (x + margin + 0.5) + ' ' + (y + margin));
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 0.5');
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 -0.5 0.5');
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 -0.5 -0.5');
|
||||||
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 -0.5');
|
||||||
|
newPathSpec.push('z');
|
||||||
|
}
|
||||||
|
if(style == 'mosaic') {
|
||||||
|
// For the mosaic style, we jury-rig a pseudo-random rotation for each pixel.
|
||||||
|
let size = 0.9; // relative to grid size
|
||||||
|
let maxAngle = Math.PI * 0.03;
|
||||||
|
let angle = (prng.next() * 2 - 1) * maxAngle;
|
||||||
|
// |------ middle of the pixel ------| |-north displacement-| |-west displacement-|
|
||||||
|
let topLeftX = x + margin + 0.5 + ((1 - size) / 2) - 0.5 * Math.cos(angle) + 0.5 * Math.sin(angle);
|
||||||
|
let topLeftY = y + margin + 0.5 + ((1 - size) / 2) + 0.5 * Math.cos(angle) - 0.5 * Math.sin(angle) - 1;
|
||||||
|
newPathSpec.push('M' + topLeftX.toPrecision(3) + ' ' + topLeftY.toPrecision(3));
|
||||||
|
newPathSpec.push('l' + (size * Math.cos(angle)).toPrecision(3) + ' ' + (size * Math.sin(angle)).toPrecision(3));
|
||||||
|
newPathSpec.push(('l-' + (size * Math.sin(angle)).toPrecision(3) + ' ' + (size * Math.cos(angle)).toPrecision(3)).replaceAll('--', ''));
|
||||||
|
newPathSpec.push(('l-' + (size * Math.cos(angle)).toPrecision(3) + ' -' + (size * Math.sin(angle)).toPrecision(3)).replaceAll('--', ''));
|
||||||
|
newPathSpec.push(('l' + (size * Math.sin(angle)).toPrecision(3) + ' -' + (size * Math.cos(angle)).toPrecision(3)).replaceAll('--', ''));
|
||||||
|
newPathSpec.push('z');
|
||||||
|
}
|
||||||
|
if(!bitmask.get(x - 1, y) && !bitmask.get(x + 1, y) && !bitmask.get(x, y - 1) && !bitmask.get(x, y + 1)) {
|
||||||
|
contour.dots = contour.dots.concat(newPathSpec);
|
||||||
|
} else {
|
||||||
|
contour.shapes = contour.shapes.concat(newPathSpec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contour;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For styles other than `dots` or `mosaic`, this method traces along
|
||||||
|
// contiguous shapes in the bitmask and builds a contour. Still skips
|
||||||
|
// the PDP and assumes it is handled separately.
|
||||||
|
function calculateShapeContour(bitmask, margin, style) {
|
||||||
|
let contour = new Contour();
|
||||||
|
let corners = new Array();
|
||||||
|
let width = bitmask.width + 1;
|
||||||
|
let height = bitmask.height + 1;
|
||||||
|
for(let y = 0; y < height; y++) {
|
||||||
|
for(let x = 0; x < width; x++) {
|
||||||
|
corners.push({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(let y = 0; y < height; y++) {
|
||||||
|
for(let x = 0; x < width; x++) {
|
||||||
|
if(Object.keys(corners[y * width + x]).includes('e')) continue;
|
||||||
|
if(bitmask.get(x, y) == bitmask.get(x - 1, y) && bitmask.get(x, y) == bitmask.get(x, y - 1)
|
||||||
|
&& bitmask.get(x, y) == bitmask.get(x - 1, y - 1)) continue; // This corner is not part of any edge.
|
||||||
|
if(bitmask.get(x, y - 1) || !bitmask.get(x, y)) continue;
|
||||||
|
let contourX = x;
|
||||||
|
let contourY = y;
|
||||||
|
let direction = 'e';
|
||||||
|
while(!corners[contourY * width + contourX][direction]) {
|
||||||
|
let prevDirection = direction;
|
||||||
|
if(direction == 'n') {
|
||||||
|
if(bitmask.get(contourX, contourY - 1) && !bitmask.get(contourX - 1, contourY - 1)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX, contourY - 1];
|
||||||
|
} else if(!bitmask.get(contourX, contourY - 1)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX + 1, contourY];
|
||||||
|
direction = 'e';
|
||||||
|
} else if(bitmask.get(contourX - 1, contourY - 1) && bitmask.get(contourX, contourY - 1)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX - 1, contourY];
|
||||||
|
direction = 'w';
|
||||||
|
}
|
||||||
|
} else if(direction == 'e') {
|
||||||
|
if(bitmask.get(contourX, contourY) && !bitmask.get(contourX, contourY - 1)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX + 1, contourY];
|
||||||
|
} else if(!bitmask.get(contourX, contourY)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX, contourY + 1];
|
||||||
|
direction = 's';
|
||||||
|
} else if(bitmask.get(contourX, contourY) && bitmask.get(contourX, contourY - 1)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX, contourY - 1];
|
||||||
|
direction = 'n';
|
||||||
|
}
|
||||||
|
} else if(direction == 's') {
|
||||||
|
if(bitmask.get(contourX - 1, contourY) && !bitmask.get(contourX, contourY)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX, contourY + 1];
|
||||||
|
} else if(!bitmask.get(contourX - 1, contourY)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX - 1, contourY];
|
||||||
|
direction = 'w';
|
||||||
|
} else if(bitmask.get(contourX, contourY) && bitmask.get(contourX - 1, contourY)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX + 1, contourY];
|
||||||
|
direction = 'e';
|
||||||
|
}
|
||||||
|
} else if(direction == 'w') {
|
||||||
|
if(bitmask.get(contourX - 1, contourY - 1) && !bitmask.get(contourX - 1, contourY)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX - 1, contourY];
|
||||||
|
} else if(!bitmask.get(contourX - 1, contourY - 1)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX, contourY - 1];
|
||||||
|
direction = 'n';
|
||||||
|
} else if(bitmask.get(contourX - 1, contourY) && bitmask.get(contourX - 1, contourY - 1)) {
|
||||||
|
corners[contourY * width + contourX][direction] = [contourX, contourY + 1];
|
||||||
|
direction = 's';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let next = corners[contourY * width + contourX][prevDirection];
|
||||||
|
if(!next) break;
|
||||||
|
contourX = next[0];
|
||||||
|
contourY = next[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(let y = 0; y < height; y++) {
|
||||||
|
for(let x = 0; x < width; x++) {
|
||||||
|
if(bitmask.width > 16 && bitmask.height > 16) {
|
||||||
|
// Check if we are inside a PDP area, because they have already been handled separately.
|
||||||
|
if((x < 7 + margin && y < 7 + margin) ||
|
||||||
|
(x < 7 + margin && y > bitmask.height - margin - 7) ||
|
||||||
|
(x > bitmask.width - margin - 7 && y < 7 + margin)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(Object.keys(corners[y * width + x]).length == 0) continue;
|
||||||
|
let direction = Object.keys(corners[y * width + x])[0];
|
||||||
|
let newPathSpec = new Array();
|
||||||
|
newPathSpec.push('M' + (x + margin) + ' ' + (y + margin));
|
||||||
|
let contourX = x;
|
||||||
|
let contourY = y;
|
||||||
|
while(corners[contourY * width + contourX][direction]) {
|
||||||
|
let next = corners[contourY * width + contourX][direction];
|
||||||
|
let prevSpecStep = newPathSpec[newPathSpec.length - 1];
|
||||||
|
delete corners[contourY * width + contourX][direction];
|
||||||
|
let pathCommand, pathDelta;
|
||||||
|
if(next[0] > contourX) {
|
||||||
|
direction = 'e';
|
||||||
|
pathCommand = 'h';
|
||||||
|
pathDelta = 1;
|
||||||
|
} else if(next[0] < contourX) {
|
||||||
|
direction = 'w';
|
||||||
|
pathCommand = 'h';
|
||||||
|
pathDelta = -1;
|
||||||
|
} else if(next[1] > contourY) {
|
||||||
|
direction = 's';
|
||||||
|
pathCommand = 'v';
|
||||||
|
pathDelta = 1;
|
||||||
|
} else if(next[1] < contourY) {
|
||||||
|
direction = 'n';
|
||||||
|
pathCommand = 'v';
|
||||||
|
pathDelta = -1;
|
||||||
|
}
|
||||||
|
newPathSpec.push(pathCommand + pathDelta);
|
||||||
|
contourX = next[0];
|
||||||
|
contourY = next[1];
|
||||||
|
}
|
||||||
|
// Skip non-shaped paths for serialization
|
||||||
|
if(newPathSpec.length <= 2) continue;
|
||||||
|
// Avoid double-pathing the initial segment
|
||||||
|
if(newPathSpec.length % 2 == 0) {
|
||||||
|
newPathSpec.pop();
|
||||||
|
}
|
||||||
|
// Technically at this point we should have already returned to the start,
|
||||||
|
// but adding an explicit `z` anyway helps with rendering under some
|
||||||
|
// circumstances. For pure line segment paths we could use `z` to jump
|
||||||
|
// back instead of making the last step before here explicit, but that can
|
||||||
|
// interfere with the way we round corners in some styles.
|
||||||
|
newPathSpec.push('z');
|
||||||
|
if(newPathSpec.length == 6 && !newPathSpec[1].startsWith('v')) {
|
||||||
|
contour.dots = contour.dots.concat(newPathSpec);
|
||||||
|
} else {
|
||||||
|
contour.shapes = contour.shapes.concat(newPathSpec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contour;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overarching method that turns a 2D bitmask into a set of contour pathspecs.
|
||||||
|
// `margin` is an offset that is added to all x and y coordinates in the output.
|
||||||
|
// output. It defaults to 1 to accommodate jitter and mosaic styles that have
|
||||||
|
// elements randomly extending slightly outside of the basic QR code area.
|
||||||
|
function calculateContour(bitmask, margin = 1, style = 'basic') {
|
||||||
|
let contour = new Contour();
|
||||||
|
if(bitmask.width > 16 && bitmask.height > 16) {
|
||||||
|
// This is where we build the PDP contours, regardless of style. Skipped for
|
||||||
|
// bitmasks below a size threshold - those are assumed to not be valid QR codes.
|
||||||
|
// I also tried rendering the PDP paths as individual "pixels" in the dots and
|
||||||
|
// mosaic styles, but that led to bad scanning compatibility, so we take care
|
||||||
|
// to keep those more solid than the rest of the code.
|
||||||
|
for(let offset of [[margin, margin], [bitmask.width + margin - 7, margin], [margin, bitmask.height + margin - 7]]) {
|
||||||
|
contour.pdpOuter.push('M' + offset[0] + ' ' + offset[1]);
|
||||||
|
contour.pdpOuter.push(...Array(7).fill('h1'));
|
||||||
|
contour.pdpOuter.push(...Array(7).fill('v1'));
|
||||||
|
contour.pdpOuter.push(...Array(7).fill('h-1'));
|
||||||
|
contour.pdpOuter.push(...Array(7).fill('v-1'));
|
||||||
|
contour.pdpOuter.push('z');
|
||||||
|
contour.pdpOuter.push('M' + (offset[0] + 1) + ' ' + (offset[1] + 1));
|
||||||
|
contour.pdpOuter.push(...Array(5).fill('v1'));
|
||||||
|
contour.pdpOuter.push(...Array(5).fill('h1'));
|
||||||
|
contour.pdpOuter.push(...Array(5).fill('v-1'));
|
||||||
|
contour.pdpOuter.push(...Array(5).fill('h-1'));
|
||||||
|
contour.pdpOuter.push('z');
|
||||||
|
contour.pdpInner.push('M' + (offset[0] + 2) + ' ' + (offset[1] + 2));
|
||||||
|
contour.pdpInner.push(...Array(3).fill('h1'));
|
||||||
|
contour.pdpInner.push(...Array(3).fill('v1'));
|
||||||
|
contour.pdpInner.push(...Array(3).fill('h-1'));
|
||||||
|
contour.pdpInner.push(...Array(3).fill('v-1'));
|
||||||
|
contour.pdpInner.push('z');
|
||||||
|
}
|
||||||
|
if(style == 'dots' || style == 'rounded') {
|
||||||
|
contour.pdpInner = makePathSpecRound(contour.pdpInner);
|
||||||
|
contour.pdpOuter = makePathSpecRound(contour.pdpOuter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let newContour;
|
||||||
|
if(style == 'dots' || style == 'mosaic') {
|
||||||
|
newContour = calculateDotsOrMosaicContour(bitmask, margin, style);
|
||||||
|
} else {
|
||||||
|
newContour = calculateShapeContour(bitmask, margin, style);
|
||||||
|
}
|
||||||
|
contour.dots = newContour.dots;
|
||||||
|
contour.shapes = newContour.shapes;
|
||||||
|
if(style == 'rounded') {
|
||||||
|
contour.shapes = makePathSpecRound(contour.shapes);
|
||||||
|
contour.dots = makePathSpecRound(contour.dots);
|
||||||
|
}
|
||||||
|
if(style.startsWith('jitter-')) {
|
||||||
|
let jitterValue = 0.0;
|
||||||
|
// Suitable jitter values that still lead to good scanning compatibility
|
||||||
|
// have been derived experimentally. Customize as you see fit.
|
||||||
|
if(style == 'jitter-heavy') {
|
||||||
|
jitterValue = 0.15;
|
||||||
|
} else if (style == 'jitter-light') {
|
||||||
|
jitterValue = 0.07;
|
||||||
|
}
|
||||||
|
let prng = new PRNG(1);
|
||||||
|
// It's important to jitterize the shapes and dots first, as they
|
||||||
|
// are different for every QR code. Reusing the same PRNG afterwards
|
||||||
|
// for the PDP paths ensures that they get different jitter values
|
||||||
|
// for different QR codes and thus not look the same every time.
|
||||||
|
contour.shapes = addJitterToPathSpec(contour.shapes, jitterValue, prng);
|
||||||
|
contour.dots = addJitterToPathSpec(contour.dots, jitterValue, prng);
|
||||||
|
contour.pdpInner = addJitterToPathSpec(contour.pdpInner, jitterValue, prng);
|
||||||
|
contour.pdpOuter = addJitterToPathSpec(contour.pdpOuter, jitterValue, prng);
|
||||||
|
} else {
|
||||||
|
contour.shapes = compactPathSpec(contour.shapes);
|
||||||
|
contour.dots = compactPathSpec(contour.dots);
|
||||||
|
contour.pdpInner = compactPathSpec(contour.pdpInner);
|
||||||
|
contour.pdpOuter = compactPathSpec(contour.pdpOuter);
|
||||||
|
}
|
||||||
|
return contour;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the main callable method to render a bitmask into an SVG
|
||||||
|
// element using a specific render style. The SVG element will be
|
||||||
|
// cleared, the viewBox will be adjusted as needed, and four path
|
||||||
|
// elements will be created within containing the PDP inner and
|
||||||
|
// outer parts, dots, and other shapes. See a few lines below for
|
||||||
|
// the list of valid styles.
|
||||||
|
function render(bitmask, renderTarget, style = 'basic') {
|
||||||
|
if(!['basic', 'rounded', 'dots', 'mosaic', 'jitter-light', 'jitter-heavy'].includes(style)) {
|
||||||
|
throw Error('Unsupported render style: ' + style);
|
||||||
|
}
|
||||||
|
renderTarget.setAttribute('viewBox', '0 0 ' + (bitmask.width + 2) + ' ' + (bitmask.height + 2));
|
||||||
|
while(renderTarget.firstChild) {
|
||||||
|
renderTarget.firstChild.remove();
|
||||||
|
}
|
||||||
|
let contours = qrsvg.calculateContour(bitmask, 1, style);
|
||||||
|
for(let contourType in contours) {
|
||||||
|
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||||
|
path.setAttribute('d', contours[contourType].join(''));
|
||||||
|
// Add your customizations - e.g. `fill` colors, custom classes... - here.
|
||||||
|
renderTarget.appendChild(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qrsvg['Bitmask'] = Bitmask;
|
||||||
|
qrsvg['Contour'] = Contour;
|
||||||
|
qrsvg['PRNG'] = PRNG;
|
||||||
|
qrsvg['makePathSpecRound'] = makePathSpecRound;
|
||||||
|
qrsvg['addJitterToPathSpec'] = addJitterToPathSpec;
|
||||||
|
qrsvg['compactPathSpec'] = compactPathSpec;
|
||||||
|
qrsvg['calculateDotsOrMosaicContour'] = calculateDotsOrMosaicContour;
|
||||||
|
qrsvg['calculateShapeContour'] = calculateShapeContour;
|
||||||
|
qrsvg['calculateContour'] = calculateContour;
|
||||||
|
qrsvg['render'] = render;
|
||||||
|
|
||||||
|
})(qrsvg || (qrsvg = {}));
|
Loading…
Reference in New Issue
Block a user