@qr-styled/browser 1.0.0
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/LICENSE +21 -0
- package/README.md +341 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/QRGenerator.d.ts +59 -0
- package/dist/lib/QRGenerator.js +184 -0
- package/dist/lib/renderers/BackgroundRenderer.d.ts +21 -0
- package/dist/lib/renderers/BackgroundRenderer.js +47 -0
- package/dist/lib/renderers/GradientRenderer.d.ts +17 -0
- package/dist/lib/renderers/GradientRenderer.js +43 -0
- package/dist/lib/renderers/LogoRenderer.d.ts +25 -0
- package/dist/lib/renderers/LogoRenderer.js +89 -0
- package/dist/lib/renderers/ModuleRenderer.d.ts +43 -0
- package/dist/lib/renderers/ModuleRenderer.js +161 -0
- package/dist/lib/renderers/SVGRenderer.d.ts +22 -0
- package/dist/lib/renderers/SVGRenderer.js +73 -0
- package/dist/lib/utils/formatters.d.ts +25 -0
- package/dist/lib/utils/formatters.js +106 -0
- package/dist/lib/utils/types.d.ts +119 -0
- package/dist/lib/utils/types.js +34 -0
- package/dist/lib/utils/validators.d.ts +10 -0
- package/dist/lib/utils/validators.js +126 -0
- package/package.json +71 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles gradient creation for QR codes
|
|
3
|
+
*/
|
|
4
|
+
export class GradientRenderer {
|
|
5
|
+
constructor(ctx, options) {
|
|
6
|
+
this.ctx = ctx;
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a gradient fill style
|
|
11
|
+
*/
|
|
12
|
+
createGradient() {
|
|
13
|
+
const { size, gradientColors, gradientAngle } = this.options;
|
|
14
|
+
const ctx = this.ctx;
|
|
15
|
+
// Parse gradient colors
|
|
16
|
+
const colors = gradientColors.split(',').map(c => c.trim());
|
|
17
|
+
// Calculate angle in radians
|
|
18
|
+
const angleRad = (gradientAngle * Math.PI) / 180;
|
|
19
|
+
// Calculate gradient start and end points
|
|
20
|
+
const centerX = size / 2;
|
|
21
|
+
const centerY = size / 2;
|
|
22
|
+
const length = Math.sqrt(size * size + size * size) / 2;
|
|
23
|
+
const x0 = centerX - Math.cos(angleRad) * length;
|
|
24
|
+
const y0 = centerY - Math.sin(angleRad) * length;
|
|
25
|
+
const x1 = centerX + Math.cos(angleRad) * length;
|
|
26
|
+
const y1 = centerY + Math.sin(angleRad) * length;
|
|
27
|
+
// Create linear gradient
|
|
28
|
+
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
29
|
+
// Add color stops uniformly distributed
|
|
30
|
+
colors.forEach((color, index) => {
|
|
31
|
+
const stop = index / (colors.length - 1);
|
|
32
|
+
gradient.addColorStop(stop, color);
|
|
33
|
+
});
|
|
34
|
+
return gradient;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Gets the appropriate fill style (gradient or solid color)
|
|
38
|
+
*/
|
|
39
|
+
getFillStyle() {
|
|
40
|
+
const { gradient, foregroundColor } = this.options;
|
|
41
|
+
return gradient ? this.createGradient() : foregroundColor;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { QROptions } from '../utils/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Handles logo rendering on QR codes
|
|
4
|
+
*/
|
|
5
|
+
export declare class LogoRenderer {
|
|
6
|
+
private ctx;
|
|
7
|
+
private options;
|
|
8
|
+
constructor(ctx: CanvasRenderingContext2D, options: Required<QROptions>);
|
|
9
|
+
/**
|
|
10
|
+
* Renders logo with background
|
|
11
|
+
*/
|
|
12
|
+
render(logo: string | HTMLImageElement): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Loads an image from URL
|
|
15
|
+
*/
|
|
16
|
+
private loadImage;
|
|
17
|
+
/**
|
|
18
|
+
* Draws logo background (circle or rounded square)
|
|
19
|
+
*/
|
|
20
|
+
private drawLogoBackground;
|
|
21
|
+
/**
|
|
22
|
+
* Draws logo with shadow effect
|
|
23
|
+
*/
|
|
24
|
+
private drawLogoWithShadow;
|
|
25
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles logo rendering on QR codes
|
|
3
|
+
*/
|
|
4
|
+
export class LogoRenderer {
|
|
5
|
+
constructor(ctx, options) {
|
|
6
|
+
this.ctx = ctx;
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Renders logo with background
|
|
11
|
+
*/
|
|
12
|
+
async render(logo) {
|
|
13
|
+
if (!logo)
|
|
14
|
+
return;
|
|
15
|
+
let img;
|
|
16
|
+
if (typeof logo === 'string') {
|
|
17
|
+
// Load image from URL
|
|
18
|
+
img = await this.loadImage(logo);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
img = logo;
|
|
22
|
+
}
|
|
23
|
+
const { size, logoPadding } = this.options;
|
|
24
|
+
const logoSize = size * 0.25;
|
|
25
|
+
const bgSize = logoSize + logoPadding;
|
|
26
|
+
const centerX = size / 2;
|
|
27
|
+
const centerY = size / 2;
|
|
28
|
+
// Draw logo background
|
|
29
|
+
this.drawLogoBackground(centerX, centerY, bgSize);
|
|
30
|
+
// Draw logo with shadow
|
|
31
|
+
this.drawLogoWithShadow(img, centerX, centerY, logoSize);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Loads an image from URL
|
|
35
|
+
*/
|
|
36
|
+
loadImage(url) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const img = new Image();
|
|
39
|
+
img.crossOrigin = 'anonymous';
|
|
40
|
+
img.onload = () => resolve(img);
|
|
41
|
+
img.onerror = reject;
|
|
42
|
+
img.src = url;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Draws logo background (circle or rounded square)
|
|
47
|
+
*/
|
|
48
|
+
drawLogoBackground(centerX, centerY, bgSize) {
|
|
49
|
+
const { logoShape, logoRadius, backgroundColor } = this.options;
|
|
50
|
+
const ctx = this.ctx;
|
|
51
|
+
ctx.fillStyle = backgroundColor;
|
|
52
|
+
if (logoShape === 'circle') {
|
|
53
|
+
ctx.beginPath();
|
|
54
|
+
ctx.arc(centerX, centerY, bgSize / 2, 0, Math.PI * 2);
|
|
55
|
+
ctx.fill();
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Rounded square
|
|
59
|
+
const squareX = centerX - bgSize / 2;
|
|
60
|
+
const squareY = centerY - bgSize / 2;
|
|
61
|
+
ctx.beginPath();
|
|
62
|
+
ctx.moveTo(squareX + logoRadius, squareY);
|
|
63
|
+
ctx.arcTo(squareX + bgSize, squareY, squareX + bgSize, squareY + bgSize, logoRadius);
|
|
64
|
+
ctx.arcTo(squareX + bgSize, squareY + bgSize, squareX, squareY + bgSize, logoRadius);
|
|
65
|
+
ctx.arcTo(squareX, squareY + bgSize, squareX, squareY, logoRadius);
|
|
66
|
+
ctx.arcTo(squareX, squareY, squareX + bgSize, squareY, logoRadius);
|
|
67
|
+
ctx.closePath();
|
|
68
|
+
ctx.fill();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Draws logo with shadow effect
|
|
73
|
+
*/
|
|
74
|
+
drawLogoWithShadow(img, centerX, centerY, logoSize) {
|
|
75
|
+
const ctx = this.ctx;
|
|
76
|
+
// Apply shadow
|
|
77
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.15)';
|
|
78
|
+
ctx.shadowBlur = 12;
|
|
79
|
+
ctx.shadowOffsetX = 0;
|
|
80
|
+
ctx.shadowOffsetY = 2;
|
|
81
|
+
// Draw logo
|
|
82
|
+
const logoX = centerX - logoSize / 2;
|
|
83
|
+
const logoY = centerY - logoSize / 2;
|
|
84
|
+
ctx.drawImage(img, logoX, logoY, logoSize, logoSize);
|
|
85
|
+
// Reset shadow
|
|
86
|
+
ctx.shadowColor = 'transparent';
|
|
87
|
+
ctx.shadowBlur = 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { QROptions } from '../utils/types.js';
|
|
2
|
+
interface QRModules {
|
|
3
|
+
size: number;
|
|
4
|
+
get(row: number, col: number): number;
|
|
5
|
+
}
|
|
6
|
+
interface RenderOptions extends Required<QROptions> {
|
|
7
|
+
moduleCount: number;
|
|
8
|
+
moduleSize: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Handles QR module rendering with rounded corners
|
|
12
|
+
*/
|
|
13
|
+
export declare class ModuleRenderer {
|
|
14
|
+
private ctx;
|
|
15
|
+
private modules;
|
|
16
|
+
private options;
|
|
17
|
+
constructor(ctx: CanvasRenderingContext2D, modules: QRModules, options: RenderOptions);
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a module exists at given position
|
|
20
|
+
*/
|
|
21
|
+
private hasModule;
|
|
22
|
+
/**
|
|
23
|
+
* Checks if position is part of finder pattern (eye)
|
|
24
|
+
*/
|
|
25
|
+
private isPartOfEye;
|
|
26
|
+
/**
|
|
27
|
+
* Renders all QR modules
|
|
28
|
+
*/
|
|
29
|
+
render(fillStyle: string | CanvasGradient): void;
|
|
30
|
+
/**
|
|
31
|
+
* Renders modules with rounded corners
|
|
32
|
+
*/
|
|
33
|
+
private renderRoundedModules;
|
|
34
|
+
/**
|
|
35
|
+
* Draws a single module with selectively rounded corners
|
|
36
|
+
*/
|
|
37
|
+
private drawRoundedModule;
|
|
38
|
+
/**
|
|
39
|
+
* Renders simple square modules
|
|
40
|
+
*/
|
|
41
|
+
private renderSquareModules;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles QR module rendering with rounded corners
|
|
3
|
+
*/
|
|
4
|
+
export class ModuleRenderer {
|
|
5
|
+
constructor(ctx, modules, options) {
|
|
6
|
+
this.ctx = ctx;
|
|
7
|
+
this.modules = modules;
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks if a module exists at given position
|
|
12
|
+
*/
|
|
13
|
+
hasModule(row, col) {
|
|
14
|
+
const { moduleCount } = this.options;
|
|
15
|
+
if (row < 0 || row >= moduleCount || col < 0 || col >= moduleCount) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return this.modules.get(row, col) === 1;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Checks if position is part of finder pattern (eye)
|
|
22
|
+
*/
|
|
23
|
+
isPartOfEye(row, col) {
|
|
24
|
+
const { moduleCount } = this.options;
|
|
25
|
+
// Top-left eye
|
|
26
|
+
if (row < 7 && col < 7)
|
|
27
|
+
return true;
|
|
28
|
+
// Top-right eye
|
|
29
|
+
if (row < 7 && col >= moduleCount - 7)
|
|
30
|
+
return true;
|
|
31
|
+
// Bottom-left eye
|
|
32
|
+
if (row >= moduleCount - 7 && col < 7)
|
|
33
|
+
return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders all QR modules
|
|
38
|
+
*/
|
|
39
|
+
render(fillStyle) {
|
|
40
|
+
const { rounded } = this.options;
|
|
41
|
+
this.ctx.fillStyle = fillStyle;
|
|
42
|
+
if (rounded) {
|
|
43
|
+
this.renderRoundedModules();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.renderSquareModules();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Renders modules with rounded corners
|
|
51
|
+
*/
|
|
52
|
+
renderRoundedModules() {
|
|
53
|
+
const { moduleCount, moduleSize, padding, moduleRadius, eyeColor, eyeRadius } = this.options;
|
|
54
|
+
const ctx = this.ctx;
|
|
55
|
+
for (let row = 0; row < moduleCount; row++) {
|
|
56
|
+
for (let col = 0; col < moduleCount; col++) {
|
|
57
|
+
if (this.modules.get(row, col)) {
|
|
58
|
+
const x = padding + col * moduleSize;
|
|
59
|
+
const y = padding + row * moduleSize;
|
|
60
|
+
// Determine if this is part of an eye (finder pattern)
|
|
61
|
+
const isEye = this.isPartOfEye(row, col);
|
|
62
|
+
// Use eye-specific color if available
|
|
63
|
+
if (isEye && eyeColor) {
|
|
64
|
+
ctx.fillStyle = eyeColor;
|
|
65
|
+
}
|
|
66
|
+
// Use eye-specific radius if available, otherwise use moduleRadius
|
|
67
|
+
const cornerRadius = isEye && eyeRadius !== undefined && eyeRadius > 0
|
|
68
|
+
? moduleSize * eyeRadius
|
|
69
|
+
: moduleSize * moduleRadius;
|
|
70
|
+
// Check adjacent modules
|
|
71
|
+
const top = this.hasModule(row - 1, col);
|
|
72
|
+
const right = this.hasModule(row, col + 1);
|
|
73
|
+
const bottom = this.hasModule(row + 1, col);
|
|
74
|
+
const left = this.hasModule(row, col - 1);
|
|
75
|
+
// Determine which corners to round
|
|
76
|
+
const roundTL = !top && !left;
|
|
77
|
+
const roundTR = !top && !right;
|
|
78
|
+
const roundBR = !bottom && !right;
|
|
79
|
+
const roundBL = !bottom && !left;
|
|
80
|
+
// Small extension to eliminate gaps
|
|
81
|
+
const gap = 0.5;
|
|
82
|
+
const extendTop = top ? gap : 0;
|
|
83
|
+
const extendRight = right ? gap : 0;
|
|
84
|
+
const extendBottom = bottom ? gap : 0;
|
|
85
|
+
const extendLeft = left ? gap : 0;
|
|
86
|
+
this.drawRoundedModule(x, y, moduleSize, cornerRadius, { roundTL, roundTR, roundBR, roundBL }, { extendTop, extendRight, extendBottom, extendLeft });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Draws a single module with selectively rounded corners
|
|
93
|
+
*/
|
|
94
|
+
drawRoundedModule(x, y, size, radius, corners, extensions) {
|
|
95
|
+
const ctx = this.ctx;
|
|
96
|
+
const { roundTL, roundTR, roundBR, roundBL } = corners;
|
|
97
|
+
const { extendTop, extendRight, extendBottom, extendLeft } = extensions;
|
|
98
|
+
ctx.beginPath();
|
|
99
|
+
// Top-left corner
|
|
100
|
+
if (roundTL) {
|
|
101
|
+
ctx.moveTo(x + radius - extendLeft, y - extendTop);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
ctx.moveTo(x - extendLeft, y - extendTop);
|
|
105
|
+
}
|
|
106
|
+
// Top side and top-right corner
|
|
107
|
+
if (roundTR) {
|
|
108
|
+
ctx.lineTo(x + size - radius + extendRight, y - extendTop);
|
|
109
|
+
ctx.quadraticCurveTo(x + size + extendRight, y - extendTop, x + size + extendRight, y + radius - extendTop);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
ctx.lineTo(x + size + extendRight, y - extendTop);
|
|
113
|
+
}
|
|
114
|
+
// Right side and bottom-right corner
|
|
115
|
+
if (roundBR) {
|
|
116
|
+
ctx.lineTo(x + size + extendRight, y + size - radius + extendBottom);
|
|
117
|
+
ctx.quadraticCurveTo(x + size + extendRight, y + size + extendBottom, x + size - radius + extendRight, y + size + extendBottom);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
ctx.lineTo(x + size + extendRight, y + size + extendBottom);
|
|
121
|
+
}
|
|
122
|
+
// Bottom side and bottom-left corner
|
|
123
|
+
if (roundBL) {
|
|
124
|
+
ctx.lineTo(x + radius - extendLeft, y + size + extendBottom);
|
|
125
|
+
ctx.quadraticCurveTo(x - extendLeft, y + size + extendBottom, x - extendLeft, y + size - radius + extendBottom);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
ctx.lineTo(x - extendLeft, y + size + extendBottom);
|
|
129
|
+
}
|
|
130
|
+
// Left side and close path
|
|
131
|
+
if (roundTL) {
|
|
132
|
+
ctx.lineTo(x - extendLeft, y + radius - extendTop);
|
|
133
|
+
ctx.quadraticCurveTo(x - extendLeft, y - extendTop, x + radius - extendLeft, y - extendTop);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
ctx.lineTo(x - extendLeft, y - extendTop);
|
|
137
|
+
}
|
|
138
|
+
ctx.closePath();
|
|
139
|
+
ctx.fill();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Renders simple square modules
|
|
143
|
+
*/
|
|
144
|
+
renderSquareModules() {
|
|
145
|
+
const { moduleCount, moduleSize, padding, eyeColor } = this.options;
|
|
146
|
+
const ctx = this.ctx;
|
|
147
|
+
for (let row = 0; row < moduleCount; row++) {
|
|
148
|
+
for (let col = 0; col < moduleCount; col++) {
|
|
149
|
+
if (this.modules.get(row, col)) {
|
|
150
|
+
const x = padding + col * moduleSize;
|
|
151
|
+
const y = padding + row * moduleSize;
|
|
152
|
+
// Use eye-specific color if this is part of an eye
|
|
153
|
+
if (this.isPartOfEye(row, col) && eyeColor) {
|
|
154
|
+
ctx.fillStyle = eyeColor;
|
|
155
|
+
}
|
|
156
|
+
ctx.fillRect(x, y, moduleSize, moduleSize);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { QROptions } from '../utils/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Renderer for SVG output
|
|
4
|
+
*/
|
|
5
|
+
export declare class SVGRenderer {
|
|
6
|
+
private options;
|
|
7
|
+
private qrData;
|
|
8
|
+
private size;
|
|
9
|
+
constructor(qrData: any, options: Required<QROptions>, size: number);
|
|
10
|
+
/**
|
|
11
|
+
* Generates SVG string
|
|
12
|
+
*/
|
|
13
|
+
generate(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Checks if module exists at position
|
|
16
|
+
*/
|
|
17
|
+
private hasModule;
|
|
18
|
+
/**
|
|
19
|
+
* Checks if position is part of finder pattern (eye)
|
|
20
|
+
*/
|
|
21
|
+
private isPartOfEye;
|
|
22
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer for SVG output
|
|
3
|
+
*/
|
|
4
|
+
export class SVGRenderer {
|
|
5
|
+
constructor(qrData, options, size) {
|
|
6
|
+
this.qrData = qrData;
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.size = size;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generates SVG string
|
|
12
|
+
*/
|
|
13
|
+
generate() {
|
|
14
|
+
const { size, backgroundColor, foregroundColor, moduleRadius, eyeColor, eyeRadius } = this.options;
|
|
15
|
+
const modules = this.qrData.modules;
|
|
16
|
+
const moduleCount = modules.size;
|
|
17
|
+
const moduleSize = size / moduleCount;
|
|
18
|
+
let svg = `<?xml version="1.0" encoding="UTF-8"?>`;
|
|
19
|
+
svg += `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">`;
|
|
20
|
+
// Background
|
|
21
|
+
svg += `<rect width="${size}" height="${size}" fill="${backgroundColor}"/>`;
|
|
22
|
+
// QR modules
|
|
23
|
+
svg += `<g fill="${foregroundColor}">`;
|
|
24
|
+
for (let row = 0; row < moduleCount; row++) {
|
|
25
|
+
for (let col = 0; col < moduleCount; col++) {
|
|
26
|
+
if (this.hasModule(col, row)) {
|
|
27
|
+
const x = col * moduleSize;
|
|
28
|
+
const y = row * moduleSize;
|
|
29
|
+
// Check if this is part of an eye (finder pattern)
|
|
30
|
+
const isEye = this.isPartOfEye(col, row, moduleCount);
|
|
31
|
+
const color = isEye && eyeColor ? eyeColor : foregroundColor;
|
|
32
|
+
const radius = isEye && eyeRadius
|
|
33
|
+
? eyeRadius * moduleSize
|
|
34
|
+
: moduleRadius * moduleSize;
|
|
35
|
+
if (radius > 0) {
|
|
36
|
+
svg += `<rect x="${x}" y="${y}" width="${moduleSize}" height="${moduleSize}" rx="${radius}" fill="${color}"/>`;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
svg += `<rect x="${x}" y="${y}" width="${moduleSize}" height="${moduleSize}" fill="${color}"/>`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
svg += `</g>`;
|
|
45
|
+
svg += `</svg>`;
|
|
46
|
+
return svg;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Checks if module exists at position
|
|
50
|
+
*/
|
|
51
|
+
hasModule(x, y) {
|
|
52
|
+
const modules = this.qrData.modules;
|
|
53
|
+
if (x < 0 || x >= modules.size || y < 0 || y >= modules.size) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return modules.data[y * modules.size + x] === 1;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Checks if position is part of finder pattern (eye)
|
|
60
|
+
*/
|
|
61
|
+
isPartOfEye(x, y, size) {
|
|
62
|
+
// Top-left eye
|
|
63
|
+
if (x < 7 && y < 7)
|
|
64
|
+
return true;
|
|
65
|
+
// Top-right eye
|
|
66
|
+
if (x >= size - 7 && y < 7)
|
|
67
|
+
return true;
|
|
68
|
+
// Bottom-left eye
|
|
69
|
+
if (x < 7 && y >= size - 7)
|
|
70
|
+
return true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { VCardData, WiFiData, EmailData, SMSData, GeoData, QRDataType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Formats vCard contact data to vCard 3.0 format
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatVCard(data: VCardData): string;
|
|
6
|
+
/**
|
|
7
|
+
* Formats WiFi configuration to WiFi QR format
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatWiFi(data: WiFiData): string;
|
|
10
|
+
/**
|
|
11
|
+
* Formats email data to mailto URI
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatEmail(data: EmailData): string;
|
|
14
|
+
/**
|
|
15
|
+
* Formats SMS data to SMS URI
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatSMS(data: SMSData): string;
|
|
18
|
+
/**
|
|
19
|
+
* Formats geolocation data to geo URI
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatGeo(data: GeoData): string;
|
|
22
|
+
/**
|
|
23
|
+
* Formats QR data based on type
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatQRData(type: QRDataType, url?: string, data?: VCardData | WiFiData | EmailData | SMSData | GeoData): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats vCard contact data to vCard 3.0 format
|
|
3
|
+
*/
|
|
4
|
+
export function formatVCard(data) {
|
|
5
|
+
const lines = ['BEGIN:VCARD', 'VERSION:3.0'];
|
|
6
|
+
if (data.firstName || data.lastName) {
|
|
7
|
+
const name = `${data.lastName || ''};${data.firstName || ''};;;`;
|
|
8
|
+
lines.push(`N:${name}`);
|
|
9
|
+
lines.push(`FN:${[data.firstName, data.lastName].filter(Boolean).join(' ')}`);
|
|
10
|
+
}
|
|
11
|
+
if (data.organization) {
|
|
12
|
+
lines.push(`ORG:${data.organization}`);
|
|
13
|
+
}
|
|
14
|
+
if (data.title) {
|
|
15
|
+
lines.push(`TITLE:${data.title}`);
|
|
16
|
+
}
|
|
17
|
+
if (data.phone) {
|
|
18
|
+
lines.push(`TEL:${data.phone}`);
|
|
19
|
+
}
|
|
20
|
+
if (data.email) {
|
|
21
|
+
lines.push(`EMAIL:${data.email}`);
|
|
22
|
+
}
|
|
23
|
+
if (data.url) {
|
|
24
|
+
lines.push(`URL:${data.url}`);
|
|
25
|
+
}
|
|
26
|
+
if (data.address || data.city || data.state || data.zip || data.country) {
|
|
27
|
+
const adr = `;;${data.address || ''};${data.city || ''};${data.state || ''};${data.zip || ''};${data.country || ''}`;
|
|
28
|
+
lines.push(`ADR:${adr}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push('END:VCARD');
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Formats WiFi configuration to WiFi QR format
|
|
35
|
+
*/
|
|
36
|
+
export function formatWiFi(data) {
|
|
37
|
+
const { ssid, password = '', encryption = 'WPA', hidden = false } = data;
|
|
38
|
+
const escape = (str) => str.replace(/([\\;,:])/g, '\\$1');
|
|
39
|
+
let result = 'WIFI:';
|
|
40
|
+
result += `T:${encryption};`;
|
|
41
|
+
result += `S:${escape(ssid)};`;
|
|
42
|
+
if (password) {
|
|
43
|
+
result += `P:${escape(password)};`;
|
|
44
|
+
}
|
|
45
|
+
if (hidden) {
|
|
46
|
+
result += 'H:true;';
|
|
47
|
+
}
|
|
48
|
+
result += ';';
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Formats email data to mailto URI
|
|
53
|
+
*/
|
|
54
|
+
export function formatEmail(data) {
|
|
55
|
+
let result = `mailto:${data.email}`;
|
|
56
|
+
const params = [];
|
|
57
|
+
if (data.subject) {
|
|
58
|
+
params.push(`subject=${encodeURIComponent(data.subject)}`);
|
|
59
|
+
}
|
|
60
|
+
if (data.body) {
|
|
61
|
+
params.push(`body=${encodeURIComponent(data.body)}`);
|
|
62
|
+
}
|
|
63
|
+
if (params.length > 0) {
|
|
64
|
+
result += '?' + params.join('&');
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Formats SMS data to SMS URI
|
|
70
|
+
*/
|
|
71
|
+
export function formatSMS(data) {
|
|
72
|
+
let result = `sms:${data.phone}`;
|
|
73
|
+
if (data.message) {
|
|
74
|
+
result += `?body=${encodeURIComponent(data.message)}`;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Formats geolocation data to geo URI
|
|
80
|
+
*/
|
|
81
|
+
export function formatGeo(data) {
|
|
82
|
+
return `geo:${data.latitude},${data.longitude}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Formats QR data based on type
|
|
86
|
+
*/
|
|
87
|
+
export function formatQRData(type, url, data) {
|
|
88
|
+
switch (type) {
|
|
89
|
+
case 'url':
|
|
90
|
+
return url || '';
|
|
91
|
+
case 'text':
|
|
92
|
+
return url || '';
|
|
93
|
+
case 'vcard':
|
|
94
|
+
return data ? formatVCard(data) : '';
|
|
95
|
+
case 'wifi':
|
|
96
|
+
return data ? formatWiFi(data) : '';
|
|
97
|
+
case 'email':
|
|
98
|
+
return data ? formatEmail(data) : '';
|
|
99
|
+
case 'sms':
|
|
100
|
+
return data ? formatSMS(data) : '';
|
|
101
|
+
case 'geo':
|
|
102
|
+
return data ? formatGeo(data) : '';
|
|
103
|
+
default:
|
|
104
|
+
return url || '';
|
|
105
|
+
}
|
|
106
|
+
}
|