2024-10-03 01:29:20 +00:00
|
|
|
import { Module, getSeededRand } from "https://qrframe.kylezhe.ng/utils.js";
|
2024-09-13 07:52:47 +00:00
|
|
|
|
2024-07-31 23:56:43 +00:00
|
|
|
export const paramsSchema = {
|
2024-08-09 02:55:13 +00:00
|
|
|
Margin: {
|
|
|
|
type: "number",
|
|
|
|
min: 0,
|
|
|
|
max: 20,
|
|
|
|
step: 0.1,
|
2024-09-03 23:00:55 +00:00
|
|
|
default: 8,
|
2024-07-31 23:56:43 +00:00
|
|
|
},
|
2024-08-09 02:55:13 +00:00
|
|
|
"Radius offset": {
|
|
|
|
type: "number",
|
|
|
|
min: -10,
|
|
|
|
max: 10,
|
|
|
|
default: 0,
|
|
|
|
},
|
|
|
|
Foreground: {
|
2024-09-01 00:21:10 +00:00
|
|
|
type: "color",
|
2024-08-09 02:55:13 +00:00
|
|
|
default: "#000000",
|
|
|
|
},
|
|
|
|
Background: {
|
2024-09-01 00:21:10 +00:00
|
|
|
type: "color",
|
2024-08-09 02:55:13 +00:00
|
|
|
default: "#ffffff",
|
|
|
|
},
|
2024-09-03 23:00:55 +00:00
|
|
|
"Frame thickness": {
|
|
|
|
type: "number",
|
|
|
|
min: 0,
|
|
|
|
max: 10,
|
|
|
|
step: 0.1,
|
|
|
|
},
|
2024-08-09 02:55:13 +00:00
|
|
|
"Finder pattern": {
|
2024-09-01 00:21:10 +00:00
|
|
|
type: "select",
|
2024-08-23 06:30:33 +00:00
|
|
|
options: ["Default", "Circle", "Square"],
|
2024-08-09 02:55:13 +00:00
|
|
|
},
|
|
|
|
"Alignment pattern": {
|
2024-09-01 00:21:10 +00:00
|
|
|
type: "select",
|
2024-08-23 06:30:33 +00:00
|
|
|
options: ["Default", "Circle", "Square"],
|
2024-08-09 02:55:13 +00:00
|
|
|
},
|
2024-09-03 23:00:55 +00:00
|
|
|
Logo: {
|
|
|
|
type: "file",
|
|
|
|
accept: ".jpeg, .jpg, .png, .svg",
|
|
|
|
},
|
|
|
|
"Logo size": {
|
|
|
|
type: "number",
|
|
|
|
min: 0,
|
|
|
|
max: 1,
|
|
|
|
step: 0.01,
|
|
|
|
default: 0.25,
|
|
|
|
},
|
|
|
|
"Show data behind logo": {
|
|
|
|
type: "boolean",
|
|
|
|
},
|
|
|
|
"Pixel size": {
|
2024-09-01 00:21:10 +00:00
|
|
|
type: "select",
|
2024-09-03 23:00:55 +00:00
|
|
|
options: ["None", "Center", "Edge", "Random"],
|
2024-08-09 02:55:13 +00:00
|
|
|
},
|
|
|
|
Seed: {
|
|
|
|
type: "number",
|
|
|
|
min: 1,
|
|
|
|
max: 100,
|
|
|
|
default: 1,
|
2024-07-31 23:56:43 +00:00
|
|
|
},
|
2024-08-17 03:44:53 +00:00
|
|
|
};
|
2024-07-31 23:56:43 +00:00
|
|
|
|
2024-09-03 23:00:55 +00:00
|
|
|
const fmt = (n) => n.toFixed(2).replace(/.00$/, "");
|
|
|
|
|
|
|
|
export async function renderSVG(qr, params) {
|
2024-07-31 23:56:43 +00:00
|
|
|
const matrixWidth = qr.version * 4 + 17;
|
2024-08-09 02:55:13 +00:00
|
|
|
const margin = params["Margin"];
|
|
|
|
const fg = params["Foreground"];
|
|
|
|
const bg = params["Background"];
|
|
|
|
const rOffset = params["Radius offset"];
|
2024-09-03 23:00:55 +00:00
|
|
|
const file = params["Logo"];
|
|
|
|
const logoRatio = params["Logo size"];
|
|
|
|
const showLogoData = params["Show data behind logo"];
|
2024-09-13 07:52:47 +00:00
|
|
|
const rand = getSeededRand(params["Seed"]);
|
2024-09-03 23:00:55 +00:00
|
|
|
const range = (min, max) => rand() * (max - min) + min;
|
2024-08-09 02:55:13 +00:00
|
|
|
|
|
|
|
const size = matrixWidth + 2 * margin;
|
|
|
|
|
|
|
|
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${-margin} ${-margin} ${size} ${size}">`;
|
|
|
|
svg += `<rect x="${-margin}" y="${-margin}" width="${size}" height="${size}" fill="${bg}"/>`;
|
|
|
|
|
2024-09-03 23:00:55 +00:00
|
|
|
// nearest odd number
|
|
|
|
let diameter = Math.round(Math.sqrt(2) * matrixWidth) + 2 * rOffset;
|
|
|
|
if (!(diameter & 1)) diameter += 1;
|
|
|
|
|
|
|
|
const frameThick = params["Frame thickness"];
|
|
|
|
if (frameThick) {
|
|
|
|
const frameR = diameter / 2 + 1 + frameThick / 2;
|
|
|
|
svg += `<circle cx="${matrixWidth / 2}" cy="${matrixWidth / 2}" r="${frameR}" fill="none" stroke="${fg}" stroke-width="${frameThick}"/>`;
|
|
|
|
if (rOffset < -1) {
|
|
|
|
const c = matrixWidth / 2;
|
|
|
|
const offset = (frameR * Math.sqrt(2)) / 2;
|
|
|
|
const r = (-rOffset + 1) * Math.max(frameThick / 2, 1);
|
|
|
|
svg += `<circle cx="${c - offset}" cy="${c - offset}" r="${r}" fill="${bg}"/>`;
|
|
|
|
svg += `<circle cx="${c + offset}" cy="${c - offset}" r="${r}" fill="${bg}"/>`;
|
|
|
|
svg += `<circle cx="${c - offset}" cy="${c + offset}" r="${r}" fill="${bg}"/>`;
|
|
|
|
if (rOffset < -2) {
|
|
|
|
svg += `<circle cx="${c + offset}" cy="${c + offset}" r="${r}" fill="${bg}"/>`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-23 06:30:33 +00:00
|
|
|
if (params["Finder pattern"] !== "Default") {
|
2024-08-09 02:55:13 +00:00
|
|
|
for (const [x, y] of [
|
|
|
|
[0, 0],
|
|
|
|
[matrixWidth - 7, 0],
|
|
|
|
[0, matrixWidth - 7],
|
|
|
|
]) {
|
2024-08-23 06:30:33 +00:00
|
|
|
if (params["Finder pattern"] === "Circle") {
|
|
|
|
svg += `<circle cx="${x + 3.5}" cy="${y + 3.5}" r="3" fill="none" stroke="${fg}" stroke-width="1"/>`;
|
|
|
|
svg += `<circle cx="${x + 3.5}" cy="${y + 3.5}" r="1.5" fill="${fg}"/>`;
|
|
|
|
} else {
|
|
|
|
svg += `<path d="M${x},${y}h7v7h-7zM${x + 1},${y + 1}v5h5v-5zM${x + 2},${y + 2}h3v3h-3z"/>`;
|
|
|
|
}
|
2024-07-31 23:56:43 +00:00
|
|
|
}
|
|
|
|
}
|
2024-08-09 02:55:13 +00:00
|
|
|
svg += `<path fill="${fg}" d="`;
|
|
|
|
|
|
|
|
const maxDist = Math.sqrt(2) * (matrixWidth / 2);
|
2024-09-03 23:00:55 +00:00
|
|
|
const lower = Math.min(-(diameter - matrixWidth) / 2, 0);
|
|
|
|
const upper = Math.max(diameter - lower, matrixWidth);
|
2024-08-09 02:55:13 +00:00
|
|
|
|
2024-09-03 23:00:55 +00:00
|
|
|
const logoInner = Math.floor(((1 - logoRatio) * size) / 2 - margin);
|
|
|
|
const logoUpper = matrixWidth - logoInner;
|
|
|
|
for (let y = lower; y < upper; y++) {
|
|
|
|
for (let x = lower; x < upper; x++) {
|
|
|
|
if (
|
|
|
|
file &&
|
|
|
|
!showLogoData &&
|
|
|
|
x >= logoInner &&
|
|
|
|
y >= logoInner &&
|
|
|
|
x < logoUpper &&
|
|
|
|
y < logoUpper
|
|
|
|
) {
|
|
|
|
continue;
|
|
|
|
}
|
2024-08-09 02:55:13 +00:00
|
|
|
|
|
|
|
// Quiet zone around qr
|
|
|
|
const xRange1 = x >= -1 && x < 8;
|
|
|
|
const yRange1 = y >= -1 && y < 8;
|
|
|
|
const yRange2 = y > matrixWidth - 9 && y <= matrixWidth;
|
|
|
|
const xRange2 = x > matrixWidth - 9 && x <= matrixWidth;
|
|
|
|
if (
|
|
|
|
(x === -1 && (yRange1 || yRange2)) ||
|
|
|
|
(y === -1 && (xRange1 || xRange2)) ||
|
|
|
|
(x === matrixWidth && yRange1) ||
|
|
|
|
(y === matrixWidth && xRange1)
|
|
|
|
) {
|
|
|
|
continue;
|
|
|
|
}
|
2024-07-31 23:56:43 +00:00
|
|
|
|
2024-08-09 02:55:13 +00:00
|
|
|
const dx = x - (matrixWidth - 1) / 2;
|
|
|
|
const dy = y - (matrixWidth - 1) / 2;
|
|
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
2024-07-31 23:56:43 +00:00
|
|
|
|
2024-08-09 02:55:13 +00:00
|
|
|
if (x >= 0 && x < matrixWidth && y >= 0 && y < matrixWidth) {
|
|
|
|
const module = qr.matrix[y * matrixWidth + x];
|
2024-09-13 07:52:47 +00:00
|
|
|
if (!(module & Module.ON)) continue;
|
2024-07-31 23:56:43 +00:00
|
|
|
|
2024-09-13 07:52:47 +00:00
|
|
|
if (params["Finder pattern"] !== "Default" && module & Module.FINDER) {
|
2024-08-23 06:30:33 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
params["Alignment pattern"] !== "Default" &&
|
2024-09-13 07:52:47 +00:00
|
|
|
module & Module.ALIGNMENT
|
2024-08-23 06:30:33 +00:00
|
|
|
) {
|
2024-09-13 07:52:47 +00:00
|
|
|
if (module & Module.MODIFIER) {
|
2024-08-23 06:30:33 +00:00
|
|
|
if (params["Alignment pattern"] === "Circle") {
|
|
|
|
svg += `M${x + 0.5},${y - 2}a2.5,2.5 0,0,0 0,5a2.5,2.5 0,0,0 0,-5`;
|
|
|
|
svg += `M${x + 0.5},${y - 1}a1.5,1.5 0,0,1 0,3a1.5,1.5 0,0,1 0,-3`;
|
|
|
|
svg += `M${x + 0.5},${y}a.5,.5 0,0,0 0,1a.5,.5 0,0,0 0,-1`;
|
|
|
|
} else {
|
|
|
|
svg += `M${x - 2},${y - 2}h5v5h-5zM${x - 1},${y - 1}v3h3v-3zM${x},${y}h1v1h-1z`;
|
|
|
|
}
|
2024-07-31 23:56:43 +00:00
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
2024-08-09 02:55:13 +00:00
|
|
|
} else if (dist > diameter / 2) {
|
|
|
|
continue;
|
|
|
|
} else if (rand() > 0.5) {
|
|
|
|
continue;
|
|
|
|
}
|
2024-07-31 23:56:43 +00:00
|
|
|
|
2024-08-09 02:55:13 +00:00
|
|
|
let ratio;
|
2024-09-03 23:00:55 +00:00
|
|
|
switch (params["Pixel size"]) {
|
2024-08-09 02:55:13 +00:00
|
|
|
case "Center":
|
|
|
|
ratio = 1 - dist / maxDist + 0.8;
|
|
|
|
break;
|
|
|
|
case "Edge":
|
|
|
|
ratio = dist / maxDist + 0.8;
|
|
|
|
break;
|
2024-09-03 23:00:55 +00:00
|
|
|
case "Random":
|
|
|
|
ratio = range(0.8, 1.2);
|
|
|
|
break;
|
2024-08-09 02:55:13 +00:00
|
|
|
default:
|
|
|
|
ratio = 1;
|
|
|
|
}
|
2024-07-31 23:56:43 +00:00
|
|
|
|
2024-09-03 23:00:55 +00:00
|
|
|
const radius = fmt(0.5 * ratio);
|
2024-07-31 23:56:43 +00:00
|
|
|
|
2024-08-13 06:44:46 +00:00
|
|
|
svg += `M${x + 0.5},${y + 0.5 - radius}a${radius},${radius} 0,0,0 0,${2 * radius}a${radius},${radius} 0,0,0 0,${-2 * radius}`;
|
2024-07-31 23:56:43 +00:00
|
|
|
}
|
|
|
|
}
|
2024-09-03 23:00:55 +00:00
|
|
|
svg += `"/>`;
|
|
|
|
|
|
|
|
if (file != null) {
|
2024-10-02 03:31:18 +00:00
|
|
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
2024-09-03 23:00:55 +00:00
|
|
|
const b64 = btoa(
|
|
|
|
Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("")
|
|
|
|
);
|
|
|
|
const logoSize = fmt(logoRatio * size);
|
|
|
|
const logoOffset = fmt(((1 - logoRatio) * size) / 2 - margin);
|
|
|
|
svg += `<image x="${logoOffset}" y="${logoOffset}" width="${logoSize}" height="${logoSize}" href="data:${file.type};base64,${b64}"/>`;
|
|
|
|
}
|
|
|
|
svg += `</svg>`;
|
2024-08-09 02:55:13 +00:00
|
|
|
|
|
|
|
return svg;
|
2024-07-31 23:56:43 +00:00
|
|
|
}
|