UNPKG

receiptline

Version:

Markdown for receipts. Printable digital receipts. Generate receipt printer commands and images.

1,067 lines (1,053 loc) 184 kB
/* Copyright 2019 Open Foodservice System Consortium Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // QR Code is a registered trademark of DENSO WAVE INCORPORATED. (function () { let iconv = undefined; let PNG = undefined; let stream = undefined; let decoder = undefined; // Node.js if (typeof require !== 'undefined') { iconv = require('iconv-lite'); PNG = require('pngjs').PNG; stream = require('stream'); decoder = require('string_decoder'); } /** * Transform ReceiptLine document to printer commands or SVG images. * @param {string} doc ReceiptLine document * @param {object} [printer] printer configuration * @returns {string} printer command or SVG image */ function transform(doc, printer) { // initialize state variables const state = { wrap: true, border: 1, width: [], align: 1, option: { type: 'code128', width: 2, height: 72, hri: false, cell: 3, level: 'l', quietZone: false }, line: 'waiting', rules: { left: 0, width: 0, right: 0, widths: [] } }; // validate printer configuration const ptr = parseOption(printer); // append commands to start printing let result = ptr.command.open(ptr); // strip bom if (doc[0] === '\ufeff') { doc = doc.slice(1); } // parse each line and generate commands const res = doc.normalize().split(/\n|\r\n|\r/).map(line => createLine(parseLine(line, state), ptr, state)); // if rules is not finished switch (state.line) { case 'ready': // set state to cancel rules state.line = 'waiting'; break; case 'running': case 'horizontal': // append commands to stop rules res.push(ptr.command.normal() + ptr.command.area(state.rules.left, state.rules.width, state.rules.right) + ptr.command.align(0) + ptr.command.vrstop(state.rules.widths) + ptr.command.vrlf(false)); state.line = 'waiting'; break; default: break; } // flip upside down if (ptr.upsideDown) { res.reverse(); } // append commands result += res.join(''); // append commands to end printing result += ptr.command.close(); return result; } /** * Create transform stream that converts ReceiptLine document to printer commands or SVG images. * @param {object} [printer] printer configuration * @returns {stream.Transform} transform stream */ function createTransform(printer) { // initialize state variables const state = { wrap: true, border: 1, width: [], align: 1, option: { type: 'code128', width: 2, height: 72, hri: false, cell: 3, level: 'l', quietZone: false }, line: 'waiting', rules: { left: 0, width: 0, right: 0, widths: [] } }; // validate printer configuration const ptr = parseOption(printer); // create transform stream const transform = new stream.Transform({ construct(callback) { // initialize this.bom = true; this.decoder = new decoder.StringDecoder('utf8'); this.data = ''; this.encoding = /^(svg|text)$/.test(printer.command) ? 'utf8' : 'binary'; this._push = function (chunk) { if (chunk.length > 0) { this.push(chunk, this.encoding); } }; this.buffer = []; // append commands to start printing this._push(ptr.command.open(ptr)); callback(); }, transform(chunk, encoding, callback) { // append chunk this.data += this.decoder.write(chunk); // strip bom if (this.bom) { if (this.data[0] === '\ufeff') { this.data = this.data.slice(1); } this.bom = false; } // parse each line and generate commands const lines = this.data.split(/\n|\r\n|\r/); while (lines.length > 1) { const s = createLine(parseLine(lines.shift().normalize(), state), ptr, state); ptr.upsideDown ? this.buffer.push(s) : this._push(s); } this.data = lines.shift(); callback(); }, flush(callback) { // parse last line and generate commands const s = createLine(parseLine(this.data.normalize(), state), ptr, state); ptr.upsideDown ? this.buffer.push(s) : this._push(s); // if rules is not finished switch (state.line) { case 'ready': // set state to cancel rules state.line = 'waiting'; break; case 'running': case 'horizontal': // append commands to stop rules const s = ptr.command.normal() + ptr.command.area(state.rules.left, state.rules.width, state.rules.right) + ptr.command.align(0) + ptr.command.vrstop(state.rules.widths) + ptr.command.vrlf(false); ptr.upsideDown ? this.buffer.push(s) : this._push(s); state.line = 'waiting'; break; default: break; } // flip upside down if (ptr.upsideDown) { this._push(this.buffer.reverse().join('')); } // append commands to end printing this._push(ptr.command.close()); callback(); } }); return transform; } /** * Validate printer configuration. * @param {object} printer printer configuration * @returns {object} validated printer configuration */ function parseOption(printer) { // validate printer configuration const p = Object.assign({}, printer); p.cpl = p.cpl || 48; p.encoding = /^(cp(437|85[28]|86[0356]|1252|93[26]|949|950)|multilingual|shiftjis|gb18030|ksc5601|big5|tis620)$/.test(p.encoding) ? p.encoding : 'cp437'; p.upsideDown = !!p.upsideDown; p.spacing = !!p.spacing; p.cutting = 'cutting' in p ? !!p.cutting : true; p.margin = p.margin || 0; p.marginRight = p.marginRight || 0; p.gradient = 'gradient' in p ? !!p.gradient : true; p.gamma = p.gamma || 1.8; p.threshold = p.threshold || 128; p.command = Object.assign({}, (typeof p.command !== 'object' ? commands[p.command] : p.command) || commands.svg); return p; } /** * Parse lines. * @param {string} columns line text without line breaks * @param {object} state state variables * @returns {object} parsed line object */ function parseLine(columns, state) { // extract columns const line = columns // trim whitespace .replace(/^[\t ]+|[\t ]+$/g, '') // convert escape characters ('\\', '\{', '\|', '\}') to hexadecimal escape characters .replace(/\\[\\{|}]/g, match => '\\x' + match.charCodeAt(1).toString(16)) // append a space if the first column does not start with '|' and is right-aligned .replace(/^[^|]*[^\t |]\|/, ' $&') // append a space if the last column does not end with '|' and is left-aligned .replace(/\|[^\t |][^|]*$/, '$& ') // remove '|' at the beginning of the first column .replace(/^\|(.*)$/, '$1') // remove '|' at the end of the last column .replace(/^(.*)\|$/, '$1') // separate text with '|' .split('|') // parse columns .map((column, index, array) => { // parsed column object let result = {}; // trim whitespace const element = column.replace(/^[\t ]+|[\t ]+$/g, ''); // determin alignment from whitespaces around column text result.align = 1 + Number(/^[\t ]/.test(column)) - Number(/[\t ]$/.test(column)); // parse properties if (/^\{[^{}]*\}$/.test(element)) { // extract members result.property = element // trim property delimiters .slice(1, -1) // convert escape character ('\;') to hexadecimal escape characters .replace(/\\;/g, '\\x3b') // separate property with ';' .split(';') // parse members .reduce((obj, member) => { // abbreviations const abbr = { a: 'align', b: 'border', c: 'code', i: 'image', o: 'option', t: 'text', w: 'width', x: 'command', _: 'comment' }; // parse key-value pair if (!/^[\t ]*$/.test(member) && member.replace(/^[\t ]*([A-Za-z_]\w*)[\t ]*:[\t ]*([^\t ].*?)[\t ]*$/, (match, key, value) => obj[key.replace(/^[abciotwx_]$/, m => abbr[m])] = parseEscape(value.replace(/\\n/g, '\n'))) === member) { // invalid members result.error = element; } return obj; }, {}); // if the column is single if (array.length === 1) { // parse text property if ('text' in result.property) { const c = result.property.text.toLowerCase(); state.wrap = !/^nowrap$/.test(c); } // parse border property if ('border' in result.property) { const c = result.property.border.toLowerCase(); const border = { 'line': -1, 'space': 1, 'none': 0 }; const previous = state.border; state.border = /^(line|space|none)$/.test(c) ? border[c.toLowerCase()] : /^\d+$/.test(c) && Number(c) <= 2 ? Number(c) : 1; // start rules if (previous >= 0 && state.border < 0) { result.vr = '+'; } // stop rules if (previous < 0 && state.border >= 0) { result.vr = '-'; } } // parse width property if ('width' in result.property) { const width = result.property.width.toLowerCase().split(/[\t ]+|,/); state.width = width.find(c => /^auto$/.test(c)) ? [] : width.map(c => /^\*$/.test(c) ? -1 : /^\d+$/.test(c) ? Number(c) : 0); } // parse align property if ('align' in result.property) { const c = result.property.align.toLowerCase(); const align = { 'left': 0, 'center': 1, 'right': 2 }; state.align = /^(left|center|right)$/.test(c) ? align[c.toLowerCase()] : 1; } // parse option property if ('option' in result.property) { const option = result.property.option.toLowerCase().split(/[\t ]+|,/); state.option = { type: (option.find(c => /^(upc|ean|jan|code39|itf|codabar|nw7|code93|code128|qrcode)$/.test(c)) || 'code128'), width: Number(option.find(c => /^\d+$/.test(c) && Number(c) >= 2 && Number(c) <= 4) || '2'), height: Number(option.find(c => /^\d+$/.test(c) && Number(c) >= 24 && Number(c) <= 240) || '72'), hri: !!option.find(c => /^hri$/.test(c)), cell: Number(option.find(c => /^\d+$/.test(c) && Number(c) >= 3 && Number(c) <= 8) || '3'), level: (option.find(c => /^[lmqh]$/.test(c)) || 'l'), quietZone: false }; } // parse code property if ('code' in result.property) { result.code = Object.assign({ data: result.property.code }, state.option); } // parse image property if ('image' in result.property) { const c = result.property.image.replace(/=.*|[^A-Za-z0-9+/]/g, ''); switch (c.length % 4) { case 1: result.image = c.slice(0, -1); break; case 2: result.image = c + '=='; break; case 3: result.image = c + '='; break; default: result.image = c; break; } } // parse command property if ('command' in result.property) { result.command = result.property.command; } // parse comment property if ('comment' in result.property) { result.comment = result.property.comment; } } } // remove invalid property delimiter else if (/[{}]/.test(element)) { result.error = element; } // parse horizontal rule of special character in text else if (array.length === 1 && /^-+$|^=+$/.test(element)) { result.hr = element.slice(-1); } // parse text else { result.text = element // remove control codes and hexadecimal control codes .replace(/[\x00-\x1f\x7f]|\\x[01][\dA-Fa-f]|\\x7[Ff]/g, '') // convert escape characters ('\-', '\=', '\_', '\"', \`', '\^', '\~') to hexadecimal escape characters .replace(/\\[-=_"`^~]/g, match => '\\x' + match.charCodeAt(1).toString(16)) // convert escape character ('\n') to LF .replace(/\\n/g, '\n') // convert escape character ('~') to space .replace(/~/g, ' ') // separate text with '_', '"', '`', '^'(1 or more), '\n' .split(/([_"`\n]|\^+)/) // convert escape characters to normal characters .map(text => parseEscape(text)); } // set current text wrapping result.wrap = state.wrap; // set current column border result.border = state.border; // set current column width if (state.width.length === 0) { // set '*' for all columns when the width property is 'auto' result.width = -1; } else if ('text' in result) { // text: set column width result.width = index < state.width.length ? state.width[index] : 0; } else if (state.width.find(c => c < 0)) { // image, code, command: when the width property includes '*', set '*' result.width = -1; } else { // image, code, command: when the width property does not include '*', set the sum of column width and border width const w = state.width.filter(c => c > 0); result.width = w.length > 0 ? w.reduce((a, c) => a + c, result.border < 0 ? w.length + 1 : (w.length - 1) * result.border) : 0; } // set line alignment result.alignment = state.align; return result; }); // if the line is text and the width property is not 'auto' if (line.every(el => 'text' in el) && state.width.length > 0) { // if the line has fewer columns while (line.length < state.width.length) { // fill empty columns line.push({ align: 1, text: [''], wrap: state.wrap, border: state.border, width: state.width[line.length] }); } } return line; } /** * Parse escape characters. * @param {string} chars string containing escape characters * @returns {string} unescaped string */ function parseEscape(chars) { return chars // remove invalid escape sequences .replace(/\\$|\\x(.?$|[^\dA-Fa-f].|.[^\dA-Fa-f])/g, '') // ignore invalid escape characters .replace(/\\[^x]/g, '') // convert hexadecimal escape characters to normal characters .replace(/\\x([\dA-Fa-f]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16))); } /** * Generate commands from line objects. * @param {object} line parsed line object * @param {object} printer printer configuration * @param {object} state state variables * @returns {string} printer command fragment or SVG image fragment */ function createLine(line, printer, state) { const result = []; // text or property const text = line.every(el => 'text' in el); // the first column const column = line[0]; // remove zero width columns let columns = line.filter(el => el.width !== 0); // remove overflowing columns if (text) { columns = columns.slice(0, Math.floor(column.border < 0 ? (printer.cpl - 1) / 2 : (printer.cpl + column.border) / (column.border + 1))); } // fixed columns const f = columns.filter(el => el.width > 0); // variable columns const g = columns.filter(el => el.width < 0); // reserved width let u = f.reduce((a, el) => a + el.width, 0); // free width let v = printer.cpl - u; // subtract border width from free width if (text && columns.length > 0) { v -= column.border < 0 ? columns.length + 1 : (columns.length - 1) * column.border; } // number of variable columns const n = g.length; // reduce the width of fixed columns when reserved width is too many while (n > v) { f.reduce((a, el) => a.width > el.width ? a : el).width--; v++; } // allocate free width among variable columns if (n > 0) { g.forEach((el, i) => el.width = Math.floor((v + i) / n)); v = 0; } // print area const left = Math.floor(v * column.alignment / 2); const width = printer.cpl - v; const right = v - left; // process text if (text) { // wrap text const cols = columns.map(column => wrapText(column, printer)); // vertical line spacing const widths = columns.map(column => column.width); // rules switch (state.line) { case 'ready': // append commands to start rules result.push(printer.command.normal() + printer.command.area(left, width, right) + printer.command.align(0) + printer.command.vrstart(widths) + printer.command.vrlf(true)); state.line = 'running'; break; case 'horizontal': // append commands to print horizontal rule const m = left - state.rules.left; const w = width - state.rules.width; const l = Math.min(left, state.rules.left); const r = Math.min(right, state.rules.right); result.push(printer.command.normal() + printer.command.area(l, printer.cpl - l - r, r) + printer.command.align(0) + printer.command.vrhr(state.rules.widths, widths, m, m + w) + printer.command.lf()); state.line = 'running'; break; default: break; } // save parameters to stop rules state.rules = { left: left, width: width, right: right, widths: widths }; // maximum number of wraps const row = column.wrap ? cols.reduce((a, col) => Math.max(a, col.length), 1) : 1; // sort text for (let j = 0; j < row; j++) { // append commands to set print area and line alignment let res = printer.command.normal() + printer.command.area(left, width, right) + printer.command.align(0); // print position let p = 0; // process vertical rules if (state.line === 'running') { // maximum height const height = cols.reduce((a, col) => j < col.length ? Math.max(a, col[j].height) : a, 1); // append commands to print vertical rules res += printer.command.normal() + printer.command.absolute(p++) + printer.command.vr(widths, height); } // process each column cols.forEach((col, i) => { // append commands to set print position of first column res += printer.command.absolute(p); // if wrapped text is not empty if (j < col.length) { // append commands to align text res += printer.command.relative(col[j].margin); // process text const data = col[j].data; for (let k = 0; k < data.length; k += 2) { // append commands to decorate text const ul = Number(data[k][0]); const em = Number(data[k][1]); const iv = Number(data[k][2]); const wh = Number(data[k][3]); res += printer.command.normal(); if (ul) { res += printer.command.ul(); } if (em) { res += printer.command.em(); } if (iv) { res += printer.command.iv(); } if (wh) { res += printer.command.wh(wh); } // append commands to print text res += printer.command.text(data[k + 1], printer.encoding); } } // if wrapped text is empty else { res += printer.command.normal() + printer.command.text(' ', printer.encoding); } // append commands to set print position of next column p += columns[i].width + Math.abs(column.border); }); // append commands to feed new line res += printer.command.lf(); result.push(res); } } // process horizontal rule or paper cut if ('hr' in column) { // process paper cut if (column.hr === '=') { switch (state.line) { case 'running': case 'horizontal': // append commands to stop rules result.push(printer.command.normal() + printer.command.area(state.rules.left, state.rules.width, state.rules.right) + printer.command.align(0) + printer.command.vrstop(state.rules.widths) + printer.command.vrlf(false)); // append commands to cut paper result.push(printer.command.cut()); // set state to start rules state.line = 'ready'; break; default: // append commands to cut paper result.push(printer.command.cut()); break; } } // process horizontal rule else { switch (state.line) { case 'waiting': // append commands to print horizontal rule result.push(printer.command.normal() + printer.command.area(left, width, right) + printer.command.align(0) + printer.command.hr(width) + printer.command.lf()); break; case 'running': // set state to print horizontal rule state.line = 'horizontal'; break; default: break; } } } // process rules if ('vr' in column) { // start rules if (column.vr === '+') { state.line = 'ready'; } // stop rules else { switch (state.line) { case 'ready': // set state to cancel rules state.line = 'waiting'; break; case 'running': case 'horizontal': // append commands to stop rules result.push(printer.command.normal() + printer.command.area(state.rules.left, state.rules.width, state.rules.right) + printer.command.align(0) + printer.command.vrstop(state.rules.widths) + printer.command.vrlf(false)); state.line = 'waiting'; break; default: break; } } } // process image if ('image' in column) { // append commands to print image result.push(printer.command.normal() + printer.command.area(left, width, right) + printer.command.align(column.align) + printer.command.image(column.image)); } // process barcode or 2D code if ('code' in column) { // process 2D code if (column.code.type === 'qrcode') { // append commands to print 2D code result.push(printer.command.normal() + printer.command.area(left, width, right) + printer.command.align(column.align) + printer.command.qrcode(column.code, printer.encoding)); } // process barcode else { // append commands to print barcode result.push(printer.command.normal() + printer.command.area(left, width, right) + printer.command.align(column.align) + printer.command.barcode(column.code, printer.encoding)); } } // process command if ('command' in column) { // append commands to insert commands result.push(printer.command.normal() + printer.command.area(left, width, right) + printer.command.align(column.align) + printer.command.command(column.command)); } // flip upside down if (printer.upsideDown) { result.reverse(); } return result.join(''); } /** * Wrap text. * @param {object} column parsed column object * @param {object} printer printer configuration * @returns {object[]} wrapped text, text position, and text height */ function wrapText(column, printer) { const result = []; // remaining spaces let space = column.width; // text height let height = 1; // text data let res = []; // text decoration flags let ul = false; let em = false; let iv = false; let wh = 0; // process text and text decoration column.text.forEach((text, i) => { // process text if (i % 2 === 0) { // if text is not empty let t = printer.command.arrayFrom(text, printer.encoding); while (t.length > 0) { // measure character width let w = 0; let j = 0; while (j < t.length) { w = printer.command.measureText(t[j], printer.encoding) * (wh < 2 ? wh + 1 : wh - 1); // output before protruding if (w > space) { break; } space -= w; w = 0; j++; } // if characters fit if (j > 0) { // append text decoration information res.push((ul ? '1' : '0') + (em ? '1' : '0') + (iv ? '1' : '0') + wh); // append text res.push(t.slice(0, j).join('')); // update text height height = Math.max(height, wh < 3 ? wh : wh - 1); // remaining text t = t.slice(j); } // if character is too big if (w > column.width) { // do not output t = t.slice(1); continue; } // if there is no spece left if (w > space || space === 0) { // wrap text automatically result.push({ data: res, margin: space * column.align / 2, height: height }); space = column.width; res = []; height = 1; } } } // process text decoration else { // update text decoration flags switch (text) { case '\n': // wrap text manually result.push({ data: res, margin: space * column.align / 2, height: height }); space = column.width; res = []; height = 1; break; case '_': ul = !ul; break; case '"': em = !em; break; case '`': iv = !iv; break; default: const d = Math.min(text.length, 7); wh = wh === d ? 0 : d; break; } } }); // output last text if (res.length > 0) { result.push({ data: res, margin: space * column.align / 2, height: height }); } return result; } // // Barcode generator // const barcode = (() => { // CODE128 patterns: const c128 = { element: '212222,222122,222221,121223,121322,131222,122213,122312,132212,221213,221312,231212,112232,122132,122231,113222,123122,123221,223211,221132,221231,213212,223112,312131,311222,321122,321221,312212,322112,322211,212123,212321,232121,111323,131123,131321,112313,132113,132311,211313,231113,231311,112133,112331,132131,113123,113321,133121,313121,211331,231131,213113,213311,213131,311123,311321,331121,312113,312311,332111,314111,221411,431111,111224,111422,121124,121421,141122,141221,112214,112412,122114,122411,142112,142211,241211,221114,413111,241112,134111,111242,121142,121241,114212,124112,124211,411212,421112,421211,212141,214121,412121,111143,111341,131141,114113,114311,411113,411311,113141,114131,311141,411131,211412,211214,211232,2331112'.split(','), starta: 103, startb: 104, startc: 105, atob: 100, atoc: 99, btoa: 101, btoc: 99, ctoa: 101, ctob: 100, shift: 98, stop: 106 }; // generate CODE128 data (minimize symbol width): const code128 = symbol => { const r = {}; let s = symbol.data.replace(/((?!^[\x00-\x7f]+$).)*/, ''); if (s.length > 0) { // generate HRI r.hri = symbol.hri; r.text = s.replace(/[\x00- \x7f]/g, ' '); // minimize symbol width const d = []; const p = s.search(/[^ -_]/); if (/^\d{2}$/.test(s)) { d.push(c128.startc, Number(s)); } else if (/^\d{4,}/.test(s)) { code128c(c128.startc, s, d); } else if (p >= 0 && s.charCodeAt(p) < 32) { code128a(c128.starta, s, d); } else if (s.length > 0) { code128b(c128.startb, s, d); } else { // end } // calculate check digit and append stop character d.push(d.reduce((a, c, i) => a + c * i) % 103, c128.stop); // generate bars and spaces const q = symbol.quietZone ? 'a' : '0'; const m = d.reduce((a, c) => a + c128.element[c], q) + q; r.widths = m.split('').map(c => parseInt(c, 16) * symbol.width); r.length = symbol.width * (d.length * 11 + (symbol.quietZone ? 22 : 2)); r.height = symbol.height; } return r; }; // process CODE128 code set A: const code128a = (x, s, d) => { if (x !== c128.shift) { d.push(x); } s = s.replace(/^((?!\d{4,})[\x00-_])+/, m => (m.split('').forEach(c => d.push((c.charCodeAt(0) + 64) % 96)), '')); s = s.replace(/^\d(?=(\d\d){2,}(\D|$))/, m => (d.push((m.charCodeAt(0) + 64) % 96), '')); const t = s.slice(1); const p = t.search(/[^ -_]/); if (/^\d{4,}/.test(s)) { code128c(c128.atoc, s, d); } else if (p >= 0 && t.charCodeAt(p) < 32) { d.push(c128.shift, s.charCodeAt(0) - 32); code128a(c128.shift, t, d); } else if (s.length > 0) { code128b(c128.atob, s, d); } else { // end } }; // process CODE128 code set B: const code128b = (x, s, d) => { if (x !== c128.shift) { d.push(x); } s = s.replace(/^((?!\d{4,})[ -\x7f])+/, m => (m.split('').forEach(c => d.push(c.charCodeAt(0) - 32)), '')); s = s.replace(/^\d(?=(\d\d){2,}(\D|$))/, m => (d.push(m.charCodeAt(0) - 32), '')); const t = s.slice(1); const p = t.search(/[^ -_]/); if (/^\d{4,}/.test(s)) { code128c(c128.btoc, s, d); } else if (p >= 0 && t.charCodeAt(p) > 95) { d.push(c128.shift, s.charCodeAt(0) + 64); code128b(c128.shift, t, d); } else if (s.length > 0) { code128a(c128.btoa, s, d); } else { // end } }; // process CODE128 code set C: const code128c = (x, s, d) => { if (x !== c128.shift) { d.push(x); } s = s.replace(/^\d{4,}/g, m => m.replace(/\d{2}/g, c => (d.push(Number(c)), ''))); const p = s.search(/[^ -_]/); if (p >= 0 && s.charCodeAt(p) < 32) { code128a(c128.ctoa, s, d); } else if (s.length > 0) { code128b(c128.ctob, s, d); } else { // end } }; // CODE93 patterns: const c93 = { escape: 'cU,dA,dB,dC,dD,dE,dF,dG,dH,dI,dJ,dK,dL,dM,dN,dO,dP,dQ,dR,dS,dT,dU,dV,dW,dX,dY,dZ,cA,cB,cC,cD,cE, ,sA,sB,sC,$,%,sF,sG,sH,sI,sJ,+,sL,-,.,/,0,1,2,3,4,5,6,7,8,9,sZ,cF,cG,cH,cI,cJ,cV,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,cK,cL,cM,cN,cO,cW,pA,pB,pC,pD,pE,pF,pG,pH,pI,pJ,pK,pL,pM,pN,pO,pP,pQ,pR,pS,pT,pU,pV,pW,pX,pY,pZ,cP,cQ,cR,cS,cT'.split(','), code: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%dcsp'.split('').reduce((a, c, i) => (a[c] = i, a), {}), element: '131112,111213,111312,111411,121113,121212,121311,111114,131211,141111,211113,211212,211311,221112,221211,231111,112113,112212,112311,122112,132111,111123,111222,111321,121122,131121,212112,212211,211122,211221,221121,222111,112122,112221,122121,123111,121131,311112,311211,321111,112131,113121,211131,121221,312111,311121,122211,111141,1111411'.split(','), start: 47, stop: 48 }; // generate CODE93 data: const code93 = symbol => { const r = {}; let s = symbol.data.replace(/((?!^[\x00-\x7f]+$).)*/, ''); if (s.length > 0) { // generate HRI r.hri = symbol.hri; r.text = s.replace(/[\x00- \x7f]/g, ' '); // calculate check digit const d = s.split('').reduce((a, c) => a + c93.escape[c.charCodeAt(0)], '').split('').map(c => c93.code[c]); d.push(d.reduceRight((a, c, i) => a + c * ((d.length - 1 - i) % 20 + 1)) % 47); d.push(d.reduceRight((a, c, i) => a + c * ((d.length - 1 - i) % 15 + 1)) % 47); // append start character and stop character d.unshift(c93.start); d.push(c93.stop); // generate bars and spaces const q = symbol.quietZone ? 'a' : '0'; const m = d.reduce((a, c) => a + c93.element[c], q) + q; r.widths = m.split('').map(c => parseInt(c, 16) * symbol.width); r.length = symbol.width * (d.length * 9 + (symbol.quietZone ? 21 : 1)); r.height = symbol.height; } return r; }; // Codabar(NW-7) patterns: const nw7 = { '0': '2222255', '1': '2222552', '2': '2225225', '3': '5522222', '4': '2252252', '5': '5222252', '6': '2522225', '7': '2522522', '8': '2552222', '9': '5225222', '-': '2225522', '$': '2255222', ':': '5222525', '/': '5252225', '.': '5252522', '+': '2252525', 'A': '2255252', 'B': '2525225', 'C': '2225255', 'D': '2225552' }; // generate Codabar(NW-7) data: const codabar = symbol => { const r = {}; let s = symbol.data.replace(/((?!^[A-D][0-9\-$:/.+]+[A-D]$).)*/i, ''); if (s.length > 0) { // generate HRI r.hri = symbol.hri; r.text = s; // generate bars and spaces const q = symbol.quietZone ? 'a' : '0'; const m = s.toUpperCase().split('').reduce((a, c) => a + nw7[c] + '2', q).slice(0, -1) + q; r.widths = m.split('').map(c => parseInt(c, 16) * symbol.width + 1 >> 1); const w = [ 25, 39, 50, 3, 5, 6 ]; r.length = s.length * w[symbol.width - 2] - (s.match(/[\d\-$]/g) || []).length * w[symbol.width + 1] + symbol.width * (symbol.quietZone ? 19 : -1); r.height = symbol.height; } return r; }; // Interleaved 2 of 5 patterns: const i25 = { element: '22552,52225,25225,55222,22525,52522,25522,22255,52252,25252'.split(','), start: '2222', stop: '522' }; // generate Interleaved 2 of 5 data: const itf = symbol => { const r = {}; let s = symbol.data.replace(/((?!^(\d{2})+$).)*/, ''); if (s.length > 0) { // generate HRI r.hri = symbol.hri; r.text = s; // generate bars and spaces const d = s.split('').map(c => Number(c)); const q = symbol.quietZone ? 'a' : '0'; let m = q + i25.start; let i = 0; while (i < d.length) { const b = i25.element[d[i++]]; const s = i25.element[d[i++]]; m += b.split('').reduce((a, c, j) => a + c + s[j], ''); } m += i25.stop + q; r.widths = m.split('').map(c => parseInt(c, 16) * symbol.width + 1 >> 1); const w = [ 16, 25, 32, 17, 26, 34 ]; r.length = s.length * w[symbol.width - 2] + w[symbol.width + 1] + symbol.width * (symbol.quietZone ? 20 : 0); r.height = symbol.height; } return r; }; // CODE39 patterns: const c39 = { '0': '222552522', '1': '522522225', '2': '225522225', '3': '525522222', '4': '222552225', '5': '522552222', '6': '225552222', '7': '222522525', '8': '522522522', '9': '225522522', 'A': '522225225', 'B': '225225225', 'C': '525225222', 'D': '222255225', 'E': '522255222', 'F': '225255222', 'G': '222225525', 'H': '522225522', 'I': '225225522', 'J': '222255522', 'K': '522222255', 'L': '225222255', 'M': '525222252', 'N': '222252255', 'O': '522252252', 'P': '225252252', 'Q': '222222555', 'R': '522222552', 'S': '225222552', 'T': '222252552', 'U': '552222225', 'V': '255222225', 'W': '555222222', 'X': '252252225', 'Y': '552252222', 'Z': '255252222', '-': '252222525', '.': '552222522', ' ': '255222522', '$': '252525222', '/': '252522252', '+': '252225252', '%': '222525252', '*': '252252522' }; // generate CODE39 data: const code39 = symbol => { const r = {}; let s = symbol.data.replace(/((?!^\*?[0-9A-Z\-. $/+%]+\*?$).)*/, ''); if (s.length > 0) { // append start character and stop character s = s.replace(/^\*?([^*]+)\*?$/, '*$1*'); // generate HRI r.hri = symbol.hri; r.text = s; // generate bars and spaces const q = symbol.quietZone ? 'a' : '0'; const m = s.split('').reduce((a, c) => a + c39[c] + '2', q).slice(0, -1) + q; r.widths = m.split('').map(c => parseInt(c, 16) * symbol.width + 1 >> 1); const w = [ 29, 45, 58 ]; r.length = s.length * w[symbol.width - 2] + symbol.width * (symbol.quietZone ? 19 : -1); r.height = symbol.height; } return r; }; // UPC/EAN/JAN patterns: const ean = { a: '3211,2221,2122,1411,1132,1231,1114,1312,1213,3112'.split(','), b: '1123,1222,2212,1141,2311,1321,4111,2131,3121,2113'.split(','), c: '3211,2221,2122,1411,1132,1231,1114,1312,1213,3112'.split(','), g: '111,11111,111111,11,112'.split(','), p: 'aaaaaa,aababb,aabbab,aabbba,abaabb,abbaab,abbbaa,ababab,ababba,abbaba'.split(','), e: 'bbbaaa,bbabaa,bbaaba,bbaaab,babbaa,baabba,baaabb,bababa,babaab,baabab'.split(',') }; // generate UPC-A data: const upca = symbol => { const s = Object.assign({}, symbol); s.data = '0' + symbol.data; const r = ean13(s); if ('text' in r) { r.text = r.text.slice(1); } return r; }; // generate UPC-E data: const upce = symbol => { const r = {}; const d = symbol.data.replace(/((?!^0\d{6,7}$).)*/, '').split('').map(c => Number(c)); if (d.length > 0) { // calculate check digit d[7] = 0; d[7] = (10 - upcetoa(d).reduce((a, c, i) => a + c * (3 - (i % 2) * 2), 0) % 10) % 10; // generate HRI r.hri = symbol.hri; r.text = d.join(''); // generate bars and spaces const q = symbol.quietZone ? '7' : '0'; let m = q + ean.g[0]; for (let i = 1; i < 7; i++) m += ean[ean.e[d[7]][i - 1]][d[i]]; m += ean.g[2] + q; r.widths = m.split('').map(c => parseInt(c, 16) * symbol.width); r.length = symbol.width * (symbol.quietZone ? 65 : 51); r.height = symbol.height; } return r; }; // convert UPC-E to UPC-A: const upcetoa = e => { const a = e.slice(0, 3); switch (e[6]) { case 0: case 1: case 2: a.push(e[6], 0, 0, 0, 0, e[3], e[4], e[5]); break; case 3: a.push(e[3], 0, 0, 0, 0, 0, e[4], e[5]); break; case 4: a.push(e[3], e[4], 0, 0, 0, 0, 0, e[5]); break; default: a.push(e[3], e[4], e[5], 0, 0, 0, 0, e[6]); break; } a.push(e[7]); return a; }; // generate EAN-13(JAN-13) data: const ean13 = symbol => { const r = {}; const d = symbol.data.replace(/((?!^\d{12,13}$).)*/, '').split('').map(c => Number(c)); if (d.length > 0) { // calculate check digit d[12] = 0; d[12] = (10 - d.reduce((a, c, i) => a + c *