somdigi-qr-generator 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +2 -4
- package/package.json +3 -4
- package/src/core.js +61 -45
- package/src/svg-helper.js +49 -0
- package/src/presets.js +0 -48
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "somdigi-qr-generator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"description": "",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"qr-code-styling": "^1.9.2"
|
|
13
|
+
"qrcode": "^1.5.4",
|
|
14
|
+
"sharp": "^0.34.5"
|
|
16
15
|
}
|
|
17
16
|
}
|
package/src/core.js
CHANGED
|
@@ -1,61 +1,77 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
const QRCode = require("qrcode");
|
|
2
|
+
const sharp = require("sharp");
|
|
3
|
+
const fs = require("fs/promises");
|
|
4
|
+
const path = require("path");
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const {
|
|
7
|
+
extractPaths,
|
|
8
|
+
buildGradient,
|
|
9
|
+
pathsToDots,
|
|
10
|
+
insertLogo
|
|
11
|
+
} = require("./svg-helper");
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
class FastDotQR {
|
|
14
|
+
static async toSVG(text, options = {}) {
|
|
15
|
+
const rawSVG = await QRCode.toString(text, {
|
|
16
|
+
type: "svg",
|
|
17
|
+
errorCorrectionLevel: "H",
|
|
18
|
+
margin: options.margin ?? 1
|
|
19
|
+
});
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
global.document = window.document;
|
|
13
|
-
global.Image = canvas.Image;
|
|
14
|
-
global.HTMLCanvasElement = canvas.Canvas;
|
|
21
|
+
const sizeMatch = rawSVG.match(/viewBox="([^"]+)"/);
|
|
22
|
+
const viewBox = sizeMatch ? sizeMatch[1] : "0 0 100 100";
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
}
|
|
24
|
+
const paths = extractPaths(rawSVG);
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
throw new Error("options.data is required");
|
|
22
|
-
}
|
|
26
|
+
const gradientId = "qrGradient";
|
|
27
|
+
const useGradient = options.gradient?.length > 1;
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
const fill = useGradient
|
|
30
|
+
? `url(#${gradientId})`
|
|
31
|
+
: options.color ?? "#000";
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
data: options.data,
|
|
30
|
-
margin: options.margin ?? 10,
|
|
33
|
+
const defs = useGradient
|
|
34
|
+
? buildGradient(gradientId, options.gradient)
|
|
35
|
+
: "";
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
const dots = pathsToDots(
|
|
38
|
+
paths,
|
|
39
|
+
options.radius ?? 1.4,
|
|
40
|
+
fill
|
|
41
|
+
);
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
43
|
+
const logo = options.logo
|
|
44
|
+
? insertLogo(options.logo, options.logoSize ?? 0.25)
|
|
45
|
+
: "";
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
// 🔒 SVG FINAL — VALID 100%
|
|
48
|
+
return `
|
|
49
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}">
|
|
50
|
+
${defs}
|
|
51
|
+
${dots}
|
|
52
|
+
${logo}
|
|
53
|
+
</svg>`;
|
|
54
|
+
}
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
static async toPNG(text, options = {}) {
|
|
57
|
+
const svg = await this.toSVG(text, options);
|
|
58
|
+
return sharp(Buffer.from(svg)).png().toBuffer();
|
|
59
|
+
}
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
static async toFile(text, filename, options = {}) {
|
|
62
|
+
const dir = path.join(process.cwd(), "storage/qr-codes");
|
|
63
|
+
await fs.mkdir(dir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const fullPath = path.join(dir, filename);
|
|
66
|
+
|
|
67
|
+
if (filename.endsWith(".svg")) {
|
|
68
|
+
await fs.writeFile(fullPath, await this.toSVG(text, options));
|
|
69
|
+
} else {
|
|
70
|
+
await fs.writeFile(fullPath, await this.toPNG(text, options));
|
|
53
71
|
}
|
|
54
|
-
});
|
|
55
72
|
|
|
56
|
-
|
|
73
|
+
return fullPath;
|
|
74
|
+
}
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
module.exports =
|
|
60
|
-
generateQR
|
|
61
|
-
};
|
|
77
|
+
module.exports = FastDotQR;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function extractPaths(svg) {
|
|
2
|
+
return svg.match(/<path[^>]*>/g) || [];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function buildGradient(id, colors) {
|
|
6
|
+
return `
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient id="${id}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
9
|
+
${colors
|
|
10
|
+
.map(
|
|
11
|
+
(c, i) =>
|
|
12
|
+
`<stop offset="${(i / (colors.length - 1)) * 100}%" stop-color="${c}" />`
|
|
13
|
+
)
|
|
14
|
+
.join("")}
|
|
15
|
+
</linearGradient>
|
|
16
|
+
</defs>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pathsToDots(paths, radius, fill) {
|
|
20
|
+
return paths
|
|
21
|
+
.map(p => {
|
|
22
|
+
const m = p.match(/M(\d+) (\d+)/);
|
|
23
|
+
if (!m) return "";
|
|
24
|
+
const x = Number(m[1]) + radius;
|
|
25
|
+
const y = Number(m[2]) + radius;
|
|
26
|
+
return `<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" />`;
|
|
27
|
+
})
|
|
28
|
+
.join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function insertLogo(logo, size = 0.25) {
|
|
32
|
+
const percent = size * 100;
|
|
33
|
+
return `
|
|
34
|
+
<image
|
|
35
|
+
href="${logo}"
|
|
36
|
+
x="${50 - percent / 2}%"
|
|
37
|
+
y="${50 - percent / 2}%"
|
|
38
|
+
width="${percent}%"
|
|
39
|
+
height="${percent}%"
|
|
40
|
+
preserveAspectRatio="xMidYMid meet"
|
|
41
|
+
/>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
extractPaths,
|
|
46
|
+
buildGradient,
|
|
47
|
+
pathsToDots,
|
|
48
|
+
insertLogo
|
|
49
|
+
};
|
package/src/presets.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
const purpleBlue = {
|
|
2
|
-
type: "linear",
|
|
3
|
-
rotation: Math.PI / 4,
|
|
4
|
-
colorStops: [
|
|
5
|
-
{ offset: 0, color: "#7F00FF" },
|
|
6
|
-
{ offset: 1, color: "#00C6FF" }
|
|
7
|
-
]
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const sunset = {
|
|
11
|
-
type: "linear",
|
|
12
|
-
rotation: Math.PI / 2,
|
|
13
|
-
colorStops: [
|
|
14
|
-
{ offset: 0, color: "#FF512F" },
|
|
15
|
-
{ offset: 1, color: "#F09819" }
|
|
16
|
-
]
|
|
17
|
-
};
|
|
18
|
-
const black = {
|
|
19
|
-
type: "linear",
|
|
20
|
-
rotation: Math.PI / 2,
|
|
21
|
-
colorStops: [
|
|
22
|
-
{ offset: 0, color: "#000000" },
|
|
23
|
-
{ offset: 1, color: "#000000" }
|
|
24
|
-
]
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const generatePresets = ({
|
|
28
|
-
type = "linear",
|
|
29
|
-
rotation = Math.PI / 4,
|
|
30
|
-
colorPrimary = "#000000",
|
|
31
|
-
colorsSecondary = "#000000"
|
|
32
|
-
}) => {
|
|
33
|
-
return{
|
|
34
|
-
type,
|
|
35
|
-
rotation,
|
|
36
|
-
colorStops: [
|
|
37
|
-
{ offset: 0, color: colorPrimary },
|
|
38
|
-
{ offset: 1, color: colorsSecondary }
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
module.exports = {
|
|
44
|
-
purpleBlue,
|
|
45
|
-
sunset,
|
|
46
|
-
black,
|
|
47
|
-
generatePresets
|
|
48
|
-
};
|