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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [2.1.0] — Logo overlay, Web Worker, AbortSignal
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
**Logo overlay** (`utils/logo.js`)
|
|
15
|
+
- `makeQrWithLogoSvg(model, logoDataUrl, opts)` — zero-DOM SVG with embedded logo. Works in Node, Deno, Cloudflare Workers, Web Workers, and browser. Function modules always rendered above the logo layer for scan reliability.
|
|
16
|
+
- `getLogoConstraints(model, size, margin, maxCoverage)` — returns `maxLogoSize`, `paddedSize`, and `coverageFraction` so you can pre-validate a logo before building the SVG.
|
|
17
|
+
- `loadLogoAsDataUrl(src, { signal })` — browser helper: URL → base64 data URL.
|
|
18
|
+
- `buildQrWithLogoSvgAsync(model, src, opts)` — browser convenience that loads logo and builds SVG in one call.
|
|
19
|
+
- `LOGO_MAX_COVERAGE_ECC_M` (0.11) and `LOGO_MAX_COVERAGE_ECC_L` (0.04) — exported constants for coverage budget.
|
|
20
|
+
|
|
21
|
+
**Web Worker** (`worker/qr.worker.js`)
|
|
22
|
+
- `qr.worker.js` — Worker entry point that runs `makeQr` off the main thread. Transfers `Uint8Array` buffers via `postMessage` to avoid memory copies.
|
|
23
|
+
- `useQrWorker(value, opts)` React hook — async QR computation in a shared background Worker. Use for v7–12 or real-time input animations to prevent frame drops.
|
|
24
|
+
|
|
25
|
+
**AbortSignal support**
|
|
26
|
+
- `buildQrCompositeBlob({ signal })` — cancellable poster composition.
|
|
27
|
+
- `buildPdfWithTemplateBytes({ signal })` — cancellable PDF generation.
|
|
28
|
+
- `loadLogoAsDataUrl(src, { signal })` — cancellable logo fetch.
|
|
29
|
+
- `loadImage(src, { signal })` (raster.js) — AbortSignal propagated to image loading.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- `package.json` bumped to v2.1.0.
|
|
33
|
+
- `scripts/size.js` updated to include new modules.
|
|
34
|
+
- `index.js` exports all new public APIs.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## [1.0.0] — Initial release
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
**Core**
|
|
43
|
+
- `makeQr(text, opts)` — pure JS QR engine, Byte mode, versions 1–12, ECC L/M
|
|
44
|
+
- Reed-Solomon error correction with GF(256) exp/log tables
|
|
45
|
+
- All 8 mask patterns evaluated; lowest-penalty mask selected
|
|
46
|
+
- Correct remainder bits, dark module, version info (v7+), format info
|
|
47
|
+
|
|
48
|
+
**React component**
|
|
49
|
+
- `QRCodeGenerator` — renders an inline SVG via `React.forwardRef`
|
|
50
|
+
- `onEccFallback` prop — notifies caller when ECC M is silently downgraded to L
|
|
51
|
+
- Accessibility: `role="img"`, `aria-label`, `<title>`, `shapeRendering="crispEdges"`
|
|
52
|
+
|
|
53
|
+
**Layout**
|
|
54
|
+
- `computeLayout(moduleCount, size, margin)` — shared pixel geometry for SVG and canvas renderers; guarantees pixel-identical output
|
|
55
|
+
|
|
56
|
+
**Raster utilities** (`utils/raster.js`)
|
|
57
|
+
- `svgToPngDataURL(svgEl, scale)` — SVG → PNG data URL
|
|
58
|
+
- `pngDataURLtoJpegBytes(pngDataURL, quality)` — PNG data URL → JPEG bytes
|
|
59
|
+
- `dataURLToBytes(dataUrl)` — data URL → Uint8Array
|
|
60
|
+
- `dataURLToImage(dataUrl)` — data URL → HTMLImageElement
|
|
61
|
+
- `loadImage(src)` — URL → HTMLImageElement (crossOrigin: anonymous)
|
|
62
|
+
- `imageURLtoJpegBytes(src, quality)` — URL → JPEG bytes
|
|
63
|
+
- `downloadQrPng(svgEl, opts)` — downloads QR as PNG
|
|
64
|
+
|
|
65
|
+
**JPEG export** (`utils/jpegQr.js`)
|
|
66
|
+
- `downloadQrJpeg(opts)` — renders QR to canvas and downloads as JPEG
|
|
67
|
+
- `downloadBlob(blob, fileName)` — generic Blob download helper
|
|
68
|
+
|
|
69
|
+
**Poster / image composite** (`utils/poster.js`)
|
|
70
|
+
- `downloadQrComposite(opts)` — composites QR onto a background image and downloads
|
|
71
|
+
|
|
72
|
+
**PDF export** (`utils/pdf.js`)
|
|
73
|
+
- `downloadPdfWithTemplateImage(opts)` — full-page background + QR overlay PDF
|
|
74
|
+
- `downloadQrPdf(opts)` — simple A4 PDF with title, text, and QR
|
|
75
|
+
- Non-JPEG backgrounds (PNG, WebP, CMYK) auto-converted to RGB JPEG
|
|
76
|
+
|
|
77
|
+
**URL utilities** (`utils/url.js`)
|
|
78
|
+
- `sanitizeUrlForQR(input, opts)` — strips tracking params; optionally removes protocol
|
|
79
|
+
- `utf8ByteLen(s)` — UTF-8 byte length of a string
|
|
80
|
+
|
|
81
|
+
**Link builder** (`utils/link.js`)
|
|
82
|
+
- `buildQrLink(opts)` — encodes JSON payload as Base64 in a URL; binary-search trim to fit QR byte budget
|
|
83
|
+
- Strategies: `'trim'` | `'drop'` | `'error'`
|
|
84
|
+
- Guard for non-string `trimKey` values
|
|
85
|
+
|
|
86
|
+
**Tests** (zero dependencies, Node.js only)
|
|
87
|
+
- 47 tests across `qr-core`, `layout`, `url`, `link`
|
|
88
|
+
|
|
89
|
+
**TypeScript**
|
|
90
|
+
- Full declarations in `types/index.d.ts` for all public APIs
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yaroslav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🎯 qr-kit
|
|
4
|
+
|
|
5
|
+
**Complete QR code toolkit. Zero dependencies. 5.4 kB core.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/qr-kit)
|
|
8
|
+
[](https://bundlephobia.com/package/qr-kit)
|
|
9
|
+
[](tests/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
**[🛝 Live Playground](playground/index.html)** · **[📖 Docs](#installation)** · **[🎨 Examples](#quick-start)**
|
|
13
|
+
|
|
14
|
+
<img src="docs/hero-demo.gif" width="600" alt="QR code with logo overlay demo" />
|
|
15
|
+
|
|
16
|
+
*Generate QR codes with logos, export to PDF, optimize URLs — all in the browser, zero dependencies.*
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## ✨ Why this library?
|
|
23
|
+
|
|
24
|
+
Every QR library on npm either:
|
|
25
|
+
- 📦 Pulls in 10+ dependencies
|
|
26
|
+
- 🐌 Bundles 150+ kB of minified code
|
|
27
|
+
- 🔨 Requires a build step (TypeScript, Babel, Webpack)
|
|
28
|
+
- 🌐 Works only in Node.js OR only in browser
|
|
29
|
+
|
|
30
|
+
**This library:**
|
|
31
|
+
- ✅ Zero dependencies — literally `"dependencies": {}`
|
|
32
|
+
- ✅ 5.4 kB gzipped core — smaller than most images on your page
|
|
33
|
+
- ✅ Ships as ES modules — use directly, no build step
|
|
34
|
+
- ✅ Works everywhere — Node, Deno, Cloudflare Workers, browser, Web Worker
|
|
35
|
+
- ✅ Logo overlay with ECC budget enforcement
|
|
36
|
+
- ✅ PDF export, poster generation, link optimization
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🚀 Quick start
|
|
41
|
+
|
|
42
|
+
### Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install qr-kit
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Basic QR in React (3 lines)
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
import QRCodeGenerator from 'qr-kit';
|
|
52
|
+
|
|
53
|
+
export default () => <QRCodeGenerator value="https://example.com" />;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Output:** Crisp SVG, single `<path>` element (not 500 `<rect>`), accessible, scales infinitely.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🎨 Logo overlay — the killer feature
|
|
61
|
+
|
|
62
|
+
```jsx
|
|
63
|
+
import { makeQr } from 'qr-kit';
|
|
64
|
+
import { buildQrWithLogoSvgAsync } from 'qr-kit/utils/logo';
|
|
65
|
+
|
|
66
|
+
const model = makeQr('https://example.com', { eccLevel: 'M' });
|
|
67
|
+
const svg = await buildQrWithLogoSvgAsync(model, '/logo.svg', {
|
|
68
|
+
size: 400,
|
|
69
|
+
maxCoverage: 0.11, // 11% of QR area — safe for ECC M
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
document.body.innerHTML = svg; // self-contained SVG string
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**How it works:**
|
|
76
|
+
- QR rendered in 3 layers: data → logo → finder patterns (always on top)
|
|
77
|
+
- ECC M budget: 11% coverage leaves 4% safety margin
|
|
78
|
+
- Zero-DOM implementation — works in Node.js, Cloudflare Workers, anywhere
|
|
79
|
+
|
|
80
|
+
<div align="center">
|
|
81
|
+
<img src="docs/logo-overlay-example.png" width="300" alt="QR with company logo" />
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 📦 Tiny bundle, huge features
|
|
87
|
+
|
|
88
|
+
| Feature | Size (gzip) | Description |
|
|
89
|
+
|---------|-------------|-------------|
|
|
90
|
+
| **qr-core** | 4.7 kB | Pure QR engine — works everywhere |
|
|
91
|
+
| **React component** | +1.1 kB | SVG component with `forwardRef` |
|
|
92
|
+
| **Logo overlay** | +3.0 kB | Embed logos with ECC enforcement |
|
|
93
|
+
| **PDF export** | +4.4 kB | Generate PDFs in browser |
|
|
94
|
+
| **Link optimizer** | +2.1 kB | Fit URLs in QR byte budget |
|
|
95
|
+
| **Web Worker** | +0.8 kB | Off-thread for v10-12 |
|
|
96
|
+
|
|
97
|
+
**Total library:** 26.7 kB gzip
|
|
98
|
+
**Core only:** 5.4 kB gzip (qr-core + layout + url utilities)
|
|
99
|
+
|
|
100
|
+
Tree-shake what you don't need. Import only what you use.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 🔥 More examples
|
|
105
|
+
|
|
106
|
+
### Export to PNG/JPEG/PDF
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { downloadQrPng } from 'qr-kit/utils/raster';
|
|
110
|
+
import { downloadQrJpeg } from 'qr-kit/utils/jpegQr';
|
|
111
|
+
import { downloadQrPdf } from 'qr-kit/utils/pdf';
|
|
112
|
+
|
|
113
|
+
// PNG at 3× scale
|
|
114
|
+
await downloadQrPng(svgRef.current, { scale: 3 });
|
|
115
|
+
|
|
116
|
+
// JPEG (direct from canvas, no SVG round-trip)
|
|
117
|
+
await downloadQrJpeg({ value: 'https://example.com', size: 300 });
|
|
118
|
+
|
|
119
|
+
// PDF with title and metadata
|
|
120
|
+
await downloadQrPdf({
|
|
121
|
+
svgEl: svgRef.current,
|
|
122
|
+
title: 'Event QR Code',
|
|
123
|
+
org: 'Your Company',
|
|
124
|
+
url: 'https://example.com',
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Optimize URLs for QR (Link Builder)
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { buildQrLink } from 'qr-kit/utils/link';
|
|
132
|
+
|
|
133
|
+
const { qrUrl, fullUrl, trimmed } = buildQrLink({
|
|
134
|
+
baseUrl: 'https://example.com/app',
|
|
135
|
+
payload: { userId: 'abc123', campaign: 'summer2024promo' },
|
|
136
|
+
budget: 120, // target bytes
|
|
137
|
+
strategy: 'trim', // shorten campaign if needed
|
|
138
|
+
trimKey: 'campaign',
|
|
139
|
+
removeProtocol: true, // saves 8 bytes
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// qrUrl: "example.com/app?d={...shortened...}"
|
|
143
|
+
// fullUrl: "https://example.com/app?d={...full payload...}"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Use case:** Generate short QR codes, redirect to full URLs on server.
|
|
147
|
+
|
|
148
|
+
### Composite QR onto background image (Poster)
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { downloadQrComposite } from 'qr-kit/utils/poster';
|
|
152
|
+
|
|
153
|
+
await downloadQrComposite({
|
|
154
|
+
svgEl: qrRef.current,
|
|
155
|
+
templateSrc: '/event-poster.jpg',
|
|
156
|
+
qr: { x: 50, y: 400, size: 200 }, // position in px
|
|
157
|
+
scale: 2, // 2× for print
|
|
158
|
+
fileName: 'poster.jpg',
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
<div align="center">
|
|
163
|
+
<img src="docs/poster-example.png" width="400" alt="QR on event poster" />
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
### Branded QR (custom colors for finder patterns)
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
import { makeQrSvgString } from 'qr-kit/renderers/svg';
|
|
170
|
+
|
|
171
|
+
const svg = makeQrSvgString(model, {
|
|
172
|
+
fg: '#000', // data modules
|
|
173
|
+
fnColor: '#f97316', // finder patterns (3 corners)
|
|
174
|
+
bg: '#fff',
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Web Worker (no jank on v10-12)
|
|
179
|
+
|
|
180
|
+
```jsx
|
|
181
|
+
import { useQrWorker } from 'qr-kit';
|
|
182
|
+
|
|
183
|
+
function MyQr({ url }) {
|
|
184
|
+
const { model, error, pending } = useQrWorker(url, { maxVersion: 10 });
|
|
185
|
+
|
|
186
|
+
if (pending) return <Spinner />;
|
|
187
|
+
return <canvas ref={useQrCanvas(model)} />;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**When to use:**
|
|
192
|
+
- `useQrCode` (sync) — fast, zero overhead, v1-6
|
|
193
|
+
- `useQrWorker` (async) — prevents jank on v7-12 or real-time input
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 🎮 Interactive playground
|
|
198
|
+
|
|
199
|
+
Open [`playground/index.html`](playground/index.html) in your browser — no build, no server.
|
|
200
|
+
|
|
201
|
+
**Features:**
|
|
202
|
+
- 4 render modes: Basic, Rounded, Branded, Logo
|
|
203
|
+
- Link Builder with real-time byte counting
|
|
204
|
+
- PDF Export with metadata
|
|
205
|
+
- All export formats (SVG, PNG, JPEG)
|
|
206
|
+
- Live code examples
|
|
207
|
+
|
|
208
|
+
**Works offline.** Single HTML file, 53 kB.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 🏗️ Architecture
|
|
213
|
+
|
|
214
|
+
Three layers, zero coupling:
|
|
215
|
+
|
|
216
|
+
**Layer 1 — Pure computation** (Node, Deno, Workers, browser)
|
|
217
|
+
```js
|
|
218
|
+
import { makeQr } from 'qr-kit/qr/qr-core';
|
|
219
|
+
// → { modules: Uint8Array, functionMask: Uint8Array, version, size, eccLevel }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Layer 2 — Rendering adapters** (return data, no side effects)
|
|
223
|
+
```js
|
|
224
|
+
import { makeQrSvgString } from 'qr-kit/renderers/svg';
|
|
225
|
+
import { renderQrToCanvas } from 'qr-kit/renderers/canvas';
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Layer 3 — Browser actions** (downloads, side effects)
|
|
229
|
+
```js
|
|
230
|
+
import { downloadQrPng } from 'qr-kit/utils/raster';
|
|
231
|
+
// Internally calls Layer 2 → triggers download
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Design principle:** Functions return data, not perform actions.
|
|
235
|
+
Every `download*` function has a `build*Bytes` / `build*Blob` primitive.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 📊 Bundle size comparison
|
|
240
|
+
|
|
241
|
+
| Library | Size (gzip) | Dependencies | Logo overlay |
|
|
242
|
+
|---------|-------------|--------------|--------------|
|
|
243
|
+
| **qr-kit** | **5.4 kB** | **0** | ✅ |
|
|
244
|
+
| qrcode | 29.8 kB | 4 | ❌ |
|
|
245
|
+
| qr-code-generator | 7.2 kB | 0 | ❌ |
|
|
246
|
+
| node-qrcode | 52.1 kB | 6 | ❌ |
|
|
247
|
+
|
|
248
|
+
*Core bundle size. Full library with all utilities: 26.7 kB gzip.*
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 🧪 Testing
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npm test # Run 109 tests
|
|
256
|
+
npm run size # Bundle size report
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Test coverage:**
|
|
260
|
+
- ✅ 20 unit tests (qr-core, layout, svg, url, link, logo)
|
|
261
|
+
- ✅ 23 logo overlay tests (ECC budget, constraints, rendering)
|
|
262
|
+
- ✅ 8 property-based tests (500-10K random inputs)
|
|
263
|
+
- ✅ 8 golden file tests (regression against known outputs)
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 📚 API Reference
|
|
268
|
+
|
|
269
|
+
Full API docs: [`types/index.d.ts`](types/index.d.ts)
|
|
270
|
+
|
|
271
|
+
**Core:**
|
|
272
|
+
- `makeQr(text, opts)` — Generate QR model
|
|
273
|
+
- `getModule(model, x, y)` — Read module at position
|
|
274
|
+
- `isFunctionModule(model, x, y)` — Check if module is functional
|
|
275
|
+
|
|
276
|
+
**Renderers:**
|
|
277
|
+
- `makeQrPath(model, opts)` — SVG `<path>` d-attribute
|
|
278
|
+
- `makeQrPathSplit(model, opts)` — Separate data/function paths
|
|
279
|
+
- `makeQrSvgString(model, opts)` — Complete SVG markup
|
|
280
|
+
- `renderQrToCanvas(model, canvas, opts)` — Draw on canvas
|
|
281
|
+
|
|
282
|
+
**Logo overlay:**
|
|
283
|
+
- `makeQrWithLogoSvg(model, logoDataUrl, opts)` — SVG with logo
|
|
284
|
+
- `getLogoConstraints(model, size, margin)` — Max safe logo size
|
|
285
|
+
- `buildQrWithLogoSvgAsync(model, logoSrc, opts)` — Browser helper
|
|
286
|
+
|
|
287
|
+
**Exports:**
|
|
288
|
+
- `downloadQrPng(svgEl, opts)` — PNG export
|
|
289
|
+
- `downloadQrJpeg(opts)` — JPEG export (canvas-based)
|
|
290
|
+
- `downloadQrPdf(opts)` — PDF with QR + metadata
|
|
291
|
+
- `downloadQrComposite(opts)` — QR on background image
|
|
292
|
+
|
|
293
|
+
**Utilities:**
|
|
294
|
+
- `buildQrLink(opts)` — Optimize URL for QR byte budget
|
|
295
|
+
- `sanitizeUrlForQR(url, opts)` — Strip tracking params
|
|
296
|
+
- `utf8ByteLen(str)` — Count UTF-8 bytes
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 🤝 Contributing
|
|
301
|
+
|
|
302
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
|
303
|
+
|
|
304
|
+
**Philosophy:**
|
|
305
|
+
- Zero dependencies, always
|
|
306
|
+
- No build step — ship source as ES modules
|
|
307
|
+
- Browser-only features are opt-in (deep imports)
|
|
308
|
+
- Every function is testable in Node.js
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 📝 License
|
|
313
|
+
|
|
314
|
+
MIT © [Your Name](https://github.com/your-org)
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 🌟 Show your support
|
|
319
|
+
|
|
320
|
+
If this library saved you time, give it a ⭐ on [GitHub](https://github.com/your-org/qr-code)!
|
|
321
|
+
|
|
322
|
+
**Built with ❤️ to prove that npm packages don't need dependencies to be powerful.**
|
|
323
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// components/QRCodeGenerator.jsx
|
|
2
|
+
// React SVG component that renders a QR code using a single <path>.
|
|
3
|
+
// Uses React.forwardRef — pass ref={svgRef} to access the <svg> element
|
|
4
|
+
// for PNG/JPEG/PDF export via utils/raster or utils/poster.
|
|
5
|
+
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { useQrCode } from './useQrCode.js';
|
|
8
|
+
import { makeQrPath } from '../renderers/svg.js';
|
|
9
|
+
import { computeLayout } from '../utils/layout.js';
|
|
10
|
+
|
|
11
|
+
const DEFAULTS = { size: 256, margin: 16, fg: '#000', bg: '#fff', ecc: 'M', maxV: 6 };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders a QR code as an inline SVG.
|
|
15
|
+
*
|
|
16
|
+
* Uses a single <path> element instead of hundreds of <rect> elements:
|
|
17
|
+
* - Fewer DOM nodes, faster rendering
|
|
18
|
+
* - Single fill target — style with one CSS rule
|
|
19
|
+
* - Animatable
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const ref = useRef(null);
|
|
23
|
+
* <QRCodeGenerator ref={ref} value="https://example.com" rounded />
|
|
24
|
+
* await downloadQrPng(ref.current);
|
|
25
|
+
*/
|
|
26
|
+
const QRCodeGenerator = React.forwardRef(function QRCodeGenerator(props, ref) {
|
|
27
|
+
const {
|
|
28
|
+
value,
|
|
29
|
+
size = DEFAULTS.size,
|
|
30
|
+
margin = DEFAULTS.margin,
|
|
31
|
+
title = 'QR Code',
|
|
32
|
+
ariaLabel = 'QR code',
|
|
33
|
+
fg = DEFAULTS.fg,
|
|
34
|
+
bg = DEFAULTS.bg,
|
|
35
|
+
eccLevel = DEFAULTS.ecc,
|
|
36
|
+
maxVersion = DEFAULTS.maxV,
|
|
37
|
+
rounded = false,
|
|
38
|
+
onEccFallback,
|
|
39
|
+
className,
|
|
40
|
+
} = props;
|
|
41
|
+
|
|
42
|
+
const { model, error } = useQrCode(value, {
|
|
43
|
+
eccLevel, maxVersion, fallbackToL: true, onEccFallback,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (error) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
role="alert"
|
|
50
|
+
style={{
|
|
51
|
+
color: '#b91c1c', fontFamily: 'monospace',
|
|
52
|
+
border: '1px solid #fecaca', borderRadius: 8, padding: 12,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
QR error: {error.message}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!model) return null;
|
|
61
|
+
|
|
62
|
+
const { outer } = computeLayout(model.size, size, margin);
|
|
63
|
+
const d = makeQrPath(model, { size, margin, rounded });
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<svg
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={className}
|
|
69
|
+
width={outer}
|
|
70
|
+
height={outer}
|
|
71
|
+
viewBox={`0 0 ${outer} ${outer}`}
|
|
72
|
+
role="img"
|
|
73
|
+
aria-label={ariaLabel}
|
|
74
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
75
|
+
shapeRendering="crispEdges"
|
|
76
|
+
data-version={model.version}
|
|
77
|
+
data-modules={model.size}
|
|
78
|
+
data-ecc={model.eccLevel}
|
|
79
|
+
>
|
|
80
|
+
<title>{title}</title>
|
|
81
|
+
<rect width="100%" height="100%" fill={bg} />
|
|
82
|
+
<path fill={fg} d={d} />
|
|
83
|
+
</svg>
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
QRCodeGenerator.displayName = 'QRCodeGenerator';
|
|
88
|
+
export default QRCodeGenerator;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// components/useQrCode.js
|
|
2
|
+
// React hook for direct access to the QR model.
|
|
3
|
+
// Use this when you need to render QR with a custom renderer
|
|
4
|
+
// (WebGL, canvas, custom SVG, branded finder patterns, etc.)
|
|
5
|
+
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { makeQr, QrInputTooLongError } from '../qr/qr-core.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Computes a QR model for the given value and options.
|
|
11
|
+
* Returns the model directly — no rendering, no DOM.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} value
|
|
14
|
+
* @param {object} [opts]
|
|
15
|
+
* @param {'L'|'M'} [opts.eccLevel='M']
|
|
16
|
+
* @param {number} [opts.maxVersion=6]
|
|
17
|
+
* @param {boolean} [opts.fallbackToL=true] - Auto-retry with ECC L if M is too long.
|
|
18
|
+
* @param {function} [opts.onEccFallback] - Called when ECC is downgraded.
|
|
19
|
+
* @returns {{ model: QRModel|null, error: Error|null, actualEccLevel: string|null }}
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* function MyQr({ url }) {
|
|
23
|
+
* const { model, error } = useQrCode(url, { eccLevel: 'L' });
|
|
24
|
+
* if (error) return <p>Error: {error.message}</p>;
|
|
25
|
+
* if (!model) return null;
|
|
26
|
+
* // render model.modules however you want
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
export function useQrCode(value, {
|
|
30
|
+
eccLevel = 'M',
|
|
31
|
+
maxVersion = 6,
|
|
32
|
+
fallbackToL = true,
|
|
33
|
+
onEccFallback,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const onEccFallbackRef = React.useRef(onEccFallback);
|
|
36
|
+
React.useLayoutEffect(() => { onEccFallbackRef.current = onEccFallback; }, [onEccFallback]);
|
|
37
|
+
|
|
38
|
+
return React.useMemo(() => {
|
|
39
|
+
if (value == null || value === '') {
|
|
40
|
+
return { model: null, error: new Error('No value provided'), actualEccLevel: null };
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const model = makeQr(value, { eccLevel, maxVersion });
|
|
44
|
+
return { model, error: null, actualEccLevel: eccLevel };
|
|
45
|
+
} catch (e1) {
|
|
46
|
+
if (fallbackToL && eccLevel === 'M') {
|
|
47
|
+
try {
|
|
48
|
+
const model = makeQr(value, { eccLevel: 'L', maxVersion });
|
|
49
|
+
// Use setTimeout to avoid calling during render
|
|
50
|
+
setTimeout(() => onEccFallbackRef.current?.('M', 'L'), 0);
|
|
51
|
+
return { model, error: null, actualEccLevel: 'L' };
|
|
52
|
+
} catch (e2) {
|
|
53
|
+
return { model: null, error: e2, actualEccLevel: null };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { model: null, error: e1, actualEccLevel: null };
|
|
57
|
+
}
|
|
58
|
+
}, [value, eccLevel, maxVersion, fallbackToL]);
|
|
59
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// components/useQrWorker.js
|
|
2
|
+
// React hook that computes QR codes in a Web Worker to avoid main-thread jank.
|
|
3
|
+
//
|
|
4
|
+
// When to use vs useQrCode:
|
|
5
|
+
// - useQrCode: synchronous, zero overhead — use for v1-6 and non-animated UIs
|
|
6
|
+
// - useQrWorker: async, off-thread — use when animating input or targeting v7-12
|
|
7
|
+
// on low-end mobile where 10-15ms CPU time causes dropped frames
|
|
8
|
+
//
|
|
9
|
+
// @example
|
|
10
|
+
// function MyQr({ url }) {
|
|
11
|
+
// const { model, error, pending } = useQrWorker(url, { eccLevel: 'L', maxVersion: 10 });
|
|
12
|
+
// if (pending) return <Skeleton />;
|
|
13
|
+
// if (error) return <p>{error.message}</p>;
|
|
14
|
+
// return <canvas ref={canvasRef} />; // render model in useEffect
|
|
15
|
+
// }
|
|
16
|
+
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
|
|
19
|
+
let _worker = null;
|
|
20
|
+
let _callbacks = new Map();
|
|
21
|
+
let _nextId = 1;
|
|
22
|
+
|
|
23
|
+
/** Lazy-initialised shared worker instance. */
|
|
24
|
+
function getWorker() {
|
|
25
|
+
if (!_worker) {
|
|
26
|
+
_worker = new Worker(new URL('../worker/qr.worker.js', import.meta.url), { type: 'module' });
|
|
27
|
+
_worker.onmessage = ({ data }) => {
|
|
28
|
+
const cb = _callbacks.get(data.id);
|
|
29
|
+
if (cb) { _callbacks.delete(data.id); cb(data); }
|
|
30
|
+
};
|
|
31
|
+
_worker.onerror = (e) => {
|
|
32
|
+
// Broadcast error to all pending callbacks
|
|
33
|
+
for (const [id, cb] of _callbacks) { _callbacks.delete(id); cb({ id, error: String(e) }); }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return _worker;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Computes a QR model off the main thread via a shared Web Worker.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} value
|
|
43
|
+
* @param {object} [opts]
|
|
44
|
+
* @param {'L'|'M'} [opts.eccLevel='M']
|
|
45
|
+
* @param {number} [opts.maxVersion=6]
|
|
46
|
+
* @returns {{ model: object|null, error: Error|null, pending: boolean }}
|
|
47
|
+
*/
|
|
48
|
+
export function useQrWorker(value, { eccLevel = 'M', maxVersion = 6 } = {}) {
|
|
49
|
+
const [state, setState] = React.useState({ model: null, error: null, pending: false });
|
|
50
|
+
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (!value) {
|
|
53
|
+
setState({ model: null, error: null, pending: false });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
setState(s => ({ ...s, pending: true }));
|
|
59
|
+
|
|
60
|
+
const id = _nextId++;
|
|
61
|
+
const worker = getWorker();
|
|
62
|
+
|
|
63
|
+
_callbacks.set(id, ({ model, error }) => {
|
|
64
|
+
if (cancelled) return;
|
|
65
|
+
if (error) {
|
|
66
|
+
setState({ model: null, error: new Error(error), pending: false });
|
|
67
|
+
} else {
|
|
68
|
+
setState({ model, error: null, pending: false });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
worker.postMessage({ id, value, eccLevel, maxVersion });
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
cancelled = true;
|
|
76
|
+
_callbacks.delete(id);
|
|
77
|
+
};
|
|
78
|
+
}, [value, eccLevel, maxVersion]);
|
|
79
|
+
|
|
80
|
+
return state;
|
|
81
|
+
}
|