QRSVG/qrsvg-v1.0.1.js

632 lines
26 KiB
JavaScript
Raw Normal View History

2024-08-29 15:22:54 -05:00
/** SPDX-License-Identifier: MIT
******************************************************************************
* QRSVG
* Version 1.0.1
2024-08-29 15:22:54 -05:00
* 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;
2024-09-12 07:56:49 -05:00
} else if(step[0] == 'm' && (prev[0] == 'm' || prev[0] == 'M')) {
let prevPos = prev.substring(1).split(' ').map(c => parseFloat(c));
let newDelta = step.substring(1).split(' ').map(c => parseFloat(c));
newPathSpec[newPathSpec.length - 1] = prev[0] + (prevPos[0] + newDelta[0]).toPrecision(3) + ' ' + (prevPos[1] + newDelta[1]).toPrecision(3);
2024-08-29 15:22:54 -05:00
} else {
newPathSpec.push(step);
}
}
return newPathSpec;
}
2024-09-12 05:02:24 -05:00
// Special case method to calculate contours for the styles
2024-08-29 15:22:54 -05:00
// where shapes are not contiguous. This also skips the PDP.
2024-09-12 05:02:24 -05:00
function calculateTileStyleContour(bitmask, margin, style) {
if(!['dots', 'mosaic', 'confetti'].includes(style)) {
throw Error('Unsupported tiled render style: ' + style);
2024-08-29 15:22:54 -05:00
}
2024-09-12 07:56:49 -05:00
let confettiShapes = [
[ // circle
'M 0.5 0.1',
'A 0.4 0.4 0 0 1 0.5 0.9',
'A 0.4 0.4 0 0 1 0.5 0.1',
],
[ // rectangle
'M 0.1 0.15',
'L 0.9 0.15',
'L 0.9 0.85',
'L 0.1 0.85',
'L 0.1 0.15',
],
[ // star
'M 0.68 0.254',
'Q 1.21 0.284 0.786 0.58',
'Q 0.9275 1.0925 0.498 0.801',
'Q 0.0545 1.1 0.212 0.606',
'Q -0.208 0.269 0.312 0.259',
'Q 0.5 -0.25 0.68 0.254',
],
[ // heart
'M 0.5 0.3',
'C 1.2 -0.1 1 0.7 0.5 1',
'C 0 0.7 -0.2 -0.1 0.5 0.3',
],
[ // diamond
'M 0.5 0',
'Q 0.65 0.35 1 0.5',
'Q 0.65 0.65 0.5 1',
'Q 0.35 0.65 0 0.5',
'Q 0.35 0.35 0.5 0',
],
[ // shamrock
'M 0.45 0.45',
'A 0.23 0.23 0 1 1 0.55 0.45',
'A 0.23 0.23 0 1 1 0.54 0.48',
'L 0.7 0.95',
'L 0.3 0.95',
'L 0.46 0.48',
'A 0.23 0.23 0 1 1 0.45 0.45',
],
[ // lemon
'M 0.5 0',
'Q 1.5 0.5 0.5 1',
'Q -0.5 0.5 0.5 0',
],
];
2024-08-29 15:22:54 -05:00
let contour = new Contour();
let prng = new PRNG(1);
2024-09-12 07:56:49 -05:00
// Rotate a point around (0.5, 0.5) by angle in radians
let rotatePoint = (x, y, angle) => {
let rotatedX = 0.5 + (x - 0.5) * Math.cos(angle) - (y - 0.5) * Math.sin(angle);
let rotatedY = 0.5 + (x - 0.5) * Math.sin(angle) + (y - 0.5) * Math.cos(angle);
return [rotatedX, rotatedY];
};
2024-08-29 15:22:54 -05:00
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 < 8 && y < 8) ||
(x < 8 && y > bitmask.height - 8) ||
(x > bitmask.width - 8 && y < 8)) {
2024-08-29 15:22:54 -05:00
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');
2024-09-12 05:02:24 -05:00
} else if(style == 'mosaic') {
2024-08-29 15:22:54 -05:00
// 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;
2024-09-12 07:56:49 -05:00
newPathSpec.push('M' + (x + 1) + ' ' + (y + 1));
let tileCorners = [
rotatePoint(0.5 - (size / 2), 0.5 - (size / 2), angle),
rotatePoint(0.5 + (size / 2), 0.5 - (size / 2), angle),
rotatePoint(0.5 + (size / 2), 0.5 + (size / 2), angle),
rotatePoint(0.5 - (size / 2), 0.5 + (size / 2), angle),
];
newPathSpec.push('m' + tileCorners[0][0].toPrecision(3) + ' ' + tileCorners[0][1].toPrecision(3));
newPathSpec.push('l' + (tileCorners[1][0] - tileCorners[0][0]).toPrecision(3) + ' ' + (tileCorners[1][1] - tileCorners[0][1]).toPrecision(3));
newPathSpec.push('l' + (tileCorners[2][0] - tileCorners[1][0]).toPrecision(3) + ' ' + (tileCorners[2][1] - tileCorners[1][1]).toPrecision(3));
newPathSpec.push('l' + (tileCorners[3][0] - tileCorners[2][0]).toPrecision(3) + ' ' + (tileCorners[3][1] - tileCorners[2][1]).toPrecision(3));
2024-08-29 15:22:54 -05:00
newPathSpec.push('z');
2024-09-12 05:02:24 -05:00
} else if(style == 'confetti') {
2024-09-12 07:56:49 -05:00
newPathSpec.push('M' + (x + margin) + ' ' + (y + margin));
let currentShape = confettiShapes[Math.floor(prng.next() * confettiShapes.length)];
let previousPoint = [0, 0];
let angle = prng.next() * Math.PI * 2;
for(let segment of currentShape) {
segment = segment.split(' ');
let formatRotatedPoint = (i, j) => {
let rotated = rotatePoint(segment[i], segment[j], angle);
return (rotated[0] - previousPoint[0]).toPrecision(3) + ' ' + (rotated[1] - previousPoint[1]).toPrecision(3);
}
if(segment[0] == 'M') {
let rotated = rotatePoint(segment[1], segment[2], angle);
newPathSpec.push('m' + formatRotatedPoint(1, 2));
previousPoint = rotated;
} else if(segment[0] == 'L') {
let rotated = rotatePoint(segment[1], segment[2], angle);
newPathSpec.push('l' + formatRotatedPoint(1, 2));
previousPoint = rotated;
} else if(segment[0] == 'Q') {
newPathSpec.push('q' + formatRotatedPoint(1, 2) + ' ' + formatRotatedPoint(3, 4));
previousPoint = rotatePoint(segment[3], segment[4], angle)
} else if(segment[0] == 'C') {
newPathSpec.push('c' + formatRotatedPoint(1, 2) + ' ' + formatRotatedPoint(3, 4) + ' ' + formatRotatedPoint(5, 6));
previousPoint = rotatePoint(segment[5], segment[6], angle);
} else if(segment[0] == 'A') {
newPathSpec.push('a' + segment.slice(1, 6).join(' ') + ' ' + formatRotatedPoint(6, 7));
previousPoint = rotatePoint(segment[6], segment[7], angle);
}
}
2024-09-12 05:02:24 -05:00
newPathSpec.push('z');
2024-08-29 15:22:54 -05:00
}
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;
}
2024-09-12 05:02:24 -05:00
// For styles that do not use individual tiles, this method traces along
2024-08-29 15:22:54 -05:00
// 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 < 8 && y < 8) ||
(x < 8 && y > bitmask.height - 8) ||
(x > bitmask.width - 8 && y < 8)) {
2024-08-29 15:22:54 -05:00
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.
2024-09-12 05:02:24 -05:00
// It defaults to 1 to accommodate jitter and mosaic styles that have elements
// randomly extending slightly outside of the basic QR code area.
2024-08-29 15:22:54 -05:00
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');
}
2024-09-12 07:56:49 -05:00
if(['dots', 'rounded', 'confetti'].includes(style)) {
2024-08-29 15:22:54 -05:00
contour.pdpInner = makePathSpecRound(contour.pdpInner);
contour.pdpOuter = makePathSpecRound(contour.pdpOuter);
}
}
let newContour;
2024-09-12 05:02:24 -05:00
if(['dots', 'mosaic', 'confetti'].includes(style)) {
newContour = calculateTileStyleContour(bitmask, margin, style);
2024-08-29 15:22:54 -05:00
} 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') {
2024-09-12 05:02:24 -05:00
if(!['basic', 'rounded', 'dots', 'mosaic', 'confetti', 'jitter-light', 'jitter-heavy'].includes(style)) {
2024-08-29 15:22:54 -05:00
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;
2024-09-12 05:02:24 -05:00
qrsvg['calculateTileStyleContour'] = calculateTileStyleContour;
2024-08-29 15:22:54 -05:00
qrsvg['calculateShapeContour'] = calculateShapeContour;
qrsvg['calculateContour'] = calculateContour;
qrsvg['render'] = render;
})(qrsvg || (qrsvg = {}));