isahaq-barcode 1.9.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.
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Barcode Encoder - Turns data into a real, scannable symbology
3
+ *
4
+ * All renderers go through this module so that PNG, SVG, HTML and PDF output
5
+ * describe the exact same encoding. Encoding is done by bwip-js (BWIPP), which
6
+ * covers every type listed in BarcodeTypes.
7
+ */
8
+
9
+ const bwipjs = require('bwip-js');
10
+ const { BarcodeTypes } = require('../types/BarcodeTypes');
11
+
12
+ /** bwip-js expresses bar height in millimetres at 72dpi */
13
+ const PX_PER_MM = 72 / 25.4;
14
+
15
+ /** Types whose size is driven by the module scale rather than a bar height */
16
+ const MATRIX_CATEGORIES = new Set(['MATRIX_2D', 'STACKED']);
17
+
18
+ /**
19
+ * Quiet zone in modules used when the caller does not specify a margin.
20
+ * Most 1D specifications require at least 10 narrow modules of clear space;
21
+ * scanners reject codes with less.
22
+ */
23
+ const DEFAULT_QUIET_ZONE_MODULES = 10;
24
+
25
+ const DEFAULT_OPTIONS = {
26
+ width: 2, // module (narrow bar) width in pixels
27
+ height: 100, // bar height in pixels - 1D symbologies only
28
+ displayValue: true, // print the human readable text
29
+ fontSize: 20, // human readable text size in pixels
30
+ textAlign: 'center', // center | left | right | justify
31
+ textMargin: 2, // gap between bars and text in pixels
32
+ background: '#ffffff',
33
+ lineColor: '#000000',
34
+ margin: null, // quiet zone in pixels; defaults to 10 modules when omitted
35
+ marginTop: null,
36
+ marginBottom: null,
37
+ marginLeft: null,
38
+ marginRight: null,
39
+ quietZone: DEFAULT_QUIET_ZONE_MODULES, // quiet zone in modules
40
+ rotate: 'N', // N | R | L | I (none, right, left, inverted)
41
+ size: null, // target size in pixels for 2D symbologies
42
+ };
43
+
44
+ /**
45
+ * Strip the internal BWIPP error prefix so messages read like normal errors.
46
+ * "bwipp.ean13badLength#6878: EAN-13 must be 12 or 13 digits" -> "EAN-13 must be 12 or 13 digits"
47
+ * @param {Error|string} error - Error thrown by the encoder
48
+ * @returns {string} Human readable message
49
+ */
50
+ function cleanMessage(error) {
51
+ const message = error && error.message ? error.message : String(error);
52
+ return message.replace(/^bwipp?\.[A-Za-z0-9]+(#\d+)?:\s*/, '');
53
+ }
54
+
55
+ /**
56
+ * Normalise a colour to the hex form bwip-js expects.
57
+ * @param {string} color - Colour as #rgb, #rrggbb or rrggbb
58
+ * @param {string} fallback - Value used when color is missing/invalid
59
+ * @returns {string} Hex colour without the leading '#'
60
+ */
61
+ function toHex(color, fallback) {
62
+ const raw = String(color == null ? fallback : color)
63
+ .replace(/^#/, '')
64
+ .toLowerCase();
65
+
66
+ if (/^[0-9a-fA-F]{3}$/.test(raw)) {
67
+ return raw
68
+ .split('')
69
+ .map(c => c + c)
70
+ .join('');
71
+ }
72
+
73
+ if (/^[0-9a-fA-F]{6}$/.test(raw) || /^[0-9a-fA-F]{8}$/.test(raw)) {
74
+ return raw;
75
+ }
76
+
77
+ return String(fallback).replace(/^#/, '');
78
+ }
79
+
80
+ /**
81
+ * Pick a positive number, falling back when the value is unusable.
82
+ * @param {*} value - Candidate value
83
+ * @param {number} fallback - Fallback value
84
+ * @returns {number} A finite positive number
85
+ */
86
+ function positive(value, fallback) {
87
+ const num = Number(value);
88
+ return Number.isFinite(num) && num > 0 ? num : fallback;
89
+ }
90
+
91
+ class BarcodeEncoder {
92
+ /**
93
+ * Get the default render options
94
+ * @returns {Object} Default options
95
+ */
96
+ static getDefaultOptions() {
97
+ return { ...DEFAULT_OPTIONS };
98
+ }
99
+
100
+ /**
101
+ * Check whether a type is a 2D/stacked symbology (sized by scale, not height)
102
+ * @param {string} type - The barcode type
103
+ * @returns {boolean} True for matrix and stacked symbologies
104
+ */
105
+ static isMatrix(type) {
106
+ const info = BarcodeTypes.getTypeInfo(type);
107
+ return !!info && MATRIX_CATEGORIES.has(info.category);
108
+ }
109
+
110
+ /**
111
+ * Translate this package's options into bwip-js encoder options
112
+ * @param {string} data - The data to encode
113
+ * @param {string} type - The barcode type
114
+ * @param {Object} options - Render options
115
+ * @returns {Object} bwip-js options
116
+ */
117
+ static buildOptions(data, type, options = {}) {
118
+ const bcid = BarcodeTypes.getSymbology(type);
119
+ if (!bcid) {
120
+ throw new Error(`Invalid barcode type: ${type}`);
121
+ }
122
+
123
+ const merged = { ...DEFAULT_OPTIONS, ...options };
124
+ const scale = Math.max(1, Math.round(positive(merged.width, 2)));
125
+
126
+ const encoded = {
127
+ bcid,
128
+ text: data,
129
+ scale,
130
+ includetext: merged.displayValue !== false,
131
+ textxalign: merged.textAlign || 'center',
132
+ textsize: Math.max(1, positive(merged.fontSize, 20) / scale),
133
+ textyoffset: Number(merged.textMargin) || 0,
134
+ barcolor: toHex(merged.lineColor, '#000000'),
135
+ backgroundcolor: toHex(merged.background, '#ffffff'),
136
+ textcolor: toHex(merged.textColor || merged.lineColor, '#000000'),
137
+ rotate: ['N', 'R', 'L', 'I'].includes(merged.rotate)
138
+ ? merged.rotate
139
+ : 'N',
140
+ ...BarcodeTypes.getEncodeOptions(type),
141
+ };
142
+
143
+ // Quiet zone. bwip-js padding is expressed in modules (it multiplies by
144
+ // scale), our public options are pixels. When no margin is given we fall
145
+ // back to the module count the specifications ask for.
146
+ const quietZone = Math.max(
147
+ 0,
148
+ Math.round(
149
+ merged.quietZone === undefined || merged.quietZone === null
150
+ ? DEFAULT_QUIET_ZONE_MODULES
151
+ : Number(merged.quietZone) || 0
152
+ )
153
+ );
154
+ const pad = value => {
155
+ const pixels =
156
+ value === undefined || value === null ? merged.margin : value;
157
+ if (pixels === undefined || pixels === null) {
158
+ return quietZone;
159
+ }
160
+ return Math.max(0, Math.round((Number(pixels) || 0) / scale));
161
+ };
162
+ encoded.paddingleft = pad(merged.marginLeft);
163
+ encoded.paddingright = pad(merged.marginRight);
164
+ encoded.paddingtop = pad(merged.marginTop);
165
+ encoded.paddingbottom = pad(merged.marginBottom);
166
+
167
+ if (this.isMatrix(type)) {
168
+ // 2D symbologies keep their own aspect ratio; an explicit target size is
169
+ // honoured by scaling the module size instead of stretching bars.
170
+ if (merged.size) {
171
+ encoded.scale = this.scaleForSize(
172
+ { ...encoded, scale: 1 },
173
+ positive(merged.size, 300)
174
+ );
175
+ }
176
+ } else {
177
+ encoded.height = positive(merged.height, 100) / (scale * PX_PER_MM);
178
+ }
179
+
180
+ // Allow advanced callers to hand raw bwip-js options through untouched
181
+ if (options.bwipOptions && typeof options.bwipOptions === 'object') {
182
+ Object.assign(encoded, options.bwipOptions);
183
+ }
184
+
185
+ return encoded;
186
+ }
187
+
188
+ /**
189
+ * Work out the module scale needed to reach a target pixel size
190
+ * @param {Object} baseOptions - bwip-js options rendered at scale 1
191
+ * @param {number} targetPx - Desired width/height in pixels
192
+ * @returns {number} Module scale
193
+ */
194
+ static scaleForSize(baseOptions, targetPx) {
195
+ try {
196
+ const svg = bwipjs.toSVG(baseOptions);
197
+ const viewBox = /viewBox="0 0 ([\d.]+) ([\d.]+)"/.exec(svg);
198
+ const baseSize = viewBox
199
+ ? Math.max(Number(viewBox[1]), Number(viewBox[2]))
200
+ : 0;
201
+
202
+ if (!baseSize) {
203
+ return 4;
204
+ }
205
+
206
+ return Math.max(1, Math.round(targetPx / baseSize));
207
+ } catch {
208
+ return 4;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Encode as an SVG string (synchronous)
214
+ * @param {string} data - The data to encode
215
+ * @param {string} type - The barcode type
216
+ * @param {Object} options - Render options
217
+ * @returns {string} SVG markup
218
+ */
219
+ static toSvg(data, type, options = {}) {
220
+ try {
221
+ return bwipjs.toSVG(this.buildOptions(data, type, options));
222
+ } catch (error) {
223
+ throw new Error(`Failed to encode ${type}: ${cleanMessage(error)}`);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Encode as a PNG buffer
229
+ * @param {string} data - The data to encode
230
+ * @param {string} type - The barcode type
231
+ * @param {Object} options - Render options
232
+ * @returns {Promise<Buffer>} PNG buffer
233
+ */
234
+ static async toPng(data, type, options = {}) {
235
+ try {
236
+ return await bwipjs.toBuffer(this.buildOptions(data, type, options));
237
+ } catch (error) {
238
+ throw new Error(`Failed to encode ${type}: ${cleanMessage(error)}`);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Get the pixel dimensions an encoding will produce
244
+ * @param {string} data - The data to encode
245
+ * @param {string} type - The barcode type
246
+ * @param {Object} options - Render options
247
+ * @returns {{width: number, height: number}} Dimensions in pixels
248
+ */
249
+ static getDimensions(data, type, options = {}) {
250
+ const svg = this.toSvg(data, type, options);
251
+ const viewBox = /viewBox="0 0 ([\d.]+) ([\d.]+)"/.exec(svg);
252
+
253
+ return {
254
+ width: viewBox ? Math.round(Number(viewBox[1])) : 0,
255
+ height: viewBox ? Math.round(Number(viewBox[2])) : 0,
256
+ };
257
+ }
258
+ }
259
+
260
+ module.exports = { BarcodeEncoder, cleanMessage };
@@ -1,26 +1,18 @@
1
1
  /**
2
- * HTML Renderer - Renders barcodes as HTML
2
+ * HTML Renderer - Renders barcodes as an HTML fragment wrapping a real SVG
3
3
  */
4
4
 
5
+ const { BarcodeEncoder } = require('../encoders/BarcodeEncoder');
6
+
7
+ let counter = 0;
8
+
5
9
  class HTMLRenderer {
6
10
  constructor() {
7
11
  this.defaultOptions = {
8
- width: 2,
9
- height: 100,
10
- displayValue: true,
11
- fontSize: 20,
12
- textAlign: 'center',
13
- textPosition: 'bottom',
14
- textMargin: 2,
15
- background: '#ffffff',
16
- lineColor: '#000000',
17
- margin: 10,
18
- marginTop: 10,
19
- marginBottom: 10,
20
- marginLeft: 10,
21
- marginRight: 10,
12
+ ...BarcodeEncoder.getDefaultOptions(),
22
13
  className: 'barcode',
23
14
  id: null,
15
+ includeStyles: true,
24
16
  };
25
17
  }
26
18
 
@@ -29,169 +21,31 @@ class HTMLRenderer {
29
21
  * @param {string} data - The data to encode
30
22
  * @param {string} type - The barcode type
31
23
  * @param {Object} options - Render options
32
- * @returns {string} HTML string
24
+ * @returns {string} HTML markup
33
25
  */
34
26
  render(data, type, options = {}) {
35
27
  try {
36
- // Merge options with defaults
37
28
  const renderOptions = { ...this.defaultOptions, ...options };
38
-
39
- // Generate HTML structure
40
- const html = this.createHTMLStructure(data, type, renderOptions);
41
- return html;
29
+ const svg = BarcodeEncoder.toSvg(data, type, renderOptions);
30
+ const id = renderOptions.id || `barcode-${++counter}`;
31
+ const className = renderOptions.className || 'barcode';
32
+
33
+ const styles = renderOptions.includeStyles
34
+ ? ` style="display:inline-block;background:${escapeAttribute(
35
+ renderOptions.background
36
+ )};text-align:center"`
37
+ : '';
38
+
39
+ return (
40
+ `<div id="${escapeAttribute(id)}" class="${escapeAttribute(className)}"` +
41
+ `${styles} data-barcode-type="${escapeAttribute(type)}"` +
42
+ ` data-barcode-value="${escapeAttribute(data)}">${svg}</div>`
43
+ );
42
44
  } catch (error) {
43
45
  throw new Error(`HTML rendering failed: ${error.message}`);
44
46
  }
45
47
  }
46
48
 
47
- /**
48
- * Create HTML structure for barcode
49
- * @param {string} data - The data to encode
50
- * @param {string} type - The barcode type
51
- * @param {Object} options - Render options
52
- * @returns {string} HTML string
53
- */
54
- createHTMLStructure(data, type, options) {
55
- const containerId =
56
- options.id ||
57
- `barcode-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
58
- const containerClass = options.className || 'barcode';
59
-
60
- let html = `<div id="${containerId}" class="${containerClass}" style="${this.getContainerStyles(
61
- options
62
- )}">`;
63
-
64
- // Add barcode representation
65
- if (type === 'qrcode') {
66
- html += this.createQRCodeHTML(data, options);
67
- } else {
68
- html += this.createLinearBarcodeHTML(data, type, options);
69
- }
70
-
71
- // Add text if displayValue is true
72
- if (options.displayValue) {
73
- html += `<div class="barcode-text" style="${this.getTextStyles(
74
- options
75
- )}">${data}</div>`;
76
- }
77
-
78
- html += '</div>';
79
-
80
- // Add CSS styles
81
- html += this.getCSSStyles(containerId, options);
82
-
83
- return html;
84
- }
85
-
86
- /**
87
- * Create QR Code HTML representation
88
- * @param {string} data - The data to encode
89
- * @param {Object} options - Render options
90
- * @returns {string} HTML content
91
- */
92
- createQRCodeHTML(data, options) {
93
- // This is a simplified representation
94
- // In a real implementation, you would use a QR code library
95
- const size = 200;
96
- const cellSize = 10;
97
- const cells = size / cellSize;
98
-
99
- let html = `<div class="qr-code" style="width: ${size}px; height: ${size}px; display: grid; grid-template-columns: repeat(${cells}, 1fr); gap: 1px; background: ${options.background}; padding: 10px;">`;
100
-
101
- // Generate a simple pattern based on data
102
- for (let i = 0; i < cells * cells; i++) {
103
- const shouldFill = (data.charCodeAt(i % data.length) + i) % 2 === 0;
104
- const color = shouldFill ? options.lineColor : options.background;
105
- html += `<div style="background-color: ${color}; width: 100%; height: 100%;"></div>`;
106
- }
107
-
108
- html += '</div>';
109
- return html;
110
- }
111
-
112
- /**
113
- * Create linear barcode HTML representation
114
- * @param {string} data - The data to encode
115
- * @param {string} type - The barcode type
116
- * @param {Object} options - Render options
117
- * @returns {string} HTML content
118
- */
119
- createLinearBarcodeHTML(data, type, options) {
120
- const barWidth = options.width || 2;
121
- const barHeight = options.height || 100;
122
-
123
- let html = `<div class="linear-barcode" style="display: flex; align-items: flex-start; background: ${options.background}; padding: 10px;">`;
124
-
125
- // Generate bars based on data
126
- for (let i = 0; i < data.length; i++) {
127
- const char = data.charCodeAt(i);
128
- const barCount = (char % 5) + 1;
129
-
130
- for (let j = 0; j < barCount; j++) {
131
- html += `<div style="width: ${barWidth}px; height: ${barHeight}px; background-color: ${options.lineColor}; margin-right: ${barWidth}px;"></div>`;
132
- }
133
- }
134
-
135
- html += '</div>';
136
- return html;
137
- }
138
-
139
- /**
140
- * Get container styles
141
- * @param {Object} options - Render options
142
- * @returns {string} CSS styles
143
- */
144
- getContainerStyles(options) {
145
- return `
146
- display: inline-block;
147
- background: ${options.background};
148
- padding: ${options.margin}px;
149
- margin: ${options.marginTop}px ${options.marginRight}px ${options.marginBottom}px ${options.marginLeft}px;
150
- border: 1px solid #ccc;
151
- text-align: center;
152
- `;
153
- }
154
-
155
- /**
156
- * Get text styles
157
- * @param {Object} options - Render options
158
- * @returns {string} CSS styles
159
- */
160
- getTextStyles(options) {
161
- return `
162
- font-family: Arial, sans-serif;
163
- font-size: ${options.fontSize}px;
164
- color: ${options.lineColor};
165
- text-align: ${options.textAlign};
166
- margin-top: ${options.textMargin}px;
167
- `;
168
- }
169
-
170
- /**
171
- * Get CSS styles for the barcode
172
- * @param {string} containerId - Container ID
173
- * @param {Object} options - Render options
174
- * @returns {string} CSS styles
175
- */
176
- getCSSStyles(containerId) {
177
- return `
178
- <style>
179
- #${containerId} {
180
- font-family: Arial, sans-serif;
181
- }
182
- #${containerId} .barcode-text {
183
- font-weight: bold;
184
- }
185
- #${containerId} .qr-code {
186
- margin: 0 auto;
187
- }
188
- #${containerId} .linear-barcode {
189
- justify-content: center;
190
- }
191
- </style>
192
- `;
193
- }
194
-
195
49
  /**
196
50
  * Get default options
197
51
  * @returns {Object} Default options
@@ -209,4 +63,17 @@ class HTMLRenderer {
209
63
  }
210
64
  }
211
65
 
66
+ /**
67
+ * Escape a value for use inside a double quoted HTML attribute
68
+ * @param {string} value - Raw value
69
+ * @returns {string} Escaped value
70
+ */
71
+ function escapeAttribute(value) {
72
+ return String(value == null ? '' : value)
73
+ .replace(/&/g, '&amp;')
74
+ .replace(/"/g, '&quot;')
75
+ .replace(/</g, '&lt;')
76
+ .replace(/>/g, '&gt;');
77
+ }
78
+
212
79
  module.exports = HTMLRenderer;