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.
@@ -1,28 +1,22 @@
1
1
  /**
2
- * PDF Renderer - Renders barcodes as PDF
2
+ * PDF Renderer - Renders barcodes as PDF documents
3
+ *
4
+ * The barcode itself is encoded as a real symbology (see BarcodeEncoder) and
5
+ * embedded as a lossless PNG image, so the PDF is scannable.
3
6
  */
4
7
 
5
8
  const PDFDocument = require('pdfkit');
9
+ const { BarcodeEncoder } = require('../encoders/BarcodeEncoder');
6
10
 
7
11
  class PDFRenderer {
8
12
  constructor() {
9
13
  this.defaultOptions = {
10
- width: 2,
11
- height: 100,
12
- displayValue: true,
13
- fontSize: 20,
14
- textAlign: 'center',
15
- textPosition: 'bottom',
16
- textMargin: 2,
17
- background: '#ffffff',
18
- lineColor: '#000000',
19
- margin: 10,
20
- marginTop: 10,
21
- marginBottom: 10,
22
- marginLeft: 10,
23
- marginRight: 10,
24
- pageWidth: 612,
14
+ ...BarcodeEncoder.getDefaultOptions(),
15
+ pageWidth: 612, // US Letter at 72dpi
25
16
  pageHeight: 792,
17
+ pageSize: null, // e.g. 'A4' - takes precedence over pageWidth/pageHeight
18
+ fitToBarcode: false, // size the page to the barcode plus margins
19
+ title: null,
26
20
  };
27
21
  }
28
22
 
@@ -31,137 +25,93 @@ class PDFRenderer {
31
25
  * @param {string} data - The data to encode
32
26
  * @param {string} type - The barcode type
33
27
  * @param {Object} options - Render options
34
- * @returns {Buffer} PDF buffer
28
+ * @returns {Promise<Buffer>} PDF buffer
35
29
  */
36
- render(data, type, options = {}) {
37
- try {
38
- // Merge options with defaults
39
- const renderOptions = { ...this.defaultOptions, ...options };
40
-
41
- // Create PDF document
42
- const doc = new PDFDocument({
43
- size: [renderOptions.pageWidth, renderOptions.pageHeight],
44
- margins: {
45
- top: renderOptions.marginTop,
46
- bottom: renderOptions.marginBottom,
47
- left: renderOptions.marginLeft,
48
- right: renderOptions.marginRight,
49
- },
50
- });
51
-
52
- // Collect PDF data
53
- const chunks = [];
54
- doc.on('data', chunk => chunks.push(chunk));
55
-
56
- return new Promise((resolve, reject) => {
57
- doc.on('end', () => {
58
- const pdfBuffer = Buffer.concat(chunks);
59
- resolve(pdfBuffer);
60
- });
30
+ async render(data, type, options = {}) {
31
+ const renderOptions = { ...this.defaultOptions, ...options };
61
32
 
62
- doc.on('error', reject);
63
-
64
- // Add barcode to PDF
65
- this.addBarcodeToPDF(doc, data, type, renderOptions);
33
+ let png;
34
+ let dimensions;
35
+ try {
36
+ png = await BarcodeEncoder.toPng(data, type, renderOptions);
37
+ dimensions = readPngSize(png);
38
+ } catch (error) {
39
+ throw new Error(`PDF rendering failed: ${error.message}`);
40
+ }
66
41
 
67
- // Finalize PDF
68
- doc.end();
69
- });
42
+ try {
43
+ return await this.buildDocument(png, dimensions, renderOptions);
70
44
  } catch (error) {
71
45
  throw new Error(`PDF rendering failed: ${error.message}`);
72
46
  }
73
47
  }
74
48
 
75
49
  /**
76
- * Add barcode to PDF document
77
- * @param {PDFDocument} doc - PDF document
78
- * @param {string} data - The data to encode
79
- * @param {string} type - The barcode type
50
+ * Build the PDF document around an encoded barcode image
51
+ * @param {Buffer} png - Encoded barcode as PNG
52
+ * @param {{width: number, height: number}} dimensions - Image size in pixels
80
53
  * @param {Object} options - Render options
54
+ * @returns {Promise<Buffer>} PDF buffer
81
55
  */
82
- addBarcodeToPDF(doc, data, type, options) {
83
- // Set background color
84
- doc.rect(0, 0, options.pageWidth, options.pageHeight);
85
- doc.fillColor(options.background);
86
- doc.fill();
87
-
88
- // Calculate position
89
- const x = options.marginLeft;
90
- const y = options.marginTop;
91
-
92
- if (type === 'qrcode') {
93
- this.addQRCodeToPDF(doc, data, x, y, options);
94
- } else {
95
- this.addLinearBarcodeToPDF(doc, data, type, x, y, options);
96
- }
56
+ buildDocument(png, dimensions, options) {
57
+ const margins = {
58
+ top: fallbackNumber(options.marginTop, options.margin, 10),
59
+ bottom: fallbackNumber(options.marginBottom, options.margin, 10),
60
+ left: fallbackNumber(options.marginLeft, options.margin, 10),
61
+ right: fallbackNumber(options.marginRight, options.margin, 10),
62
+ };
97
63
 
98
- // Add text if displayValue is true
99
- if (options.displayValue) {
100
- doc.fontSize(options.fontSize);
101
- doc.fillColor(options.lineColor);
102
- doc.text(data, x, y + options.height + options.textMargin, {
103
- align: options.textAlign,
104
- });
64
+ const pageSetup = options.fitToBarcode
65
+ ? {
66
+ size: [
67
+ dimensions.width + margins.left + margins.right,
68
+ dimensions.height + margins.top + margins.bottom,
69
+ ],
70
+ }
71
+ : {
72
+ size: options.pageSize || [options.pageWidth, options.pageHeight],
73
+ };
74
+
75
+ const doc = new PDFDocument({ ...pageSetup, margins, autoFirstPage: true });
76
+
77
+ if (options.title) {
78
+ doc.info.Title = options.title;
105
79
  }
106
- }
107
80
 
108
- /**
109
- * Add QR Code to PDF
110
- * @param {PDFDocument} doc - PDF document
111
- * @param {string} data - The data to encode
112
- * @param {number} x - X position
113
- * @param {number} y - Y position
114
- * @param {Object} options - Render options
115
- */
116
- addQRCodeToPDF(doc, data, x, y, options) {
117
- // This is a simplified representation
118
- // In a real implementation, you would use a QR code library
119
- const size = 200;
120
- const cellSize = 10;
121
- const cells = size / cellSize;
122
-
123
- // Draw QR code pattern
124
- for (let row = 0; row < cells; row++) {
125
- for (let col = 0; col < cells; col++) {
126
- const shouldFill =
127
- (data.charCodeAt((row * cells + col) % data.length) + row + col) %
128
- 2 ===
129
- 0;
130
-
131
- if (shouldFill) {
132
- doc.rect(x + col * cellSize, y + row * cellSize, cellSize, cellSize);
133
- doc.fillColor(options.lineColor);
134
- doc.fill();
81
+ const chunks = [];
82
+ doc.on('data', chunk => chunks.push(chunk));
83
+
84
+ return new Promise((resolve, reject) => {
85
+ doc.on('end', () => resolve(Buffer.concat(chunks)));
86
+ doc.on('error', reject);
87
+
88
+ try {
89
+ const pageWidth = doc.page.width;
90
+ const pageHeight = doc.page.height;
91
+ const availableWidth = pageWidth - margins.left - margins.right;
92
+ const availableHeight = pageHeight - margins.top - margins.bottom;
93
+
94
+ if (options.background) {
95
+ doc.rect(0, 0, pageWidth, pageHeight).fill(options.background);
135
96
  }
136
- }
137
- }
138
- }
139
97
 
140
- /**
141
- * Add linear barcode to PDF
142
- * @param {PDFDocument} doc - PDF document
143
- * @param {string} data - The data to encode
144
- * @param {string} type - The barcode type
145
- * @param {number} x - X position
146
- * @param {number} y - Y position
147
- * @param {Object} options - Render options
148
- */
149
- addLinearBarcodeToPDF(doc, data, type, x, y, options) {
150
- const barWidth = options.width || 2;
151
- let currentX = x;
152
-
153
- // Draw bars based on data
154
- for (let i = 0; i < data.length; i++) {
155
- const char = data.charCodeAt(i);
156
- const barCount = (char % 5) + 1;
157
-
158
- for (let j = 0; j < barCount; j++) {
159
- doc.rect(currentX, y, barWidth, options.height);
160
- doc.fillColor(options.lineColor);
161
- doc.fill();
162
- currentX += barWidth * 2;
98
+ // Never upscale - a barcode drawn larger than encoded loses crispness
99
+ const scale = Math.min(
100
+ 1,
101
+ availableWidth / dimensions.width,
102
+ availableHeight / dimensions.height
103
+ );
104
+
105
+ doc.image(png, margins.left, margins.top, {
106
+ width: dimensions.width * scale,
107
+ height: dimensions.height * scale,
108
+ });
109
+
110
+ doc.end();
111
+ } catch (error) {
112
+ reject(error);
163
113
  }
164
- }
114
+ });
165
115
  }
166
116
 
167
117
  /**
@@ -181,4 +131,32 @@ class PDFRenderer {
181
131
  }
182
132
  }
183
133
 
134
+ /**
135
+ * Read the pixel dimensions out of a PNG buffer's IHDR chunk
136
+ * @param {Buffer} png - PNG buffer
137
+ * @returns {{width: number, height: number}} Dimensions
138
+ */
139
+ function readPngSize(png) {
140
+ if (!Buffer.isBuffer(png) || png.length < 24) {
141
+ throw new Error('Encoded barcode is not a valid PNG buffer');
142
+ }
143
+
144
+ return { width: png.readUInt32BE(16), height: png.readUInt32BE(20) };
145
+ }
146
+
147
+ /**
148
+ * First finite number out of the candidates
149
+ * @param {...*} values - Candidate values
150
+ * @returns {number} The first usable number, or 0
151
+ */
152
+ function fallbackNumber(...values) {
153
+ for (const value of values) {
154
+ const num = Number(value);
155
+ if (Number.isFinite(num)) {
156
+ return num;
157
+ }
158
+ }
159
+ return 0;
160
+ }
161
+
184
162
  module.exports = PDFRenderer;
@@ -2,27 +2,11 @@
2
2
  * PNG Renderer - Renders barcodes as PNG images
3
3
  */
4
4
 
5
- const { createCanvas } = require('canvas');
6
- const JsBarcode = require('jsbarcode');
5
+ const { BarcodeEncoder } = require('../encoders/BarcodeEncoder');
7
6
 
8
7
  class PNGRenderer {
9
8
  constructor() {
10
- this.defaultOptions = {
11
- width: 2,
12
- height: 100,
13
- displayValue: true,
14
- fontSize: 20,
15
- textAlign: 'center',
16
- textPosition: 'bottom',
17
- textMargin: 2,
18
- background: '#ffffff',
19
- lineColor: '#000000',
20
- margin: 10,
21
- marginTop: 10,
22
- marginBottom: 10,
23
- marginLeft: 10,
24
- marginRight: 10,
25
- };
9
+ this.defaultOptions = BarcodeEncoder.getDefaultOptions();
26
10
  }
27
11
 
28
12
  /**
@@ -30,79 +14,19 @@ class PNGRenderer {
30
14
  * @param {string} data - The data to encode
31
15
  * @param {string} type - The barcode type
32
16
  * @param {Object} options - Render options
33
- * @returns {Buffer} PNG buffer
17
+ * @returns {Promise<Buffer>} PNG buffer
34
18
  */
35
- render(data, type, options = {}) {
19
+ async render(data, type, options = {}) {
36
20
  try {
37
- // Merge options with defaults
38
- const renderOptions = { ...this.defaultOptions, ...options };
39
-
40
- // Create canvas
41
- const canvas = createCanvas(400, 200);
42
- const ctx = canvas.getContext('2d');
43
-
44
- // Set background
45
- ctx.fillStyle = renderOptions.background;
46
- ctx.fillRect(0, 0, canvas.width, canvas.height);
47
-
48
- // Generate barcode using JsBarcode
49
- JsBarcode(canvas, data, {
50
- format: this.mapBarcodeType(type),
51
- width: renderOptions.width,
52
- height: renderOptions.height,
53
- displayValue: renderOptions.displayValue,
54
- fontSize: renderOptions.fontSize,
55
- textAlign: renderOptions.textAlign,
56
- textPosition: renderOptions.textPosition,
57
- textMargin: renderOptions.textMargin,
58
- background: renderOptions.background,
59
- lineColor: renderOptions.lineColor,
60
- margin: renderOptions.margin,
61
- marginTop: renderOptions.marginTop,
62
- marginBottom: renderOptions.marginBottom,
63
- marginLeft: renderOptions.marginLeft,
64
- marginRight: renderOptions.marginRight,
21
+ return await BarcodeEncoder.toPng(data, type, {
22
+ ...this.defaultOptions,
23
+ ...options,
65
24
  });
66
-
67
- // Convert to PNG buffer
68
- return canvas.toBuffer('image/png');
69
25
  } catch (error) {
70
26
  throw new Error(`PNG rendering failed: ${error.message}`);
71
27
  }
72
28
  }
73
29
 
74
- /**
75
- * Map barcode type to JsBarcode format
76
- * @param {string} type - The barcode type
77
- * @returns {string} JsBarcode format
78
- */
79
- mapBarcodeType(type) {
80
- const typeMap = {
81
- code128: 'CODE128',
82
- code128a: 'CODE128A',
83
- code128b: 'CODE128B',
84
- code128c: 'CODE128C',
85
- code128auto: 'CODE128',
86
- code39: 'CODE39',
87
- code39extended: 'CODE39',
88
- code39checksum: 'CODE39',
89
- code39auto: 'CODE39',
90
- code93: 'CODE93',
91
- ean13: 'EAN13',
92
- ean8: 'EAN8',
93
- upca: 'UPC',
94
- upce: 'UPC',
95
- codabar: 'codabar',
96
- code11: 'CODE11',
97
- msi: 'MSI',
98
- pharmazentral: 'pharmazentral',
99
- interleaved25: 'ITF',
100
- standard25: 'STD25',
101
- };
102
-
103
- return typeMap[type] || 'CODE128';
104
- }
105
-
106
30
  /**
107
31
  * Get default options
108
32
  * @returns {Object} Default options
@@ -2,40 +2,75 @@
2
2
  * Render Formats - Supported output formats
3
3
  */
4
4
 
5
+ const FORMAT_REGISTRY = {
6
+ png: {
7
+ name: 'PNG',
8
+ mimeType: 'image/png',
9
+ extension: '.png',
10
+ returns: 'Buffer',
11
+ description: 'Portable Network Graphics - Raster image format',
12
+ },
13
+ svg: {
14
+ name: 'SVG',
15
+ mimeType: 'image/svg+xml',
16
+ extension: '.svg',
17
+ returns: 'string',
18
+ description: 'Scalable Vector Graphics - Vector image format',
19
+ },
20
+ html: {
21
+ name: 'HTML',
22
+ mimeType: 'text/html',
23
+ extension: '.html',
24
+ returns: 'string',
25
+ description: 'HTML fragment wrapping an inline SVG barcode',
26
+ },
27
+ pdf: {
28
+ name: 'PDF',
29
+ mimeType: 'application/pdf',
30
+ extension: '.pdf',
31
+ returns: 'Buffer',
32
+ description: 'Portable Document Format - Document format',
33
+ },
34
+ };
35
+
36
+ const FORMATS = Object.keys(FORMAT_REGISTRY).reduce((acc, format) => {
37
+ acc[format.toUpperCase()] = format;
38
+ return acc;
39
+ }, {});
40
+
5
41
  class RenderFormats {
6
- static FORMATS = {
7
- PNG: 'png',
8
- SVG: 'svg',
9
- HTML: 'html',
10
- PDF: 'pdf',
11
- JPG: 'jpg',
12
- JPEG: 'jpeg',
13
- };
42
+ static FORMATS = FORMATS;
14
43
 
15
- static MIME_TYPES = {
16
- png: 'image/png',
17
- svg: 'image/svg+xml',
18
- html: 'text/html',
19
- pdf: 'application/pdf',
20
- jpg: 'image/jpeg',
21
- jpeg: 'image/jpeg',
22
- };
44
+ static MIME_TYPES = Object.entries(FORMAT_REGISTRY).reduce(
45
+ (acc, [format, info]) => {
46
+ acc[format] = info.mimeType;
47
+ return acc;
48
+ },
49
+ {}
50
+ );
23
51
 
24
- static EXTENSIONS = {
25
- png: '.png',
26
- svg: '.svg',
27
- html: '.html',
28
- pdf: '.pdf',
29
- jpg: '.jpg',
30
- jpeg: '.jpeg',
31
- };
52
+ static EXTENSIONS = Object.entries(FORMAT_REGISTRY).reduce(
53
+ (acc, [format, info]) => {
54
+ acc[format] = info.extension;
55
+ return acc;
56
+ },
57
+ {}
58
+ );
32
59
 
33
60
  /**
34
61
  * Get all supported formats
35
- * @returns {Array} Array of all formats
62
+ * @returns {string[]} Array of all formats
36
63
  */
37
64
  static getAll() {
38
- return Object.values(this.FORMATS);
65
+ return Object.keys(FORMAT_REGISTRY);
66
+ }
67
+
68
+ /**
69
+ * Get all supported formats (alias of getAll)
70
+ * @returns {string[]} Array of all formats
71
+ */
72
+ static getFormats() {
73
+ return this.getAll();
39
74
  }
40
75
 
41
76
  /**
@@ -44,7 +79,7 @@ class RenderFormats {
44
79
  * @returns {boolean} True if valid
45
80
  */
46
81
  static isValid(format) {
47
- return Object.values(this.FORMATS).includes(format);
82
+ return typeof format === 'string' && Object.hasOwn(FORMAT_REGISTRY, format);
48
83
  }
49
84
 
50
85
  /**
@@ -53,7 +88,9 @@ class RenderFormats {
53
88
  * @returns {string} MIME type
54
89
  */
55
90
  static getMimeType(format) {
56
- return this.MIME_TYPES[format] || 'application/octet-stream';
91
+ return FORMAT_REGISTRY[format]
92
+ ? FORMAT_REGISTRY[format].mimeType
93
+ : 'application/octet-stream';
57
94
  }
58
95
 
59
96
  /**
@@ -62,7 +99,7 @@ class RenderFormats {
62
99
  * @returns {string} File extension
63
100
  */
64
101
  static getExtension(format) {
65
- return this.EXTENSIONS[format] || '';
102
+ return FORMAT_REGISTRY[format] ? FORMAT_REGISTRY[format].extension : '';
66
103
  }
67
104
 
68
105
  /**
@@ -71,16 +108,30 @@ class RenderFormats {
71
108
  * @returns {string} Description
72
109
  */
73
110
  static getDescription(format) {
74
- const descriptions = {
75
- png: 'Portable Network Graphics - Raster image format',
76
- svg: 'Scalable Vector Graphics - Vector image format',
77
- html: 'HyperText Markup Language - Web page format',
78
- pdf: 'Portable Document Format - Document format',
79
- jpg: 'JPEG - Compressed raster image format',
80
- jpeg: 'JPEG - Compressed raster image format',
81
- };
111
+ return FORMAT_REGISTRY[format]
112
+ ? FORMAT_REGISTRY[format].description
113
+ : 'Unknown format';
114
+ }
115
+
116
+ /**
117
+ * Get full information about a format
118
+ * @param {string} format - The format
119
+ * @returns {Object|null} Format information, or null when unknown
120
+ */
121
+ static getFormatInfo(format) {
122
+ const info = FORMAT_REGISTRY[format];
123
+ if (!info) {
124
+ return null;
125
+ }
82
126
 
83
- return descriptions[format] || 'Unknown format';
127
+ return {
128
+ format,
129
+ name: info.name,
130
+ mimeType: info.mimeType,
131
+ extension: info.extension,
132
+ returns: info.returns,
133
+ description: info.description,
134
+ };
84
135
  }
85
136
  }
86
137