@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.
Files changed (82) hide show
  1. package/LICENSE.md +211 -0
  2. package/NOTICE.md +13 -0
  3. package/README.md +175 -0
  4. package/dist/encode.cjs +147 -0
  5. package/dist/encode.cjs.map +6 -0
  6. package/dist/encode.d.ts +11 -0
  7. package/dist/encode.mjs +122 -0
  8. package/dist/encode.mjs.map +6 -0
  9. package/dist/images/path.cjs +145 -0
  10. package/dist/images/path.cjs.map +6 -0
  11. package/dist/images/path.d.ts +6 -0
  12. package/dist/images/path.mjs +120 -0
  13. package/dist/images/path.mjs.map +6 -0
  14. package/dist/images/pdf.cjs +96 -0
  15. package/dist/images/pdf.cjs.map +6 -0
  16. package/dist/images/pdf.d.ts +3 -0
  17. package/dist/images/pdf.mjs +71 -0
  18. package/dist/images/pdf.mjs.map +6 -0
  19. package/dist/images/png.cjs +86 -0
  20. package/dist/images/png.cjs.map +6 -0
  21. package/dist/images/png.d.ts +3 -0
  22. package/dist/images/png.mjs +61 -0
  23. package/dist/images/png.mjs.map +6 -0
  24. package/dist/images/svg.cjs +64 -0
  25. package/dist/images/svg.cjs.map +6 -0
  26. package/dist/images/svg.d.ts +54 -0
  27. package/dist/images/svg.mjs +38 -0
  28. package/dist/images/svg.mjs.map +6 -0
  29. package/dist/index.cjs +110 -0
  30. package/dist/index.cjs.map +6 -0
  31. package/dist/index.d.ts +74 -0
  32. package/dist/index.mjs +74 -0
  33. package/dist/index.mjs.map +6 -0
  34. package/dist/matrix.cjs +360 -0
  35. package/dist/matrix.cjs.map +6 -0
  36. package/dist/matrix.d.ts +3 -0
  37. package/dist/matrix.mjs +335 -0
  38. package/dist/matrix.mjs.map +6 -0
  39. package/dist/qrcode.cjs +183 -0
  40. package/dist/qrcode.cjs.map +6 -0
  41. package/dist/qrcode.d.ts +20 -0
  42. package/dist/qrcode.mjs +158 -0
  43. package/dist/qrcode.mjs.map +6 -0
  44. package/dist/utils/crc32.cjs +53 -0
  45. package/dist/utils/crc32.cjs.map +6 -0
  46. package/dist/utils/crc32.d.ts +9 -0
  47. package/dist/utils/crc32.mjs +28 -0
  48. package/dist/utils/crc32.mjs.map +6 -0
  49. package/dist/utils/dataurl.cjs +35 -0
  50. package/dist/utils/dataurl.cjs.map +6 -0
  51. package/dist/utils/dataurl.d.ts +1 -0
  52. package/dist/utils/dataurl.mjs +10 -0
  53. package/dist/utils/dataurl.mjs.map +6 -0
  54. package/dist/utils/deflate.cjs +39 -0
  55. package/dist/utils/deflate.cjs.map +6 -0
  56. package/dist/utils/deflate.d.ts +2 -0
  57. package/dist/utils/deflate.mjs +14 -0
  58. package/dist/utils/deflate.mjs.map +6 -0
  59. package/dist/utils/ecc.cjs +93 -0
  60. package/dist/utils/ecc.cjs.map +6 -0
  61. package/dist/utils/ecc.d.ts +2 -0
  62. package/dist/utils/ecc.mjs +68 -0
  63. package/dist/utils/ecc.mjs.map +6 -0
  64. package/dist/utils/merge.cjs +41 -0
  65. package/dist/utils/merge.cjs.map +6 -0
  66. package/dist/utils/merge.d.ts +2 -0
  67. package/dist/utils/merge.mjs +16 -0
  68. package/dist/utils/merge.mjs.map +6 -0
  69. package/package.json +61 -0
  70. package/src/encode.ts +180 -0
  71. package/src/images/path.ts +131 -0
  72. package/src/images/pdf.ts +76 -0
  73. package/src/images/png.ts +105 -0
  74. package/src/images/svg.ts +102 -0
  75. package/src/index.ts +147 -0
  76. package/src/matrix.ts +392 -0
  77. package/src/qrcode.ts +217 -0
  78. package/src/utils/crc32.ts +50 -0
  79. package/src/utils/dataurl.ts +7 -0
  80. package/src/utils/deflate.ts +17 -0
  81. package/src/utils/ecc.ts +95 -0
  82. 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
+ }