qr-kit 2.1.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/CHANGELOG.md +90 -0
- package/LICENSE +21 -0
- package/README.md +323 -0
- package/components/QRCodeGenerator.jsx +88 -0
- package/components/useQrCode.js +59 -0
- package/components/useQrWorker.js +81 -0
- package/index.js +40 -0
- package/package.json +71 -0
- package/qr/qr-core.js +408 -0
- package/renderers/canvas.js +96 -0
- package/renderers/svg.js +132 -0
- package/types/index.d.ts +462 -0
- package/utils/jpegQr.js +54 -0
- package/utils/layout.js +31 -0
- package/utils/link.js +154 -0
- package/utils/logo.js +189 -0
- package/utils/pdf.js +314 -0
- package/utils/poster.js +102 -0
- package/utils/raster.js +140 -0
- package/utils/url.js +20 -0
- package/worker/qr.worker.js +45 -0
package/renderers/svg.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// renderers/svg.js
|
|
2
|
+
// Pure SVG rendering. Returns strings/paths — no DOM, no side effects.
|
|
3
|
+
// Works in Node.js, Deno, browser, Edge Runtime.
|
|
4
|
+
|
|
5
|
+
import { computeLayout } from '../utils/layout.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds a compact SVG <path> d-attribute from a QR model.
|
|
9
|
+
*
|
|
10
|
+
* One <path> instead of hundreds of <rect> elements:
|
|
11
|
+
* - fewer DOM nodes → faster browser rendering
|
|
12
|
+
* - single fill/stroke CSS target → easier custom styling
|
|
13
|
+
* - animatable via CSS clip-path or stroke-dashoffset
|
|
14
|
+
*
|
|
15
|
+
* @param {import('../qr/qr-core.js').QRModel} model
|
|
16
|
+
* @param {object} [opts]
|
|
17
|
+
* @param {number} [opts.size=256] - Outer SVG size in pixels.
|
|
18
|
+
* @param {number} [opts.margin=16] - Minimum quiet-zone in pixels.
|
|
19
|
+
* @param {boolean} [opts.rounded=false] - Round module corners (r = 35% of moduleSize).
|
|
20
|
+
* @returns {string} SVG path d-attribute string.
|
|
21
|
+
*/
|
|
22
|
+
export function makeQrPath(model, { size = 256, margin = 16, rounded = false } = {}) {
|
|
23
|
+
const { moduleSize, quietLeft, quietTop } = computeLayout(model.size, size, margin);
|
|
24
|
+
const { modules, size: n } = model;
|
|
25
|
+
|
|
26
|
+
if (!rounded) {
|
|
27
|
+
// Fastest path: one M + h + v + h + z per dark module
|
|
28
|
+
let d = '';
|
|
29
|
+
const ms = moduleSize;
|
|
30
|
+
for (let y = 0; y < n; y++) {
|
|
31
|
+
for (let x = 0; x < n; x++) {
|
|
32
|
+
if (modules[y * n + x]) {
|
|
33
|
+
const px = quietLeft + x * ms;
|
|
34
|
+
const py = quietTop + y * ms;
|
|
35
|
+
d += `M${px} ${py}h${ms}v${ms}h-${ms}z`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return d;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Rounded corners: proper arc commands
|
|
43
|
+
const ms = moduleSize;
|
|
44
|
+
const r = Math.max(1, Math.round(ms * 0.35));
|
|
45
|
+
let d = '';
|
|
46
|
+
for (let y = 0; y < n; y++) {
|
|
47
|
+
for (let x = 0; x < n; x++) {
|
|
48
|
+
if (modules[y * n + x]) {
|
|
49
|
+
const px = quietLeft + x * ms;
|
|
50
|
+
const py = quietTop + y * ms;
|
|
51
|
+
d += `M${px + r},${py}`
|
|
52
|
+
+ `h${ms - 2 * r}a${r},${r} 0 0 1 ${r},${r}`
|
|
53
|
+
+ `v${ms - 2 * r}a${r},${r} 0 0 1 -${r},${r}`
|
|
54
|
+
+ `h-${ms - 2 * r}a${r},${r} 0 0 1 -${r},-${r}`
|
|
55
|
+
+ `v-${ms - 2 * r}a${r},${r} 0 0 1 ${r},-${r}z`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return d;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds two separate path strings: one for data modules, one for function modules.
|
|
64
|
+
* Lets you style finder patterns differently from data (e.g. branded colour for finders).
|
|
65
|
+
*
|
|
66
|
+
* @param {import('../qr/qr-core.js').QRModel} model
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
* @param {number} [opts.size=256]
|
|
69
|
+
* @param {number} [opts.margin=16]
|
|
70
|
+
* @returns {{ dataPath: string, functionPath: string }}
|
|
71
|
+
*/
|
|
72
|
+
export function makeQrPathSplit(model, { size = 256, margin = 16 } = {}) {
|
|
73
|
+
const { moduleSize: ms, quietLeft: ql, quietTop: qt } = computeLayout(model.size, size, margin);
|
|
74
|
+
const { modules, functionMask, size: n } = model;
|
|
75
|
+
let dataPath = '', functionPath = '';
|
|
76
|
+
|
|
77
|
+
for (let y = 0; y < n; y++) {
|
|
78
|
+
for (let x = 0; x < n; x++) {
|
|
79
|
+
const i = y * n + x;
|
|
80
|
+
if (!modules[i]) continue;
|
|
81
|
+
const px = ql + x * ms, py = qt + y * ms;
|
|
82
|
+
const seg = `M${px} ${py}h${ms}v${ms}h-${ms}z`;
|
|
83
|
+
if (functionMask[i]) functionPath += seg;
|
|
84
|
+
else dataPath += seg;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { dataPath, functionPath };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Serialises a QR model to a complete, self-contained SVG string.
|
|
92
|
+
* Ready to write to a .svg file or use as an <img src="data:image/svg+xml,..."> .
|
|
93
|
+
*
|
|
94
|
+
* @param {import('../qr/qr-core.js').QRModel} model
|
|
95
|
+
* @param {object} [opts]
|
|
96
|
+
* @param {number} [opts.size=256]
|
|
97
|
+
* @param {number} [opts.margin=16]
|
|
98
|
+
* @param {string} [opts.fg='#000']
|
|
99
|
+
* @param {string} [opts.bg='#fff']
|
|
100
|
+
* @param {string} [opts.title='QR Code']
|
|
101
|
+
* @param {boolean} [opts.rounded=false]
|
|
102
|
+
* @returns {string} Complete SVG markup.
|
|
103
|
+
*/
|
|
104
|
+
export function makeQrSvgString(model, {
|
|
105
|
+
size = 256,
|
|
106
|
+
margin = 16,
|
|
107
|
+
fg = '#000',
|
|
108
|
+
bg = '#fff',
|
|
109
|
+
title = 'QR Code',
|
|
110
|
+
rounded = false,
|
|
111
|
+
} = {}) {
|
|
112
|
+
const { outer } = computeLayout(model.size, size, margin);
|
|
113
|
+
const d = makeQrPath(model, { size, margin, rounded });
|
|
114
|
+
|
|
115
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" `
|
|
116
|
+
+ `width="${outer}" height="${outer}" viewBox="0 0 ${outer} ${outer}" `
|
|
117
|
+
+ `role="img" aria-label="${escapeAttr(title)}" shape-rendering="crispEdges">`
|
|
118
|
+
+ `<title>${escapeXml(title)}</title>`
|
|
119
|
+
+ `<rect width="100%" height="100%" fill="${escapeAttr(bg)}"/>`
|
|
120
|
+
+ `<path fill="${escapeAttr(fg)}" d="${d}"/>`
|
|
121
|
+
+ `</svg>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
function escapeXml(s) {
|
|
127
|
+
return String(s).replace(/[&<>"']/g, c => ({
|
|
128
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
129
|
+
}[c]));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function escapeAttr(s) { return escapeXml(s); }
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// types/index.d.ts
|
|
2
|
+
// TypeScript declarations for qr-kit.
|
|
3
|
+
// Organised by layer — see DECISIONS.md for architecture overview.
|
|
4
|
+
|
|
5
|
+
/* ======================================================================== */
|
|
6
|
+
/* Layer 1 — Pure computation (Node / Deno / Edge Runtime / browser) */
|
|
7
|
+
/* ======================================================================== */
|
|
8
|
+
|
|
9
|
+
/* ── qr/qr-core ─────────────────────────────────────────────────────────── */
|
|
10
|
+
|
|
11
|
+
/** Error correction level: L = ~7% recovery, M = ~15% recovery. */
|
|
12
|
+
export type EccLevel = 'L' | 'M';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The result of makeQr(). Immutable — all properties are read-only.
|
|
16
|
+
*
|
|
17
|
+
* `modules` is a flat Uint8Array of size² elements.
|
|
18
|
+
* Access: `modules[y * size + x]` or use `getModule(model, x, y)`.
|
|
19
|
+
*/
|
|
20
|
+
export interface QRModel {
|
|
21
|
+
readonly version: number;
|
|
22
|
+
readonly size: number;
|
|
23
|
+
readonly modules: Uint8Array; // flat: modules[y * size + x] === 1 → dark
|
|
24
|
+
readonly functionMask: Uint8Array; // flat: 1 = finder/timing/alignment/format
|
|
25
|
+
readonly eccLevel: EccLevel;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Thrown when the input string is too long for the requested version and ECC level.
|
|
30
|
+
* Catch by instanceof for typed error handling.
|
|
31
|
+
*/
|
|
32
|
+
export declare class QrInputTooLongError extends Error {
|
|
33
|
+
readonly name: 'QrInputTooLongError';
|
|
34
|
+
readonly byteLength: number; // actual UTF-8 byte length of the input
|
|
35
|
+
readonly maxBytes: number; // maximum the version/ECC can carry
|
|
36
|
+
readonly maxVersion: number;
|
|
37
|
+
readonly eccLevel: EccLevel;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MakeQrOptions {
|
|
41
|
+
eccLevel?: EccLevel;
|
|
42
|
+
maxVersion?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generates a QR code matrix for the given text (Byte mode, UTF-8).
|
|
47
|
+
* @throws {QrInputTooLongError} if the input exceeds the requested version/ECC capacity.
|
|
48
|
+
*/
|
|
49
|
+
export function makeQr(text: string, opts?: MakeQrOptions): QRModel;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reads one module value from a flat QRModel.
|
|
53
|
+
* Equivalent to `model.modules[y * model.size + x]`.
|
|
54
|
+
*/
|
|
55
|
+
export function getModule(model: QRModel, x: number, y: number): 0 | 1;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns true if the module at (x, y) is a function pattern module
|
|
59
|
+
* (finder, separator, timing, alignment, format/version info).
|
|
60
|
+
*/
|
|
61
|
+
export function isFunctionModule(model: QRModel, x: number, y: number): boolean;
|
|
62
|
+
|
|
63
|
+
/* ── utils/layout ───────────────────────────────────────────────────────── */
|
|
64
|
+
|
|
65
|
+
export interface LayoutResult {
|
|
66
|
+
outer: number; // actual outer canvas / SVG size (floored to integer)
|
|
67
|
+
moduleSize: number; // pixels per module (integer)
|
|
68
|
+
quietLeft: number; // left offset of first module (px)
|
|
69
|
+
quietTop: number; // top offset of first module (px)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Computes the pixel geometry for rendering a QR model.
|
|
74
|
+
* Shared by all renderers — guarantees pixel-identical output across SVG, canvas, JPEG.
|
|
75
|
+
*/
|
|
76
|
+
export function computeLayout(moduleCount: number, size: number, margin: number): LayoutResult;
|
|
77
|
+
|
|
78
|
+
/* ── utils/url ──────────────────────────────────────────────────────────── */
|
|
79
|
+
|
|
80
|
+
export interface SanitizeUrlOptions {
|
|
81
|
+
/** Params to always keep (exact match). Default: []. */
|
|
82
|
+
whitelist?: string[];
|
|
83
|
+
/**
|
|
84
|
+
* When true (default), all params NOT in whitelist are removed.
|
|
85
|
+
* When false, only known trackers (utm_*, gclid, fbclid, …) are removed.
|
|
86
|
+
*/
|
|
87
|
+
aggressive?: boolean;
|
|
88
|
+
/** Strip https:// or http:// from the result. Saves ~8 bytes in the QR. Default: true. */
|
|
89
|
+
preserveProtocol?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Returns the UTF-8 byte length of a string (equivalent to new TextEncoder().encode(s).length). */
|
|
93
|
+
export function utf8ByteLen(s: string): number;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Strips tracking parameters from a URL and optionally removes the protocol.
|
|
97
|
+
* Returns the unchanged input (trimmed) if the URL is not parseable.
|
|
98
|
+
*/
|
|
99
|
+
export function sanitizeUrlForQR(input: string, opts?: SanitizeUrlOptions): string;
|
|
100
|
+
|
|
101
|
+
/* ── utils/link ─────────────────────────────────────────────────────────── */
|
|
102
|
+
|
|
103
|
+
export type BuildQrLinkStrategy = 'trim' | 'drop' | 'error';
|
|
104
|
+
|
|
105
|
+
export interface BuildQrLinkOptions {
|
|
106
|
+
/** Base URL (with or without query string). Required. */
|
|
107
|
+
baseUrl: string;
|
|
108
|
+
/** JSON object to encode as a Base64 parameter. Required. */
|
|
109
|
+
payload: Record<string, unknown>;
|
|
110
|
+
/** Payload key whose value to shorten when the URL is too long. */
|
|
111
|
+
trimKey?: string;
|
|
112
|
+
/**
|
|
113
|
+
* What to do when `qrUrl` would exceed `qrBudgetBytes`:
|
|
114
|
+
* - 'trim' — Binary-search shorten the trimKey value.
|
|
115
|
+
* - 'drop' — Remove the trimKey from the QR payload.
|
|
116
|
+
* - 'error' — Return an error string (no URL produced).
|
|
117
|
+
*/
|
|
118
|
+
strategy?: BuildQrLinkStrategy;
|
|
119
|
+
/** Max byte length of qrUrl. Default: 134 (v6 ECC-L). */
|
|
120
|
+
qrBudgetBytes?: number;
|
|
121
|
+
/** Extra params added to fullUrl only (e.g. UTM). */
|
|
122
|
+
extraParams?: Record<string, string>;
|
|
123
|
+
/** Parameter name for the encoded payload. Default: 'd'. */
|
|
124
|
+
paramName?: string;
|
|
125
|
+
/** Strip https:// from qrUrl only. Default: false. */
|
|
126
|
+
removeProtocol?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface BuildQrLinkResult {
|
|
130
|
+
fullUrl: string; // full URL with extra params — for sharing / QR caption
|
|
131
|
+
qrUrl: string; // compact URL — encode this as the QR value
|
|
132
|
+
trimmed: boolean; // true if trimKey was shortened to fit
|
|
133
|
+
removed: boolean; // true if trimKey was removed to fit
|
|
134
|
+
error: string; // non-empty if strategy='error' and URL is too long
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function buildQrLink(opts: BuildQrLinkOptions): BuildQrLinkResult;
|
|
138
|
+
|
|
139
|
+
/* ======================================================================== */
|
|
140
|
+
/* Layer 2 — Rendering adapters */
|
|
141
|
+
/* ======================================================================== */
|
|
142
|
+
|
|
143
|
+
/* ── renderers/svg ──────────────────────────────────────────────────────── */
|
|
144
|
+
|
|
145
|
+
export interface MakeQrPathOptions {
|
|
146
|
+
size?: number;
|
|
147
|
+
margin?: number;
|
|
148
|
+
rounded?: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Returns a compact SVG path `d` attribute string.
|
|
153
|
+
* One `<path>` covers the entire QR — fewer DOM nodes, faster rendering, easier styling.
|
|
154
|
+
*/
|
|
155
|
+
export function makeQrPath(model: QRModel, opts?: MakeQrPathOptions): string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Returns two separate path strings: one for data modules and one for function modules.
|
|
159
|
+
* Lets you style finder/timing patterns differently from data modules.
|
|
160
|
+
*/
|
|
161
|
+
export function makeQrPathSplit(
|
|
162
|
+
model: QRModel,
|
|
163
|
+
opts?: { size?: number; margin?: number },
|
|
164
|
+
): { dataPath: string; functionPath: string };
|
|
165
|
+
|
|
166
|
+
export interface MakeQrSvgStringOptions {
|
|
167
|
+
size?: number;
|
|
168
|
+
margin?: number;
|
|
169
|
+
fg?: string;
|
|
170
|
+
bg?: string;
|
|
171
|
+
title?: string;
|
|
172
|
+
rounded?: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns a complete, self-contained SVG string.
|
|
177
|
+
* Write to .svg file or use as `<img src="data:image/svg+xml,...">`.
|
|
178
|
+
*/
|
|
179
|
+
export function makeQrSvgString(model: QRModel, opts?: MakeQrSvgStringOptions): string;
|
|
180
|
+
|
|
181
|
+
/* ── renderers/canvas ───────────────────────────────────────────────────── */
|
|
182
|
+
|
|
183
|
+
export interface RenderQrToCanvasOptions {
|
|
184
|
+
size?: number;
|
|
185
|
+
margin?: number;
|
|
186
|
+
fg?: string;
|
|
187
|
+
bg?: string;
|
|
188
|
+
/** Device pixel ratio / export scale. Default: 1. */
|
|
189
|
+
scale?: number;
|
|
190
|
+
/** Optional separate colour for function modules. null = use fg. */
|
|
191
|
+
fnColor?: string | null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Renders a QR model onto an existing canvas (or OffscreenCanvas).
|
|
196
|
+
* The caller owns the canvas — no DOM creation.
|
|
197
|
+
*/
|
|
198
|
+
export function renderQrToCanvas(
|
|
199
|
+
model: QRModel,
|
|
200
|
+
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
201
|
+
opts?: RenderQrToCanvasOptions,
|
|
202
|
+
): void;
|
|
203
|
+
|
|
204
|
+
/** Creates a new canvas, renders onto it, and returns it. */
|
|
205
|
+
export function makeQrCanvas(model: QRModel, opts?: RenderQrToCanvasOptions): HTMLCanvasElement;
|
|
206
|
+
|
|
207
|
+
/* ── React component ─────────────────────────────────────────────────────── */
|
|
208
|
+
|
|
209
|
+
export interface QRCodeGeneratorProps {
|
|
210
|
+
value: string;
|
|
211
|
+
size?: number;
|
|
212
|
+
margin?: number;
|
|
213
|
+
title?: string;
|
|
214
|
+
ariaLabel?: string;
|
|
215
|
+
fg?: string;
|
|
216
|
+
bg?: string;
|
|
217
|
+
eccLevel?: EccLevel;
|
|
218
|
+
maxVersion?: number;
|
|
219
|
+
/** Render with rounded module corners. Default: false. */
|
|
220
|
+
rounded?: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Called when ECC M is silently downgraded to ECC L because the input
|
|
223
|
+
* is too long for ECC M at the given maxVersion.
|
|
224
|
+
*/
|
|
225
|
+
onEccFallback?: (from: EccLevel, to: EccLevel) => void;
|
|
226
|
+
className?: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
declare const QRCodeGenerator: React.ForwardRefExoticComponent<
|
|
230
|
+
QRCodeGeneratorProps & React.RefAttributes<SVGSVGElement>
|
|
231
|
+
>;
|
|
232
|
+
export default QRCodeGenerator;
|
|
233
|
+
|
|
234
|
+
/* ── useQrCode hook ─────────────────────────────────────────────────────── */
|
|
235
|
+
|
|
236
|
+
export interface UseQrCodeOptions {
|
|
237
|
+
eccLevel?: EccLevel;
|
|
238
|
+
maxVersion?: number;
|
|
239
|
+
fallbackToL?: boolean;
|
|
240
|
+
onEccFallback?: (from: EccLevel, to: EccLevel) => void;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface UseQrCodeResult {
|
|
244
|
+
model: QRModel | null;
|
|
245
|
+
error: Error | null;
|
|
246
|
+
actualEccLevel: EccLevel | null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* React hook. Returns the raw QRModel — use with your own renderer
|
|
251
|
+
* (custom SVG, WebGL, canvas, branded finder patterns, etc.)
|
|
252
|
+
*/
|
|
253
|
+
export function useQrCode(value: string, opts?: UseQrCodeOptions): UseQrCodeResult;
|
|
254
|
+
|
|
255
|
+
/* ======================================================================== */
|
|
256
|
+
/* Layer 3 — Browser-only actions (import via deep path) */
|
|
257
|
+
/* ======================================================================== */
|
|
258
|
+
|
|
259
|
+
/* ── utils/raster ───────────────────────────────────────────────────────── */
|
|
260
|
+
|
|
261
|
+
export function svgToPngDataURL(svgEl: SVGSVGElement, scale?: number): Promise<string>;
|
|
262
|
+
export function pngDataURLtoJpegBytes(pngDataURL: string, quality?: number): Promise<Uint8Array>;
|
|
263
|
+
export function dataURLToBytes(dataUrl: string): Uint8Array;
|
|
264
|
+
export function dataURLToImage(dataUrl: string): Promise<HTMLImageElement>;
|
|
265
|
+
export function loadImage(src: string): Promise<HTMLImageElement>;
|
|
266
|
+
|
|
267
|
+
export interface DownloadQrPngOptions {
|
|
268
|
+
scale?: number;
|
|
269
|
+
fileName?: string;
|
|
270
|
+
}
|
|
271
|
+
export function downloadQrPng(svgEl: SVGSVGElement, opts?: DownloadQrPngOptions): Promise<void>;
|
|
272
|
+
|
|
273
|
+
/* ── utils/jpegQr ───────────────────────────────────────────────────────── */
|
|
274
|
+
|
|
275
|
+
export function downloadBlob(blob: Blob, fileName: string): void;
|
|
276
|
+
|
|
277
|
+
export interface DownloadQrJpegOptions {
|
|
278
|
+
value: string;
|
|
279
|
+
fileName?: string;
|
|
280
|
+
size?: number;
|
|
281
|
+
margin?: number;
|
|
282
|
+
fg?: string;
|
|
283
|
+
bg?: string;
|
|
284
|
+
eccLevel?: EccLevel;
|
|
285
|
+
maxVersion?: number;
|
|
286
|
+
}
|
|
287
|
+
export function downloadQrJpeg(opts: DownloadQrJpegOptions): Promise<void>;
|
|
288
|
+
|
|
289
|
+
/* ── utils/poster ───────────────────────────────────────────────────────── */
|
|
290
|
+
|
|
291
|
+
export interface QrPosition { x: number; y: number; size: number; }
|
|
292
|
+
|
|
293
|
+
export interface QrCompositeOptions {
|
|
294
|
+
svgEl: SVGSVGElement;
|
|
295
|
+
templateSrc: string;
|
|
296
|
+
qr?: QrPosition;
|
|
297
|
+
mime?: string;
|
|
298
|
+
quality?: number;
|
|
299
|
+
scale?: number;
|
|
300
|
+
debug?: boolean;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Composites QR onto a background image and returns a Blob (no download). */
|
|
304
|
+
export function buildQrCompositeBlob(opts: QrCompositeOptions): Promise<Blob>;
|
|
305
|
+
|
|
306
|
+
/** Composites QR onto a background image and triggers a download. */
|
|
307
|
+
export function downloadQrComposite(opts: QrCompositeOptions & { fileName?: string }): Promise<void>;
|
|
308
|
+
|
|
309
|
+
/** @deprecated Use `downloadQrComposite` instead. */
|
|
310
|
+
export const downloadLeafletImage: typeof downloadQrComposite;
|
|
311
|
+
|
|
312
|
+
/* ── utils/pdf ──────────────────────────────────────────────────────────── */
|
|
313
|
+
|
|
314
|
+
export interface QrPdfOptions {
|
|
315
|
+
svgEl: SVGSVGElement;
|
|
316
|
+
title?: string;
|
|
317
|
+
org?: string;
|
|
318
|
+
url?: string;
|
|
319
|
+
notes?: string[];
|
|
320
|
+
images?: Array<{ src: string; caption?: string }>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface PdfTemplateOptions {
|
|
324
|
+
svgEl: SVGSVGElement;
|
|
325
|
+
templateSrc: string;
|
|
326
|
+
page?: { width: number; height: number };
|
|
327
|
+
qr?: { x: number; y: number; size: number };
|
|
328
|
+
recodeBgToRgb?: boolean;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Builds a simple A4 PDF and returns raw bytes. No download. */
|
|
332
|
+
export function buildQrPdfBytes(opts: QrPdfOptions): Promise<Uint8Array>;
|
|
333
|
+
|
|
334
|
+
/** Builds a full-page background + QR PDF and returns raw bytes. No download. */
|
|
335
|
+
export function buildPdfWithTemplateBytes(opts: PdfTemplateOptions): Promise<Uint8Array>;
|
|
336
|
+
|
|
337
|
+
/** Builds and downloads a simple A4 PDF. */
|
|
338
|
+
export function downloadQrPdf(opts: QrPdfOptions & { fileName?: string }): Promise<void>;
|
|
339
|
+
|
|
340
|
+
/** Builds and downloads a full-page background + QR PDF. */
|
|
341
|
+
export function downloadPdfWithTemplateImage(opts: PdfTemplateOptions & { fileName?: string }): Promise<void>;
|
|
342
|
+
|
|
343
|
+
/** @deprecated Use `downloadQrPdf` instead. */
|
|
344
|
+
export const downloadLeafletPDF: typeof downloadQrPdf;
|
|
345
|
+
|
|
346
|
+
/* ── utils/logo ─────────────────────────────────────────────────────────── */
|
|
347
|
+
|
|
348
|
+
/** Max fraction of QR area safe to cover with ECC M logo (11%). */
|
|
349
|
+
export const LOGO_MAX_COVERAGE_ECC_M: number;
|
|
350
|
+
|
|
351
|
+
/** Max fraction of QR area safe to cover with ECC L logo (4%). */
|
|
352
|
+
export const LOGO_MAX_COVERAGE_ECC_L: number;
|
|
353
|
+
|
|
354
|
+
export interface QrLogoSvgOptions {
|
|
355
|
+
/** Outer SVG size in pixels. Default: 256 */
|
|
356
|
+
size?: number;
|
|
357
|
+
/** Quiet-zone padding in pixels. Default: 16 */
|
|
358
|
+
margin?: number;
|
|
359
|
+
/** Data module colour. Default: '#000' */
|
|
360
|
+
fg?: string;
|
|
361
|
+
/** Function module colour (finder/timing). Default: same as fg */
|
|
362
|
+
fnFg?: string;
|
|
363
|
+
/** Background colour. Default: '#fff' */
|
|
364
|
+
bg?: string;
|
|
365
|
+
/**
|
|
366
|
+
* Max fraction of QR area to cover with the logo.
|
|
367
|
+
* Default: LOGO_MAX_COVERAGE_ECC_M for ECC M, LOGO_MAX_COVERAGE_ECC_L for ECC L.
|
|
368
|
+
*/
|
|
369
|
+
maxCoverage?: number;
|
|
370
|
+
/** White padding around logo in px. Default: 4 */
|
|
371
|
+
logoPadding?: number;
|
|
372
|
+
/** Colour of logo background. Default: '#fff' */
|
|
373
|
+
logoBg?: string;
|
|
374
|
+
/** Border-radius of logo background rect in px. Default: 6 */
|
|
375
|
+
logoRadius?: number;
|
|
376
|
+
/** SVG title. Default: 'QR Code' */
|
|
377
|
+
title?: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface LogoConstraints {
|
|
381
|
+
/** Maximum safe logo size in pixels. */
|
|
382
|
+
maxLogoSize: number;
|
|
383
|
+
/** maxLogoSize + default padding (8px). Recommended `<image>` container size. */
|
|
384
|
+
paddedSize: number;
|
|
385
|
+
/** Actual coverage fraction (logo pixels² / qr pixels²). */
|
|
386
|
+
coverageFraction: number;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Builds a complete SVG string with a logo embedded in the centre.
|
|
391
|
+
* Zero-DOM: works in Node, Deno, Cloudflare Workers, and browser.
|
|
392
|
+
* @param model - Output of makeQr().
|
|
393
|
+
* @param logoDataUrl - Base64 data URL of the logo image.
|
|
394
|
+
*/
|
|
395
|
+
export function makeQrWithLogoSvg(
|
|
396
|
+
model: QRModel,
|
|
397
|
+
logoDataUrl: string,
|
|
398
|
+
opts?: QrLogoSvgOptions,
|
|
399
|
+
): string;
|
|
400
|
+
|
|
401
|
+
/** Returns the maximum safe logo size (in pixels) for a given model and output size. */
|
|
402
|
+
export function getLogoConstraints(
|
|
403
|
+
model: QRModel,
|
|
404
|
+
size: number,
|
|
405
|
+
margin: number,
|
|
406
|
+
maxCoverage?: number,
|
|
407
|
+
): LogoConstraints;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Fetches an image URL and converts it to a base64 data URL. Browser-only.
|
|
411
|
+
* @param src - URL of the logo image.
|
|
412
|
+
* @param opts - Optional AbortSignal.
|
|
413
|
+
*/
|
|
414
|
+
export function loadLogoAsDataUrl(
|
|
415
|
+
src: string,
|
|
416
|
+
opts?: { signal?: AbortSignal },
|
|
417
|
+
): Promise<string>;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Browser convenience: loads a logo from a URL and returns a complete QR SVG string.
|
|
421
|
+
* @param model - Output of makeQr().
|
|
422
|
+
* @param logoSrc - URL of the logo image.
|
|
423
|
+
* @param opts - Same options as makeQrWithLogoSvg, plus optional signal.
|
|
424
|
+
*/
|
|
425
|
+
export function buildQrWithLogoSvgAsync(
|
|
426
|
+
model: QRModel,
|
|
427
|
+
logoSrc: string,
|
|
428
|
+
opts?: QrLogoSvgOptions & { signal?: AbortSignal },
|
|
429
|
+
): Promise<string>;
|
|
430
|
+
|
|
431
|
+
/* ── components/useQrWorker ─────────────────────────────────────────────── */
|
|
432
|
+
|
|
433
|
+
export interface UseQrWorkerResult {
|
|
434
|
+
model: QRModel | null;
|
|
435
|
+
error: Error | null;
|
|
436
|
+
/** True while the Worker is computing. */
|
|
437
|
+
pending: boolean;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* React hook that computes QR codes in a background Web Worker.
|
|
442
|
+
* Use for v7–12 or real-time input animations to avoid frame drops.
|
|
443
|
+
* Requires a bundler that supports `new URL('./worker/qr.worker.js', import.meta.url)`.
|
|
444
|
+
*/
|
|
445
|
+
export function useQrWorker(
|
|
446
|
+
value: string,
|
|
447
|
+
opts?: {
|
|
448
|
+
eccLevel?: EccLevel;
|
|
449
|
+
maxVersion?: number;
|
|
450
|
+
},
|
|
451
|
+
): UseQrWorkerResult;
|
|
452
|
+
|
|
453
|
+
/* ── Signal options on async utilities ─────────────────────────────────── */
|
|
454
|
+
|
|
455
|
+
// Re-declare with signal added (augments the interfaces above)
|
|
456
|
+
export function buildQrCompositeBlob(
|
|
457
|
+
opts: QrCompositeOptions & { signal?: AbortSignal },
|
|
458
|
+
): Promise<Blob>;
|
|
459
|
+
|
|
460
|
+
export function buildPdfWithTemplateBytes(
|
|
461
|
+
opts: PdfTemplateOptions & { signal?: AbortSignal },
|
|
462
|
+
): Promise<Uint8Array>;
|
package/utils/jpegQr.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// utils/jpegQr.js
|
|
2
|
+
// Downloads a QR code as a JPEG file directly from the canvas — no fetch, no data: URLs.
|
|
3
|
+
// Uses the canvas renderer for pixel-identical output to QRCodeGenerator (SVG).
|
|
4
|
+
|
|
5
|
+
import { makeQr } from '../qr/qr-core.js';
|
|
6
|
+
import { renderQrToCanvas } from '../renderers/canvas.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Triggers a file download from a Blob using a temporary object URL.
|
|
10
|
+
* @param {Blob} blob
|
|
11
|
+
* @param {string} fileName
|
|
12
|
+
*/
|
|
13
|
+
export function downloadBlob(blob, fileName) {
|
|
14
|
+
const url = URL.createObjectURL(blob);
|
|
15
|
+
const a = document.createElement('a');
|
|
16
|
+
a.href = url; a.download = fileName;
|
|
17
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
18
|
+
URL.revokeObjectURL(url);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Renders a QR code to canvas and downloads it as a JPEG file.
|
|
23
|
+
* Guaranteed pixel-identical layout to QRCodeGenerator (SVG) via shared computeLayout().
|
|
24
|
+
*
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {string} opts.value
|
|
27
|
+
* @param {string} [opts.fileName='qr.jpeg']
|
|
28
|
+
* @param {number} [opts.size=192] - Outer canvas size in pixels.
|
|
29
|
+
* @param {number} [opts.margin=12] - Minimum quiet-zone padding in pixels.
|
|
30
|
+
* @param {string} [opts.fg='#000']
|
|
31
|
+
* @param {string} [opts.bg='#fff']
|
|
32
|
+
* @param {'L'|'M'} [opts.eccLevel='L']
|
|
33
|
+
* @param {number} [opts.maxVersion=12]
|
|
34
|
+
* @returns {Promise<void>}
|
|
35
|
+
*/
|
|
36
|
+
export async function downloadQrJpeg({
|
|
37
|
+
value,
|
|
38
|
+
fileName = 'qr.jpeg',
|
|
39
|
+
size = 192,
|
|
40
|
+
margin = 12,
|
|
41
|
+
fg = '#000',
|
|
42
|
+
bg = '#fff',
|
|
43
|
+
eccLevel = 'L',
|
|
44
|
+
maxVersion = 12,
|
|
45
|
+
}) {
|
|
46
|
+
const model = makeQr(value, { eccLevel, maxVersion });
|
|
47
|
+
const canvas = document.createElement('canvas');
|
|
48
|
+
renderQrToCanvas(model, canvas, { size, margin, fg, bg });
|
|
49
|
+
|
|
50
|
+
const blob = await new Promise((resolve, reject) =>
|
|
51
|
+
canvas.toBlob(b => (b ? resolve(b) : reject(new Error('canvas.toBlob failed'))), 'image/jpeg'),
|
|
52
|
+
);
|
|
53
|
+
downloadBlob(blob, fileName);
|
|
54
|
+
}
|
package/utils/layout.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// utils/layout.js
|
|
2
|
+
// Shared pixel-layout calculation for QR rendering.
|
|
3
|
+
// Used by QRCodeGenerator (SVG) and downloadQrJpeg (canvas) to guarantee identical output.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Computes the pixel geometry needed to render a QR matrix inside a fixed outer size.
|
|
7
|
+
*
|
|
8
|
+
* Algorithm:
|
|
9
|
+
* 1. Reserve `margin` pixels on each side as the minimum quiet zone.
|
|
10
|
+
* 2. Fit as many whole-pixel modules as possible in the remaining space.
|
|
11
|
+
* 3. Distribute any leftover pixels evenly, centering the grid.
|
|
12
|
+
*
|
|
13
|
+
* @param {number} moduleCount - Matrix side length (model.size = 17 + 4 * version).
|
|
14
|
+
* @param {number} size - Desired outer size in pixels.
|
|
15
|
+
* @param {number} margin - Minimum quiet-zone padding in pixels.
|
|
16
|
+
* @returns {{ outer: number, moduleSize: number, quietLeft: number, quietTop: number }}
|
|
17
|
+
*/
|
|
18
|
+
export function computeLayout(moduleCount, size, margin) {
|
|
19
|
+
const outer = Math.max(1, Math.floor(size));
|
|
20
|
+
const minQuiet = Math.max(0, Math.floor(margin));
|
|
21
|
+
const usable = outer - 2 * minQuiet;
|
|
22
|
+
const moduleSize = Math.max(1, Math.floor(usable / moduleCount));
|
|
23
|
+
const inner = moduleSize * moduleCount;
|
|
24
|
+
const extra = Math.max(0, outer - (inner + 2 * minQuiet));
|
|
25
|
+
|
|
26
|
+
// Centre the grid; any remaining pixel goes to the right/bottom side implicitly
|
|
27
|
+
const quietLeft = minQuiet + Math.floor(extra / 2);
|
|
28
|
+
const quietTop = minQuiet + Math.floor(extra / 2);
|
|
29
|
+
|
|
30
|
+
return { outer, moduleSize, quietLeft, quietTop };
|
|
31
|
+
}
|