isahaq-barcode 1.8.0 → 2.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/CHANGELOG.md +107 -0
- package/LICENSE +21 -0
- package/README.md +319 -519
- package/bin/generate.js +171 -134
- package/browser.js +163 -170
- package/index.d.ts +497 -0
- package/index.js +118 -38
- package/package.json +68 -34
- package/src/builders/QrCodeBuilder.js +328 -165
- package/src/encoders/BarcodeEncoder.js +260 -0
- package/src/renderers/HTMLRenderer.js +36 -169
- package/src/renderers/PDFRenderer.js +108 -130
- package/src/renderers/PNGRenderer.js +7 -83
- package/src/renderers/RenderFormats.js +89 -38
- package/src/renderers/SVGRenderer.js +8 -182
- package/src/services/BarcodeService.js +96 -36
- package/src/types/BarcodeTypes.js +535 -144
- package/src/validators/Validator.js +140 -122
- package/.eslintrc.js +0 -29
- package/.prettierignore +0 -11
- package/.prettierrc +0 -11
- package/eslint.config.js +0 -67
- package/examples/basic-usage.js +0 -58
- package/examples/batch-example.json +0 -56
- package/examples/express-server.js +0 -163
- package/jest.config.js +0 -13
- package/tests/BarcodeGenerator.test.js +0 -187
- package/tests/BarcodeService.test.js +0 -76
- package/tests/QrCodeBuilder.test.js +0 -130
- package/tests/setup.js +0 -59
|
@@ -1,17 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QR Code Builder - Advanced QR code generation with logo and watermark
|
|
2
|
+
* QR Code Builder - Advanced QR code generation with logo, label and watermark
|
|
3
|
+
*
|
|
4
|
+
* Plain QR codes are produced by the `qrcode` package alone. The optional
|
|
5
|
+
* `canvas` dependency is only loaded when a logo, label or watermark has to be
|
|
6
|
+
* composited on top of the code.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
const QRCode = require('qrcode');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
|
|
11
|
+
/** Cached result of the optional canvas lookup */
|
|
12
|
+
let canvasModule;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load the optional canvas dependency
|
|
16
|
+
* @param {string} feature - Feature requiring canvas, used in the error message
|
|
17
|
+
* @returns {Object} The canvas module
|
|
18
|
+
*/
|
|
19
|
+
function requireCanvas(feature) {
|
|
20
|
+
if (canvasModule === undefined) {
|
|
21
|
+
try {
|
|
22
|
+
canvasModule = require('canvas');
|
|
23
|
+
} catch {
|
|
24
|
+
canvasModule = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!canvasModule) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`${feature} requires the optional "canvas" dependency. Install it with: npm install canvas`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return canvasModule;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert an RGB array or CSS/hex colour into a hex string
|
|
39
|
+
* @param {Array<number>|string} color - Colour value
|
|
40
|
+
* @param {string} fallback - Fallback hex colour
|
|
41
|
+
* @returns {string} Hex colour including the leading '#'
|
|
42
|
+
*/
|
|
43
|
+
function toHexColor(color, fallback) {
|
|
44
|
+
if (Array.isArray(color) && color.length >= 3) {
|
|
45
|
+
return `#${color
|
|
46
|
+
.slice(0, 3)
|
|
47
|
+
.map(component => {
|
|
48
|
+
const value = Math.max(0, Math.min(255, Number(component) || 0));
|
|
49
|
+
return value.toString(16).padStart(2, '0');
|
|
50
|
+
})
|
|
51
|
+
.join('')}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof color === 'string' && color.trim()) {
|
|
55
|
+
return color.trim().startsWith('#') ? color.trim() : `#${color.trim()}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalise the requested output format
|
|
63
|
+
* @param {string} format - Requested format
|
|
64
|
+
* @returns {string} One of png, jpeg or svg
|
|
65
|
+
*/
|
|
66
|
+
function normaliseFormat(format) {
|
|
67
|
+
const value = String(format || 'png').toLowerCase();
|
|
68
|
+
if (value === 'jpg' || value === 'jpeg') {
|
|
69
|
+
return 'jpeg';
|
|
70
|
+
}
|
|
71
|
+
if (value === 'svg') {
|
|
72
|
+
return 'svg';
|
|
73
|
+
}
|
|
74
|
+
return 'png';
|
|
15
75
|
}
|
|
16
76
|
|
|
17
77
|
class QrCodeBuilder {
|
|
@@ -24,7 +84,7 @@ class QrCodeBuilder {
|
|
|
24
84
|
foregroundColor: [0, 0, 0],
|
|
25
85
|
backgroundColor: [255, 255, 255],
|
|
26
86
|
logoPath: null,
|
|
27
|
-
logoSize:
|
|
87
|
+
logoSize: 20, // percentage of the QR code size
|
|
28
88
|
label: null,
|
|
29
89
|
labelFont: null,
|
|
30
90
|
labelFontSize: 16,
|
|
@@ -85,8 +145,8 @@ class QrCodeBuilder {
|
|
|
85
145
|
}
|
|
86
146
|
|
|
87
147
|
/**
|
|
88
|
-
* Set the foreground
|
|
89
|
-
* @param {Array} color - RGB
|
|
148
|
+
* Set the foreground colour
|
|
149
|
+
* @param {Array<number>|string} color - RGB array or hex colour
|
|
90
150
|
* @returns {QrCodeBuilder} Builder instance
|
|
91
151
|
*/
|
|
92
152
|
foregroundColor(color) {
|
|
@@ -95,8 +155,8 @@ class QrCodeBuilder {
|
|
|
95
155
|
}
|
|
96
156
|
|
|
97
157
|
/**
|
|
98
|
-
* Set the background
|
|
99
|
-
* @param {Array} color - RGB
|
|
158
|
+
* Set the background colour
|
|
159
|
+
* @param {Array<number>|string} color - RGB array or hex colour
|
|
100
160
|
* @returns {QrCodeBuilder} Builder instance
|
|
101
161
|
*/
|
|
102
162
|
backgroundColor(color) {
|
|
@@ -106,7 +166,7 @@ class QrCodeBuilder {
|
|
|
106
166
|
|
|
107
167
|
/**
|
|
108
168
|
* Set the logo path
|
|
109
|
-
* @param {string} logoPath - Path to logo image
|
|
169
|
+
* @param {string} logoPath - Path to a logo image
|
|
110
170
|
* @returns {QrCodeBuilder} Builder instance
|
|
111
171
|
*/
|
|
112
172
|
logoPath(logoPath) {
|
|
@@ -116,7 +176,7 @@ class QrCodeBuilder {
|
|
|
116
176
|
|
|
117
177
|
/**
|
|
118
178
|
* Set the logo size
|
|
119
|
-
* @param {number} logoSize - Logo size as percentage of QR code size
|
|
179
|
+
* @param {number} logoSize - Logo size as a percentage of the QR code size
|
|
120
180
|
* @returns {QrCodeBuilder} Builder instance
|
|
121
181
|
*/
|
|
122
182
|
logoSize(logoSize) {
|
|
@@ -136,8 +196,8 @@ class QrCodeBuilder {
|
|
|
136
196
|
|
|
137
197
|
/**
|
|
138
198
|
* Set the label font
|
|
139
|
-
* @param {string} fontPath -
|
|
140
|
-
* @param {number} fontSize - Font size
|
|
199
|
+
* @param {string} fontPath - Font family name or path to a font file
|
|
200
|
+
* @param {number} fontSize - Font size in pixels
|
|
141
201
|
* @returns {QrCodeBuilder} Builder instance
|
|
142
202
|
*/
|
|
143
203
|
labelFont(fontPath, fontSize = 16) {
|
|
@@ -160,7 +220,7 @@ class QrCodeBuilder {
|
|
|
160
220
|
|
|
161
221
|
/**
|
|
162
222
|
* Set the output format
|
|
163
|
-
* @param {string} format - Output format (png,
|
|
223
|
+
* @param {string} format - Output format (png, jpeg, svg)
|
|
164
224
|
* @returns {QrCodeBuilder} Builder instance
|
|
165
225
|
*/
|
|
166
226
|
format(format) {
|
|
@@ -169,39 +229,36 @@ class QrCodeBuilder {
|
|
|
169
229
|
}
|
|
170
230
|
|
|
171
231
|
/**
|
|
172
|
-
* Build the
|
|
173
|
-
* @returns {
|
|
232
|
+
* Build an immutable result object from the current options
|
|
233
|
+
* @returns {QrCodeResult} QR code result
|
|
174
234
|
*/
|
|
175
235
|
build() {
|
|
176
|
-
return new QrCodeResult(this.options);
|
|
236
|
+
return new QrCodeResult({ ...this.options });
|
|
177
237
|
}
|
|
178
238
|
|
|
179
239
|
/**
|
|
180
|
-
* Generate QR code
|
|
181
|
-
* @returns {Promise<Buffer>} QR code buffer
|
|
240
|
+
* Generate the QR code
|
|
241
|
+
* @returns {Promise<Buffer|string>} QR code buffer, or SVG markup
|
|
182
242
|
*/
|
|
183
243
|
async generate() {
|
|
184
|
-
|
|
185
|
-
return await result.generate();
|
|
244
|
+
return this.build().generate();
|
|
186
245
|
}
|
|
187
246
|
|
|
188
247
|
/**
|
|
189
|
-
* Generate QR code and save to file
|
|
248
|
+
* Generate the QR code and save it to a file
|
|
190
249
|
* @param {string} filePath - Output file path
|
|
191
|
-
* @returns {Promise<
|
|
250
|
+
* @returns {Promise<string>} The path the file was written to
|
|
192
251
|
*/
|
|
193
252
|
async saveToFile(filePath) {
|
|
194
|
-
|
|
195
|
-
return await result.saveToFile(filePath);
|
|
253
|
+
return this.build().saveToFile(filePath);
|
|
196
254
|
}
|
|
197
255
|
|
|
198
256
|
/**
|
|
199
|
-
* Generate QR code and return as data URI
|
|
257
|
+
* Generate the QR code and return it as a data URI
|
|
200
258
|
* @returns {Promise<string>} Data URI
|
|
201
259
|
*/
|
|
202
260
|
async getDataUri() {
|
|
203
|
-
|
|
204
|
-
return await result.getDataUri();
|
|
261
|
+
return this.build().getDataUri();
|
|
205
262
|
}
|
|
206
263
|
}
|
|
207
264
|
|
|
@@ -213,202 +270,308 @@ class QrCodeResult {
|
|
|
213
270
|
this.options = options;
|
|
214
271
|
}
|
|
215
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Whether the result needs canvas compositing
|
|
275
|
+
* @returns {boolean} True when a logo, watermark or label is configured
|
|
276
|
+
*/
|
|
277
|
+
needsCompositing() {
|
|
278
|
+
return !!(
|
|
279
|
+
this.options.logoPath ||
|
|
280
|
+
this.options.watermark ||
|
|
281
|
+
this.options.label
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
216
285
|
/**
|
|
217
286
|
* Generate the QR code
|
|
218
|
-
* @returns {Promise<Buffer>} QR code buffer
|
|
287
|
+
* @returns {Promise<Buffer|string>} QR code buffer, or SVG markup for svg format
|
|
219
288
|
*/
|
|
220
289
|
async generate() {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const ctx = canvas.getContext('2d');
|
|
225
|
-
|
|
226
|
-
// Set background
|
|
227
|
-
ctx.fillStyle = this.rgbToHex(this.options.backgroundColor);
|
|
228
|
-
ctx.fillRect(0, 0, this.options.size, this.options.size);
|
|
229
|
-
|
|
230
|
-
// Generate QR code
|
|
231
|
-
const qrCodeDataURL = await QRCode.toDataURL(this.options.data, {
|
|
232
|
-
width: this.options.size - this.options.margin * 2,
|
|
233
|
-
margin: 0,
|
|
234
|
-
color: {
|
|
235
|
-
dark: this.rgbToHex(this.options.foregroundColor),
|
|
236
|
-
light: this.rgbToHex(this.options.backgroundColor),
|
|
237
|
-
},
|
|
238
|
-
errorCorrectionLevel: this.options.errorCorrectionLevel,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Load QR code image
|
|
242
|
-
const qrCodeImage = await loadImage(qrCodeDataURL);
|
|
243
|
-
ctx.drawImage(qrCodeImage, this.options.margin, this.options.margin);
|
|
244
|
-
|
|
245
|
-
// Add logo if specified
|
|
246
|
-
if (this.options.logoPath) {
|
|
247
|
-
await this.addLogo(ctx);
|
|
248
|
-
}
|
|
290
|
+
if (!this.options.data) {
|
|
291
|
+
throw new Error('QR code generation failed: data is required');
|
|
292
|
+
}
|
|
249
293
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
294
|
+
const format = normaliseFormat(this.options.format);
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
if (format === 'svg') {
|
|
298
|
+
if (this.needsCompositing()) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
'SVG output does not support logo, label or watermark compositing. Use png or jpeg instead.'
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return await QRCode.toString(this.options.data, {
|
|
304
|
+
type: 'svg',
|
|
305
|
+
width: this.qrWidth(),
|
|
306
|
+
margin: 0,
|
|
307
|
+
color: this.qrColors(),
|
|
308
|
+
errorCorrectionLevel: this.options.errorCorrectionLevel,
|
|
309
|
+
});
|
|
253
310
|
}
|
|
254
311
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
312
|
+
if (!this.needsCompositing()) {
|
|
313
|
+
return await QRCode.toBuffer(this.options.data, {
|
|
314
|
+
type: 'png',
|
|
315
|
+
width: this.qrWidth(),
|
|
316
|
+
margin: this.marginModules(),
|
|
317
|
+
color: this.qrColors(),
|
|
318
|
+
errorCorrectionLevel: this.options.errorCorrectionLevel,
|
|
319
|
+
});
|
|
258
320
|
}
|
|
259
321
|
|
|
260
|
-
|
|
261
|
-
return canvas.toBuffer(`image/${this.options.format}`);
|
|
322
|
+
return await this.composite(format);
|
|
262
323
|
} catch (error) {
|
|
263
324
|
throw new Error(`QR code generation failed: ${error.message}`);
|
|
264
325
|
}
|
|
265
326
|
}
|
|
266
327
|
|
|
267
328
|
/**
|
|
268
|
-
*
|
|
269
|
-
* @param {
|
|
329
|
+
* Composite the QR code with logo, watermark and label using canvas
|
|
330
|
+
* @param {string} format - Output image format (png or jpeg)
|
|
331
|
+
* @returns {Promise<Buffer>} Image buffer
|
|
270
332
|
*/
|
|
271
|
-
async
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
333
|
+
async composite(format) {
|
|
334
|
+
const { createCanvas, loadImage } = requireCanvas(
|
|
335
|
+
'Rendering a QR code with a logo, label or watermark'
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const size = Number(this.options.size) || 300;
|
|
339
|
+
const margin = Number(this.options.margin) || 0;
|
|
340
|
+
const canvas = createCanvas(size, size);
|
|
341
|
+
const ctx = canvas.getContext('2d');
|
|
342
|
+
|
|
343
|
+
ctx.fillStyle = toHexColor(this.options.backgroundColor, '#ffffff');
|
|
344
|
+
ctx.fillRect(0, 0, size, size);
|
|
345
|
+
|
|
346
|
+
const qrBuffer = await QRCode.toBuffer(this.options.data, {
|
|
347
|
+
type: 'png',
|
|
348
|
+
width: Math.max(1, size - margin * 2),
|
|
349
|
+
margin: 0,
|
|
350
|
+
color: this.qrColors(),
|
|
351
|
+
errorCorrectionLevel: this.options.errorCorrectionLevel,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const qrImage = await loadImage(qrBuffer);
|
|
355
|
+
ctx.drawImage(
|
|
356
|
+
qrImage,
|
|
357
|
+
margin,
|
|
358
|
+
margin,
|
|
359
|
+
size - margin * 2,
|
|
360
|
+
size - margin * 2
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if (this.options.logoPath) {
|
|
364
|
+
await this.addLogo(ctx, loadImage);
|
|
365
|
+
}
|
|
277
366
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
367
|
+
if (this.options.watermark) {
|
|
368
|
+
await this.addWatermark(ctx, loadImage);
|
|
369
|
+
}
|
|
281
370
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
} catch (error) {
|
|
285
|
-
console.warn(`Failed to add logo: ${error.message}`);
|
|
371
|
+
if (this.options.label) {
|
|
372
|
+
this.addLabel(ctx);
|
|
286
373
|
}
|
|
374
|
+
|
|
375
|
+
return format === 'jpeg'
|
|
376
|
+
? canvas.toBuffer('image/jpeg')
|
|
377
|
+
: canvas.toBuffer('image/png');
|
|
287
378
|
}
|
|
288
379
|
|
|
289
380
|
/**
|
|
290
|
-
*
|
|
291
|
-
* @
|
|
381
|
+
* QR code pixel width, excluding margins
|
|
382
|
+
* @returns {number} Width in pixels
|
|
292
383
|
*/
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
watermarkSize,
|
|
309
|
-
watermarkSize
|
|
310
|
-
);
|
|
311
|
-
} else {
|
|
312
|
-
// Text watermark
|
|
313
|
-
ctx.font = `${this.options.labelFontSize}px Arial`;
|
|
314
|
-
ctx.fillStyle = this.rgbToHex(this.options.foregroundColor);
|
|
315
|
-
ctx.textAlign = 'center';
|
|
316
|
-
|
|
317
|
-
const position = this.getWatermarkPosition(0);
|
|
318
|
-
ctx.fillText(this.options.watermark, position.x, position.y);
|
|
319
|
-
}
|
|
320
|
-
} catch (error) {
|
|
321
|
-
console.warn(`Failed to add watermark: ${error.message}`);
|
|
384
|
+
qrWidth() {
|
|
385
|
+
const size = Number(this.options.size) || 300;
|
|
386
|
+
const margin = Number(this.options.margin) || 0;
|
|
387
|
+
return Math.max(21, size - margin * 2);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Margin expressed in QR modules, as expected by the qrcode package
|
|
392
|
+
* @returns {number} Margin in modules
|
|
393
|
+
*/
|
|
394
|
+
marginModules() {
|
|
395
|
+
const size = Number(this.options.size) || 300;
|
|
396
|
+
const margin = Number(this.options.margin) || 0;
|
|
397
|
+
if (!margin) {
|
|
398
|
+
return 0;
|
|
322
399
|
}
|
|
400
|
+
|
|
401
|
+
// Roughly convert a pixel margin into modules (a 25x25 code is typical)
|
|
402
|
+
return Math.max(0, Math.round((margin / Math.max(size, 1)) * 25));
|
|
323
403
|
}
|
|
324
404
|
|
|
325
405
|
/**
|
|
326
|
-
*
|
|
327
|
-
* @
|
|
406
|
+
* Colour options for the qrcode package
|
|
407
|
+
* @returns {{dark: string, light: string}} Colour options
|
|
328
408
|
*/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
409
|
+
qrColors() {
|
|
410
|
+
return {
|
|
411
|
+
dark: toHexColor(this.options.foregroundColor, '#000000'),
|
|
412
|
+
light: toHexColor(this.options.backgroundColor, '#ffffff'),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Draw the logo in the centre of the QR code
|
|
418
|
+
* @param {Object} ctx - Canvas context
|
|
419
|
+
* @param {Function} loadImage - canvas loadImage function
|
|
420
|
+
* @returns {Promise<void>}
|
|
421
|
+
*/
|
|
422
|
+
async addLogo(ctx, loadImage) {
|
|
423
|
+
const logoImage = await loadImage(this.options.logoPath);
|
|
424
|
+
const size = Number(this.options.size) || 300;
|
|
425
|
+
const percent = Math.max(
|
|
426
|
+
1,
|
|
427
|
+
Math.min(40, Number(this.options.logoSize) || 20)
|
|
428
|
+
);
|
|
429
|
+
const logoSize = (size * percent) / 100;
|
|
430
|
+
const logoX = (size - logoSize) / 2;
|
|
431
|
+
const logoY = (size - logoSize) / 2;
|
|
432
|
+
const padding = Math.max(2, Math.round(logoSize * 0.08));
|
|
433
|
+
|
|
434
|
+
ctx.fillStyle = toHexColor(this.options.backgroundColor, '#ffffff');
|
|
435
|
+
ctx.fillRect(
|
|
436
|
+
logoX - padding,
|
|
437
|
+
logoY - padding,
|
|
438
|
+
logoSize + padding * 2,
|
|
439
|
+
logoSize + padding * 2
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Draw the watermark (text or image)
|
|
447
|
+
* @param {Object} ctx - Canvas context
|
|
448
|
+
* @param {Function} loadImage - canvas loadImage function
|
|
449
|
+
* @returns {Promise<void>}
|
|
450
|
+
*/
|
|
451
|
+
async addWatermark(ctx, loadImage) {
|
|
452
|
+
const size = Number(this.options.size) || 300;
|
|
453
|
+
|
|
454
|
+
if (/\.(png|jpe?g|gif|webp|svg)$/i.test(String(this.options.watermark))) {
|
|
455
|
+
const watermarkImage = await loadImage(this.options.watermark);
|
|
456
|
+
const watermarkSize = size / 4;
|
|
457
|
+
const position = this.getWatermarkPosition(watermarkSize);
|
|
458
|
+
|
|
459
|
+
ctx.drawImage(
|
|
460
|
+
watermarkImage,
|
|
461
|
+
position.x,
|
|
462
|
+
position.y,
|
|
463
|
+
watermarkSize,
|
|
464
|
+
watermarkSize
|
|
465
|
+
);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const fontSize = Number(this.options.labelFontSize) || 16;
|
|
470
|
+
ctx.font = `${fontSize}px ${this.options.labelFont || 'sans-serif'}`;
|
|
471
|
+
ctx.fillStyle = toHexColor(this.options.foregroundColor, '#000000');
|
|
332
472
|
ctx.textAlign = 'center';
|
|
473
|
+
ctx.textBaseline = 'middle';
|
|
474
|
+
|
|
475
|
+
const position = this.getWatermarkPosition(0);
|
|
476
|
+
ctx.fillText(String(this.options.watermark), position.x, position.y);
|
|
477
|
+
}
|
|
333
478
|
|
|
334
|
-
|
|
335
|
-
|
|
479
|
+
/**
|
|
480
|
+
* Draw the label along the bottom of the QR code
|
|
481
|
+
* @param {Object} ctx - Canvas context
|
|
482
|
+
*/
|
|
483
|
+
addLabel(ctx) {
|
|
484
|
+
const size = Number(this.options.size) || 300;
|
|
485
|
+
const fontSize = Number(this.options.labelFontSize) || 16;
|
|
486
|
+
|
|
487
|
+
ctx.font = `${fontSize}px ${this.options.labelFont || 'sans-serif'}`;
|
|
488
|
+
ctx.fillStyle = toHexColor(this.options.foregroundColor, '#000000');
|
|
489
|
+
ctx.textAlign = 'center';
|
|
490
|
+
ctx.textBaseline = 'alphabetic';
|
|
491
|
+
ctx.fillText(String(this.options.label), size / 2, size - 10);
|
|
336
492
|
}
|
|
337
493
|
|
|
338
494
|
/**
|
|
339
|
-
*
|
|
340
|
-
* @param {number} size - Watermark size
|
|
341
|
-
* @returns {
|
|
495
|
+
* Resolve watermark coordinates for the configured position
|
|
496
|
+
* @param {number} size - Watermark size in pixels
|
|
497
|
+
* @returns {{x: number, y: number}} Position coordinates
|
|
342
498
|
*/
|
|
343
499
|
getWatermarkPosition(size) {
|
|
500
|
+
const canvasSize = Number(this.options.size) || 300;
|
|
501
|
+
const inset = 10;
|
|
502
|
+
const center = (canvasSize - size) / 2;
|
|
503
|
+
const far = canvasSize - size - inset;
|
|
504
|
+
|
|
344
505
|
const positions = {
|
|
345
|
-
'top-left': { x:
|
|
346
|
-
'top-
|
|
347
|
-
'
|
|
348
|
-
'
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
},
|
|
352
|
-
center: {
|
|
353
|
-
|
|
354
|
-
y: (this.options.size - size) / 2,
|
|
355
|
-
},
|
|
356
|
-
'top-center': { x: (this.options.size - size) / 2, y: 10 },
|
|
357
|
-
'bottom-center': {
|
|
358
|
-
x: (this.options.size - size) / 2,
|
|
359
|
-
y: this.options.size - size - 10,
|
|
360
|
-
},
|
|
361
|
-
'left-center': { x: 10, y: (this.options.size - size) / 2 },
|
|
362
|
-
'right-center': {
|
|
363
|
-
x: this.options.size - size - 10,
|
|
364
|
-
y: (this.options.size - size) / 2,
|
|
365
|
-
},
|
|
506
|
+
'top-left': { x: inset, y: inset },
|
|
507
|
+
'top-center': { x: center, y: inset },
|
|
508
|
+
'top-right': { x: far, y: inset },
|
|
509
|
+
'left-center': { x: inset, y: center },
|
|
510
|
+
center: { x: center, y: center },
|
|
511
|
+
'right-center': { x: far, y: center },
|
|
512
|
+
'bottom-left': { x: inset, y: far },
|
|
513
|
+
'bottom-center': { x: center, y: far },
|
|
514
|
+
'bottom-right': { x: far, y: far },
|
|
366
515
|
};
|
|
367
516
|
|
|
368
517
|
return positions[this.options.watermarkPosition] || positions.center;
|
|
369
518
|
}
|
|
370
519
|
|
|
371
520
|
/**
|
|
372
|
-
* Convert RGB array to hex
|
|
373
|
-
* @param {Array} rgb - RGB
|
|
374
|
-
* @returns {string} Hex
|
|
521
|
+
* Convert an RGB array to a hex colour
|
|
522
|
+
* @param {Array<number>|string} rgb - RGB colour array
|
|
523
|
+
* @returns {string} Hex colour
|
|
375
524
|
*/
|
|
376
525
|
rgbToHex(rgb) {
|
|
377
|
-
return
|
|
526
|
+
return toHexColor(rgb, '#000000');
|
|
378
527
|
}
|
|
379
528
|
|
|
380
529
|
/**
|
|
381
|
-
* Save QR code to file
|
|
530
|
+
* Save the QR code to a file
|
|
382
531
|
* @param {string} filePath - Output file path
|
|
383
|
-
* @returns {Promise<
|
|
532
|
+
* @returns {Promise<string>} The path the file was written to
|
|
384
533
|
*/
|
|
385
534
|
async saveToFile(filePath) {
|
|
386
|
-
if (!
|
|
535
|
+
if (!filePath) {
|
|
536
|
+
throw new Error('A file path is required');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
let fs;
|
|
540
|
+
try {
|
|
541
|
+
fs = require('fs').promises;
|
|
542
|
+
} catch {
|
|
387
543
|
throw new Error(
|
|
388
|
-
'File system operations are not available in
|
|
544
|
+
'File system operations are not available in this environment. Use generate() or getDataUri() instead.'
|
|
389
545
|
);
|
|
390
546
|
}
|
|
391
|
-
|
|
392
|
-
await fs.writeFile(filePath,
|
|
547
|
+
|
|
548
|
+
await fs.writeFile(filePath, await this.generate());
|
|
549
|
+
return filePath;
|
|
393
550
|
}
|
|
394
551
|
|
|
395
552
|
/**
|
|
396
|
-
* Get QR code as data URI
|
|
553
|
+
* Get the QR code as a data URI
|
|
397
554
|
* @returns {Promise<string>} Data URI
|
|
398
555
|
*/
|
|
399
556
|
async getDataUri() {
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
557
|
+
const format = normaliseFormat(this.options.format);
|
|
558
|
+
const output = await this.generate();
|
|
559
|
+
|
|
560
|
+
if (format === 'svg') {
|
|
561
|
+
return `data:image/svg+xml;base64,${Buffer.from(output).toString('base64')}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return `data:image/${format};base64,${output.toString('base64')}`;
|
|
403
565
|
}
|
|
404
566
|
|
|
405
567
|
/**
|
|
406
|
-
* Get QR code
|
|
407
|
-
* @returns {Promise<string>} QR code
|
|
568
|
+
* Get the QR code output (alias of generate)
|
|
569
|
+
* @returns {Promise<Buffer|string>} QR code output
|
|
408
570
|
*/
|
|
409
571
|
async getString() {
|
|
410
|
-
return
|
|
572
|
+
return this.generate();
|
|
411
573
|
}
|
|
412
574
|
}
|
|
413
575
|
|
|
414
576
|
module.exports = QrCodeBuilder;
|
|
577
|
+
module.exports.QrCodeResult = QrCodeResult;
|