@juit/qrcode 0.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.md +211 -0
- package/NOTICE.md +13 -0
- package/README.md +175 -0
- package/dist/encode.cjs +147 -0
- package/dist/encode.cjs.map +6 -0
- package/dist/encode.d.ts +11 -0
- package/dist/encode.mjs +122 -0
- package/dist/encode.mjs.map +6 -0
- package/dist/images/path.cjs +145 -0
- package/dist/images/path.cjs.map +6 -0
- package/dist/images/path.d.ts +6 -0
- package/dist/images/path.mjs +120 -0
- package/dist/images/path.mjs.map +6 -0
- package/dist/images/pdf.cjs +96 -0
- package/dist/images/pdf.cjs.map +6 -0
- package/dist/images/pdf.d.ts +3 -0
- package/dist/images/pdf.mjs +71 -0
- package/dist/images/pdf.mjs.map +6 -0
- package/dist/images/png.cjs +86 -0
- package/dist/images/png.cjs.map +6 -0
- package/dist/images/png.d.ts +3 -0
- package/dist/images/png.mjs +61 -0
- package/dist/images/png.mjs.map +6 -0
- package/dist/images/svg.cjs +64 -0
- package/dist/images/svg.cjs.map +6 -0
- package/dist/images/svg.d.ts +54 -0
- package/dist/images/svg.mjs +38 -0
- package/dist/images/svg.mjs.map +6 -0
- package/dist/index.cjs +110 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.mjs +74 -0
- package/dist/index.mjs.map +6 -0
- package/dist/matrix.cjs +360 -0
- package/dist/matrix.cjs.map +6 -0
- package/dist/matrix.d.ts +3 -0
- package/dist/matrix.mjs +335 -0
- package/dist/matrix.mjs.map +6 -0
- package/dist/qrcode.cjs +183 -0
- package/dist/qrcode.cjs.map +6 -0
- package/dist/qrcode.d.ts +20 -0
- package/dist/qrcode.mjs +158 -0
- package/dist/qrcode.mjs.map +6 -0
- package/dist/utils/crc32.cjs +53 -0
- package/dist/utils/crc32.cjs.map +6 -0
- package/dist/utils/crc32.d.ts +9 -0
- package/dist/utils/crc32.mjs +28 -0
- package/dist/utils/crc32.mjs.map +6 -0
- package/dist/utils/dataurl.cjs +35 -0
- package/dist/utils/dataurl.cjs.map +6 -0
- package/dist/utils/dataurl.d.ts +1 -0
- package/dist/utils/dataurl.mjs +10 -0
- package/dist/utils/dataurl.mjs.map +6 -0
- package/dist/utils/deflate.cjs +39 -0
- package/dist/utils/deflate.cjs.map +6 -0
- package/dist/utils/deflate.d.ts +2 -0
- package/dist/utils/deflate.mjs +14 -0
- package/dist/utils/deflate.mjs.map +6 -0
- package/dist/utils/ecc.cjs +93 -0
- package/dist/utils/ecc.cjs.map +6 -0
- package/dist/utils/ecc.d.ts +2 -0
- package/dist/utils/ecc.mjs +68 -0
- package/dist/utils/ecc.mjs.map +6 -0
- package/dist/utils/merge.cjs +41 -0
- package/dist/utils/merge.cjs.map +6 -0
- package/dist/utils/merge.d.ts +2 -0
- package/dist/utils/merge.mjs +16 -0
- package/dist/utils/merge.mjs.map +6 -0
- package/package.json +61 -0
- package/src/encode.ts +180 -0
- package/src/images/path.ts +131 -0
- package/src/images/pdf.ts +76 -0
- package/src/images/png.ts +105 -0
- package/src/images/svg.ts +102 -0
- package/src/index.ts +147 -0
- package/src/matrix.ts +392 -0
- package/src/qrcode.ts +217 -0
- package/src/utils/crc32.ts +50 -0
- package/src/utils/dataurl.ts +7 -0
- package/src/utils/deflate.ts +17 -0
- package/src/utils/ecc.ts +95 -0
- package/src/utils/merge.ts +13 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// utils/merge.ts
|
|
2
|
+
function mergeArrays(...arrays) {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
const size = arrays.reduce((size2, array) => {
|
|
5
|
+
chunks.push([size2, array]);
|
|
6
|
+
return size2 + array.length;
|
|
7
|
+
}, 0);
|
|
8
|
+
const result = new Uint8Array(size);
|
|
9
|
+
for (const [offset, array] of chunks)
|
|
10
|
+
result.set(array, offset);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
mergeArrays
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=merge.mjs.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/merge.ts"],
|
|
4
|
+
"mappings": ";AACO,SAAS,eAAe,QAAkC;AAC/D,QAAM,SAAkD,CAAC;AAEzD,QAAM,OAAO,OAAO,OAAO,CAACA,OAAM,UAAU;AAC1C,WAAO,KAAK,CAAEA,OAAM,KAAM,CAAC;AAC3B,WAAOA,QAAO,MAAM;AAAA,EACtB,GAAG,CAAC;AAEJ,QAAM,SAAS,IAAI,WAAW,IAAI;AAClC,aAAW,CAAE,QAAQ,KAAM,KAAK;AAAQ,WAAO,IAAI,OAAO,MAAM;AAChE,SAAO;AACT;",
|
|
5
|
+
"names": ["size"]
|
|
6
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juit/qrcode",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A modern QR code generator for JavaScript",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"require": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"import": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "plug",
|
|
22
|
+
"coverage": "plug coverage",
|
|
23
|
+
"dev": "plug coverage -w src -w test",
|
|
24
|
+
"lint": "plug lint",
|
|
25
|
+
"test": "plug test",
|
|
26
|
+
"transpile": "plug transpile"
|
|
27
|
+
},
|
|
28
|
+
"author": "Juit Developers <developers@juit.com>",
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@plugjs/build": "^0.5.36",
|
|
32
|
+
"@types/pdfkit": "^0.13.4",
|
|
33
|
+
"fast-png": "^6.2.0",
|
|
34
|
+
"pdfkit": "^0.15.0"
|
|
35
|
+
},
|
|
36
|
+
"directories": {
|
|
37
|
+
"test": "test"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+ssh://git@github.com/juitnow/juit-qrcode.git"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"qrcode",
|
|
45
|
+
"qr code",
|
|
46
|
+
"qr",
|
|
47
|
+
"png",
|
|
48
|
+
"svg",
|
|
49
|
+
"pdf",
|
|
50
|
+
"image"
|
|
51
|
+
],
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/juitnow/juit-qrcode/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/juitnow/juit-qrcode#readme",
|
|
56
|
+
"files": [
|
|
57
|
+
"*.md",
|
|
58
|
+
"dist/",
|
|
59
|
+
"src/"
|
|
60
|
+
]
|
|
61
|
+
}
|
package/src/encode.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* ========================================================================== *
|
|
2
|
+
* TYPES *
|
|
3
|
+
* ========================================================================== */
|
|
4
|
+
|
|
5
|
+
/** Internal interface for encoding data */
|
|
6
|
+
export interface QrCodeMessage {
|
|
7
|
+
/** Data for QR code version 27 or greater */
|
|
8
|
+
data27: boolean[],
|
|
9
|
+
/** Data for QR code version 10 or greater (shorter) */
|
|
10
|
+
data10?: boolean[],
|
|
11
|
+
/** Data for QR code version 1 or greater (shortest) */
|
|
12
|
+
data1?: boolean[],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ========================================================================== *
|
|
16
|
+
* CONSTANTS *
|
|
17
|
+
* ========================================================================== */
|
|
18
|
+
|
|
19
|
+
// Index for alphanumeric characters
|
|
20
|
+
const ALPHANUM: Record<string, number> = (function(s) {
|
|
21
|
+
const res: Record<string, number> = {}
|
|
22
|
+
for (let i = 0; i < s.length; i++) {
|
|
23
|
+
res[s[i]!] = i
|
|
24
|
+
}
|
|
25
|
+
return res
|
|
26
|
+
})('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:')
|
|
27
|
+
|
|
28
|
+
/* ========================================================================== *
|
|
29
|
+
* INTERNALS *
|
|
30
|
+
* ========================================================================== */
|
|
31
|
+
|
|
32
|
+
// Bush a number into a binary array (that is, 1s or 0s)
|
|
33
|
+
function pushBits(arr: boolean[], n: number, value: number): boolean[] {
|
|
34
|
+
for (let bit = 1 << (n - 1); bit; bit = bit >>> 1) {
|
|
35
|
+
arr.push(!! (bit & value))
|
|
36
|
+
}
|
|
37
|
+
return arr
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Encode binary data
|
|
41
|
+
function binaryEncode(data: Uint8Array): QrCodeMessage {
|
|
42
|
+
const len = data.length
|
|
43
|
+
const bits: boolean[] = []
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < len; i++) {
|
|
46
|
+
pushBits(bits, 8, data[i]!)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const d = pushBits([ false, true, false, false ], 16, len)
|
|
50
|
+
|
|
51
|
+
const res: QrCodeMessage = {
|
|
52
|
+
data27: d.concat(bits),
|
|
53
|
+
}
|
|
54
|
+
res.data10 = res.data27
|
|
55
|
+
|
|
56
|
+
if (len < 256) {
|
|
57
|
+
const d = pushBits([ false, true, false, false ], 8, len)
|
|
58
|
+
res.data1 = d.concat(bits)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return res
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Encode alphanumeric data
|
|
65
|
+
function alphanumEncode(str: string): QrCodeMessage {
|
|
66
|
+
const len = str.length
|
|
67
|
+
const bits: boolean[] = []
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < len; i += 2) {
|
|
70
|
+
let b = 6
|
|
71
|
+
let n = ALPHANUM[str[i]!]!
|
|
72
|
+
if (str[i+1]) {
|
|
73
|
+
b = 11
|
|
74
|
+
n = n * 45 + ALPHANUM[str[i+1]!]!
|
|
75
|
+
}
|
|
76
|
+
pushBits(bits, b, n)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const d = pushBits([ false, false, true, false ], 13, len)
|
|
80
|
+
|
|
81
|
+
const res: QrCodeMessage = {
|
|
82
|
+
data27: d.concat(bits),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (len < 2048) {
|
|
86
|
+
const d = pushBits([ false, false, true, false ], 11, len)
|
|
87
|
+
res.data10 = d.concat(bits)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (len < 512) {
|
|
91
|
+
const d = pushBits([ false, false, true, false ], 9, len)
|
|
92
|
+
res.data1 = d.concat(bits)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return res
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Encode numeric data
|
|
99
|
+
function numericEncode(str: string): QrCodeMessage {
|
|
100
|
+
const len = str.length
|
|
101
|
+
const bits: boolean[] = []
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < len; i += 3) {
|
|
104
|
+
const s = str.substring(i, i + 3)
|
|
105
|
+
const b = Math.ceil(s.length * 10 / 3)
|
|
106
|
+
pushBits(bits, b, parseInt(s, 10))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const d = pushBits([ false, false, false, true ], 14, len)
|
|
110
|
+
|
|
111
|
+
const res: QrCodeMessage = {
|
|
112
|
+
data27: d.concat(bits),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (len < 4096) {
|
|
116
|
+
const d = pushBits([ false, false, false, true ], 12, len)
|
|
117
|
+
res.data10 = d.concat(bits)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (len < 1024) {
|
|
121
|
+
const d = pushBits([ false, false, false, true ], 10, len)
|
|
122
|
+
res.data1 = d.concat(bits)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return res
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Encode URLs (specific string format)
|
|
129
|
+
function urlEncode(str: string): QrCodeMessage {
|
|
130
|
+
const slash = str.indexOf('/', 8) + 1 || str.length
|
|
131
|
+
const res = encodeQrCodeMessage(str.slice(0, slash).toUpperCase(), false)
|
|
132
|
+
|
|
133
|
+
if (slash >= str.length) return res
|
|
134
|
+
|
|
135
|
+
const path = encodeQrCodeMessage(str.slice(slash), false)
|
|
136
|
+
|
|
137
|
+
res.data27 = res.data27.concat(path.data27)
|
|
138
|
+
|
|
139
|
+
if (res.data10 && path.data10) {
|
|
140
|
+
res.data10 = res.data10.concat(path.data10)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (res.data1 && path.data1) {
|
|
144
|
+
res.data1 = res.data1.concat(path.data1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return res
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ========================================================================== *
|
|
151
|
+
* EXPORTED *
|
|
152
|
+
* ========================================================================== */
|
|
153
|
+
|
|
154
|
+
/** Generate a message for the specified text or binary data */
|
|
155
|
+
export function encodeQrCodeMessage(message: string | Uint8Array, url: boolean): QrCodeMessage {
|
|
156
|
+
let data: Uint8Array
|
|
157
|
+
|
|
158
|
+
if (typeof message === 'string') {
|
|
159
|
+
data = new TextEncoder().encode(message)
|
|
160
|
+
|
|
161
|
+
if (/^[0-9]+$/.test(message)) {
|
|
162
|
+
if (data.length > 7089) throw new Error(`Too much numeric data (len=${data.length})`)
|
|
163
|
+
return numericEncode(message)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (/^[0-9A-Z $%*+./:-]+$/.test(message)) {
|
|
167
|
+
if (data.length > 4296) throw new Error(`Too much alphanumeric data (len=${data.length})`)
|
|
168
|
+
return alphanumEncode(message)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (url && /^https?:/i.test(message)) {
|
|
172
|
+
return urlEncode(message)
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
data = message
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (data.length > 2953) throw new Error(`Too much binary data (len=${data.length})`)
|
|
179
|
+
return binaryEncode(data)
|
|
180
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { QrCode } from '../index'
|
|
2
|
+
|
|
3
|
+
export type PathItem = [ 'M', number, number ] | [ 'h', number ] | [ 'v', number ]
|
|
4
|
+
export type Path = PathItem[]
|
|
5
|
+
export type Paths = Path[]
|
|
6
|
+
|
|
7
|
+
/** Generate a set of vector paths for a QR code */
|
|
8
|
+
export function generatePaths(code: QrCode): Paths {
|
|
9
|
+
const { matrix, size } = code
|
|
10
|
+
|
|
11
|
+
const filled: boolean[][] = []
|
|
12
|
+
|
|
13
|
+
for (let row = -1; row <= size; row++) {
|
|
14
|
+
filled[row] = []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const path = []
|
|
18
|
+
for (let row = 0; row < size; row++) {
|
|
19
|
+
for (let col = 0; col < size; col++) {
|
|
20
|
+
if (filled[row]![col]) continue
|
|
21
|
+
|
|
22
|
+
filled[row]![col] = true
|
|
23
|
+
|
|
24
|
+
if (isDark(row, col)) {
|
|
25
|
+
if (!isDark(row - 1, col)) {
|
|
26
|
+
path.push(plot(row, col, 'right'))
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
if (isDark(row, col - 1)) {
|
|
30
|
+
path.push(plot(row, col, 'down'))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
function isDark(row: number, col: number):boolean {
|
|
39
|
+
if (row < 0 || col < 0 || row >= size || col >= size) return false
|
|
40
|
+
return !! matrix[row]![col]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function plot(row0: number, col0: number, dir: 'right' | 'left' | 'down' | 'up'): Path {
|
|
44
|
+
filled[row0]![col0] = true
|
|
45
|
+
const res: Path = []
|
|
46
|
+
|
|
47
|
+
res.push([ 'M', col0, row0 ])
|
|
48
|
+
let row = row0
|
|
49
|
+
let col = col0
|
|
50
|
+
let len = 0
|
|
51
|
+
|
|
52
|
+
do {
|
|
53
|
+
switch (dir) {
|
|
54
|
+
case 'right':
|
|
55
|
+
filled[row]![col] = true
|
|
56
|
+
if (isDark(row, col)) {
|
|
57
|
+
filled[row - 1]![col] = true
|
|
58
|
+
if (isDark(row - 1, col)) {
|
|
59
|
+
res.push([ 'h', len ])
|
|
60
|
+
len = 0
|
|
61
|
+
dir = 'up'
|
|
62
|
+
} else {
|
|
63
|
+
len++
|
|
64
|
+
col++
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
res.push([ 'h', len ])
|
|
68
|
+
len = 0
|
|
69
|
+
dir = 'down'
|
|
70
|
+
}
|
|
71
|
+
break
|
|
72
|
+
case 'left':
|
|
73
|
+
filled[row - 1]![col - 1] = true
|
|
74
|
+
if (isDark(row - 1, col - 1)) {
|
|
75
|
+
filled[row]![col - 1] = true
|
|
76
|
+
if (isDark(row, col - 1)) {
|
|
77
|
+
res.push([ 'h', -len ])
|
|
78
|
+
len = 0
|
|
79
|
+
dir = 'down'
|
|
80
|
+
} else {
|
|
81
|
+
len++
|
|
82
|
+
col--
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
res.push([ 'h', -len ])
|
|
86
|
+
len = 0
|
|
87
|
+
dir = 'up'
|
|
88
|
+
}
|
|
89
|
+
break
|
|
90
|
+
case 'down':
|
|
91
|
+
filled[row]![col - 1] = true
|
|
92
|
+
if (isDark(row, col - 1)) {
|
|
93
|
+
filled[row]![col] = true
|
|
94
|
+
if (isDark(row, col)) {
|
|
95
|
+
res.push([ 'v', len ])
|
|
96
|
+
len = 0
|
|
97
|
+
dir = 'right'
|
|
98
|
+
} else {
|
|
99
|
+
len++
|
|
100
|
+
row++
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
res.push([ 'v', len ])
|
|
104
|
+
len = 0
|
|
105
|
+
dir = 'left'
|
|
106
|
+
}
|
|
107
|
+
break
|
|
108
|
+
case 'up':
|
|
109
|
+
filled[row - 1]![col] = true
|
|
110
|
+
if (isDark(row - 1, col)) {
|
|
111
|
+
filled[row - 1]![col - 1] = true
|
|
112
|
+
if (isDark(row - 1, col - 1)) {
|
|
113
|
+
res.push([ 'v', -len ])
|
|
114
|
+
len = 0
|
|
115
|
+
dir = 'left'
|
|
116
|
+
} else {
|
|
117
|
+
len++
|
|
118
|
+
row--
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
res.push([ 'v', -len ])
|
|
122
|
+
len = 0
|
|
123
|
+
dir = 'right'
|
|
124
|
+
}
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
} while (row != row0 || col != col0)
|
|
128
|
+
|
|
129
|
+
return res
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { deflate } from '../utils/deflate'
|
|
2
|
+
import { mergeArrays } from '../utils/merge'
|
|
3
|
+
import { generatePaths } from './path'
|
|
4
|
+
|
|
5
|
+
import type { QrCode, QrCodeImageOptions } from '..'
|
|
6
|
+
|
|
7
|
+
/** Generate a PDF document for the given {@link QrCode} */
|
|
8
|
+
export async function generatePdf(code: QrCode, options?: QrCodeImageOptions): Promise<Uint8Array> {
|
|
9
|
+
const { margin = 4, scale = 9 } = { ...options }
|
|
10
|
+
const size = (code.size + 2 * margin) * scale
|
|
11
|
+
|
|
12
|
+
// Our text encoder used throughout
|
|
13
|
+
const encoder = new TextEncoder()
|
|
14
|
+
|
|
15
|
+
// PDF header and preamble
|
|
16
|
+
const chunks: Uint8Array[] = [
|
|
17
|
+
encoder.encode('%PDF-1.0\n\n'), // PDF header
|
|
18
|
+
encoder.encode('1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n'),
|
|
19
|
+
encoder.encode('2 0 obj << /Type /Pages /Count 1 /Kids [ 3 0 R ] >> endobj\n'),
|
|
20
|
+
encoder.encode(`3 0 obj << /Type /Page /Parent 2 0 R /Resources <<>> /Contents 4 0 R /MediaBox [ 0 0 ${size} ${size} ] >> endobj\n`),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
// Convert paths into a streamed PDF object
|
|
24
|
+
let path = `${scale} 0 0 ${scale} 0 0 cm\n`
|
|
25
|
+
path += generatePaths(code).map(function(subpath) {
|
|
26
|
+
let x: number = NaN
|
|
27
|
+
let y: number = NaN
|
|
28
|
+
let res: string = ''
|
|
29
|
+
|
|
30
|
+
for (let k = 0; k < subpath.length; k++) {
|
|
31
|
+
const item = subpath[k]!
|
|
32
|
+
switch (item[0]) {
|
|
33
|
+
case 'M':
|
|
34
|
+
x = item[1] + margin
|
|
35
|
+
y = code.size - item[2] + margin
|
|
36
|
+
res += x + ' ' + y + ' m '
|
|
37
|
+
break
|
|
38
|
+
case 'h':
|
|
39
|
+
x += item[1]
|
|
40
|
+
res += x + ' ' + y + ' l '
|
|
41
|
+
break
|
|
42
|
+
case 'v':
|
|
43
|
+
y -= item[1]
|
|
44
|
+
res += x + ' ' + y + ' l '
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
res += 'h'
|
|
49
|
+
return res
|
|
50
|
+
}).join('\n')
|
|
51
|
+
path += '\nf\n'
|
|
52
|
+
|
|
53
|
+
// Encode the path as our 4th object
|
|
54
|
+
const deflated = await deflate(encoder.encode(path))
|
|
55
|
+
chunks.push(mergeArrays(
|
|
56
|
+
encoder.encode(`4 0 obj << /Length ${deflated.length} /Filter /FlateDecode >> stream\n`), // start the stream
|
|
57
|
+
deflated, // the path is deflated
|
|
58
|
+
encoder.encode('\nendstream\nendobj\n'), // end the stream
|
|
59
|
+
))
|
|
60
|
+
|
|
61
|
+
// Calculate the offsets of our objects (XREFs)
|
|
62
|
+
let xref = 'xref\n0 5\n0000000000 65535 f \n'
|
|
63
|
+
let offset = chunks[0]!.length
|
|
64
|
+
for (let i = 1; i < 5; i++) {
|
|
65
|
+
xref += `0000000000${offset}`.slice(-10) + ' 00000 n \n'
|
|
66
|
+
offset += chunks[i]!.length
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
chunks.push(
|
|
70
|
+
encoder.encode(xref),
|
|
71
|
+
encoder.encode('trailer << /Root 1 0 R /Size 5 >>\n'),
|
|
72
|
+
encoder.encode('startxref\n' + offset + '\n%%EOF\n'),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return mergeArrays(...chunks)
|
|
76
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { crc32 } from '../utils/crc32'
|
|
2
|
+
import { deflate } from '../utils/deflate'
|
|
3
|
+
|
|
4
|
+
import type { QrCode, QrCodeImageOptions } from '../index'
|
|
5
|
+
|
|
6
|
+
/* ========================================================================== *
|
|
7
|
+
* TYPES *
|
|
8
|
+
* ========================================================================== */
|
|
9
|
+
|
|
10
|
+
interface Bitmap {
|
|
11
|
+
data: Uint8Array,
|
|
12
|
+
size: number,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ========================================================================== *
|
|
16
|
+
* CONSTANTS *
|
|
17
|
+
* ========================================================================== */
|
|
18
|
+
|
|
19
|
+
const PNG_HEAD = new Uint8Array([ 137, 80, 78, 71, 13, 10, 26, 10 ])
|
|
20
|
+
const PNG_IHDR = new Uint8Array([ 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0 ])
|
|
21
|
+
const PNG_IDAT = new Uint8Array([ 0, 0, 0, 0, 73, 68, 65, 84 ])
|
|
22
|
+
const PNG_IEND = new Uint8Array([ 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 ])
|
|
23
|
+
|
|
24
|
+
/* ========================================================================== *
|
|
25
|
+
* INTERNALS *
|
|
26
|
+
* ========================================================================== */
|
|
27
|
+
|
|
28
|
+
// Encode a PNG bitmap into a Uint8Array
|
|
29
|
+
async function png(bitmap: Bitmap): Promise<Uint8Array> {
|
|
30
|
+
const chunks: Uint8Array[] = []
|
|
31
|
+
|
|
32
|
+
// push the PNG header
|
|
33
|
+
chunks.push(PNG_HEAD)
|
|
34
|
+
|
|
35
|
+
// create the image header
|
|
36
|
+
const imageHeader = new Uint8Array(PNG_IHDR)
|
|
37
|
+
const imageHeaderView = new DataView(imageHeader.buffer)
|
|
38
|
+
|
|
39
|
+
imageHeaderView.setUint32(8, bitmap.size, false) // width of the PNG image
|
|
40
|
+
imageHeaderView.setUint32(12, bitmap.size, false) // height of the PNG image
|
|
41
|
+
imageHeaderView.setUint32(21, crc32(imageHeader, 4, -4), false) // crc at the end
|
|
42
|
+
|
|
43
|
+
chunks.push(imageHeader) // push our first "chunk"
|
|
44
|
+
|
|
45
|
+
// compress our image data
|
|
46
|
+
const data = await deflate(bitmap.data)
|
|
47
|
+
|
|
48
|
+
// create our image data array (header, compressed data, crc)
|
|
49
|
+
const imageData = new Uint8Array(PNG_IDAT.length + data.length + 4)
|
|
50
|
+
const imageDataView = new DataView(imageData.buffer)
|
|
51
|
+
|
|
52
|
+
imageData.set(PNG_IDAT, 0) // first is the image data preamble
|
|
53
|
+
imageData.set(data, PNG_IDAT.length) // then is the compressed data
|
|
54
|
+
imageDataView.setUint32(0, imageData.length - 12, false) // length goes at the beginning
|
|
55
|
+
imageDataView.setUint32(imageData.length - 4, crc32(imageData, 4, -4), false) // then crc at the end
|
|
56
|
+
|
|
57
|
+
chunks.push(imageData) // second "chunk"
|
|
58
|
+
|
|
59
|
+
// push the PNG trailer "as is"
|
|
60
|
+
chunks.push(PNG_IEND)
|
|
61
|
+
|
|
62
|
+
// combine our chunks into a single array
|
|
63
|
+
return new Uint8Array(await new Blob(chunks).arrayBuffer())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Convert a matrix to a PNG bitmap
|
|
67
|
+
function bitmap(matrix: readonly boolean[][], scale: number, margin: number): Bitmap {
|
|
68
|
+
const n = matrix.length
|
|
69
|
+
const x = (n + 2 * margin) * scale
|
|
70
|
+
const data = new Uint8Array((x + 1) * x).fill(255)
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < x; i++) {
|
|
73
|
+
data[i * (x + 1)] = 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < n; i++) {
|
|
77
|
+
for (let j = 0; j < n; j++) {
|
|
78
|
+
if (matrix[i]![j]) {
|
|
79
|
+
const offset = ((margin + i) * (x + 1) + (margin + j)) * scale + 1
|
|
80
|
+
data.fill(0, offset, offset + scale)
|
|
81
|
+
for (let c = 1; c < scale; c++) {
|
|
82
|
+
const chunk = data.subarray(offset, offset + scale)
|
|
83
|
+
data.set(chunk, offset + c * (x + 1))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
data: data,
|
|
91
|
+
size: x,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ========================================================================== *
|
|
96
|
+
* EXPORTED *
|
|
97
|
+
* ========================================================================== */
|
|
98
|
+
|
|
99
|
+
/** Generate a PNG image for the given {@link QrCode} */
|
|
100
|
+
export async function generatePng(code: QrCode, options?: QrCodeImageOptions): Promise<Uint8Array> {
|
|
101
|
+
const { margin = 4, scale = 1 } = { ...options }
|
|
102
|
+
const result = bitmap(code.matrix, scale, margin)
|
|
103
|
+
const image = await png(result)
|
|
104
|
+
return image
|
|
105
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { generatePaths } from './path'
|
|
2
|
+
|
|
3
|
+
import type { QrCode, QrCodeImageOptions } from '..'
|
|
4
|
+
|
|
5
|
+
/* ========================================================================== *
|
|
6
|
+
* INTERNALS *
|
|
7
|
+
* ========================================================================== */
|
|
8
|
+
|
|
9
|
+
function convertPath(
|
|
10
|
+
chunks: (string | number)[],
|
|
11
|
+
code: QrCode,
|
|
12
|
+
margin: number,
|
|
13
|
+
): (string | number)[] {
|
|
14
|
+
generatePaths(code).forEach((path) => {
|
|
15
|
+
for (let k = 0; k < path.length; k++) {
|
|
16
|
+
const item = path[k]!
|
|
17
|
+
switch (item[0]) {
|
|
18
|
+
case 'M': // move
|
|
19
|
+
chunks.push(`M${item[1] + margin} ${item[2] + margin}`)
|
|
20
|
+
break
|
|
21
|
+
default: // draw path
|
|
22
|
+
chunks.push(...item)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
chunks.push('z') // done
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return chunks
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ========================================================================== *
|
|
32
|
+
* EXPORTED *
|
|
33
|
+
* ========================================================================== */
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate a SVG _path_ for the given {@link QrCode}.
|
|
37
|
+
*
|
|
38
|
+
* The returned SVG path will be a simple conversion of the QR code's own
|
|
39
|
+
* {@link QrCode.matrix matrix} into a square path of _**N**_ "pixels"
|
|
40
|
+
* (where _**N**_ is the size of the {@link QrCode.size size} of the matrix).
|
|
41
|
+
*
|
|
42
|
+
* This can be scaled and positioned into a final SVG using the `scale(...)` and
|
|
43
|
+
* `translate(...)` [basic SVG transformations](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Basic_Transformations).
|
|
44
|
+
*
|
|
45
|
+
* This is also particulary useful with [PDFKit](https://pdfkit.org/)'s own
|
|
46
|
+
* implementation of [SVG paths](https://pdfkit.org/docs/vector.html#svg_paths).
|
|
47
|
+
*
|
|
48
|
+
* We can use it together with `translate(...)` and `scale(...)` to draw our QR
|
|
49
|
+
* code anywhere on the page, in any size. For example, to prepare a simple A4
|
|
50
|
+
* document with a 10cm QR code smack in the middle we can:
|
|
51
|
+
*
|
|
52
|
+
* ```typescript
|
|
53
|
+
* // generate the QR code _structure_ for our message
|
|
54
|
+
* const code = generateQrCode('https://www.juit.com/')
|
|
55
|
+
*
|
|
56
|
+
* // generate the SVG path for our QR code
|
|
57
|
+
* const path = generateSvgPath(code)
|
|
58
|
+
*
|
|
59
|
+
* // calculate how to translate and scale our QR code in the page
|
|
60
|
+
* const dpcm = 72 / 2.54 // PDFKit uses 72dpi (inches) we want metric!
|
|
61
|
+
* const size = 10 * dpcm // 10 cm (size of our QR code) in dots
|
|
62
|
+
* const scale = size / code.size // scale factor for our QR code to be 10 cm
|
|
63
|
+
* const x = ((21 - 10) / 2) * dpcm // center horizontally
|
|
64
|
+
* const y = ((29.7 - 10) / 2) * dpcm // center vertically
|
|
65
|
+
*
|
|
66
|
+
* // create a new A4 document, and stream it to "test.pdf"
|
|
67
|
+
* const document = new PDFDocument({ size: 'A4' })
|
|
68
|
+
* const stream = createWriteStream('test.pdf')
|
|
69
|
+
* document.pipe(stream)
|
|
70
|
+
*
|
|
71
|
+
* // draw our 10cm QR code right in the middle of the page
|
|
72
|
+
* document
|
|
73
|
+
* .translate(x, y) // move to x = 5.5cm, y = 9.85cm
|
|
74
|
+
* .scale(scale) // scale our QR code to 10cm width and height
|
|
75
|
+
* .path(path) // draw our QR code smack in the middle of the page
|
|
76
|
+
* .fill('black') // fill our QR code in black
|
|
77
|
+
* .end() // finish up and close the document
|
|
78
|
+
*
|
|
79
|
+
* // wait for the stream to finish
|
|
80
|
+
* stream.on('finish', () => {
|
|
81
|
+
* // your PDF file is ready!
|
|
82
|
+
* })
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function generateSvgPath(code: QrCode): string {
|
|
86
|
+
return convertPath([], code, 0).join()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Generate a SVG image for the given {@link QrCode} */
|
|
90
|
+
export function generateSvg(code: QrCode, options?: QrCodeImageOptions): string {
|
|
91
|
+
const { margin = 4, scale = 1 } = { ...options }
|
|
92
|
+
const size = code.size + 2 * margin
|
|
93
|
+
const scaled = size * scale
|
|
94
|
+
|
|
95
|
+
const chunks = convertPath([
|
|
96
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${scaled}" height="${scaled}" viewBox="0 0 ${size} ${size}">`,
|
|
97
|
+
'<path d="', // beginning of the path "d" attribute...
|
|
98
|
+
], code, margin)
|
|
99
|
+
|
|
100
|
+
chunks.push('"/></svg>') // close the path "d" attribute
|
|
101
|
+
return chunks.join('')
|
|
102
|
+
}
|