diff --git a/app/lib/pdf/pdf417.ts b/app/lib/pdf/pdf417.ts new file mode 100644 index 0000000..95f5d40 --- /dev/null +++ b/app/lib/pdf/pdf417.ts @@ -0,0 +1,1053 @@ +import { bcadd, bcdiv, bcmul } from "./bcmath"; +import { + textsubmodes, + textlatch, + clusters, + rsfactors, + TextLatchKeys +} from "./pdf417LookupTables"; + +/** + * PDF417 - 2D Barcode generator (LGPLv3) + * + * Ported from PHP - PDF417 class, version 1.0.005, from TCPDF library (http://www.tcpdf.org/) + */ + +export interface BarcodeArray { + num_rows: number; + num_cols: number; + bcode: number[][]; +} + +const ROWHEIGHT = 4; +const QUIETH = 2; +const QUIETV = 2; +const start_pattern = "11111111010101000"; +const stop_pattern = "111111101000101001"; + +function getInputSequences(code: string): [number, string][] { + var sequence_array: [number, string][] = []; // array to be returned + var numseq: [string, number][] = []; + // get numeric sequences + var numseqMatch = code.match(/([0-9]{13,44})/g); + if (numseqMatch == null) { + numseq = []; + } else { + // add offset to each matched line + for (var n = 0, offset = 0; n < numseqMatch.length; n++) { + offset = code.indexOf(numseqMatch[n], offset); + numseq.push([numseqMatch[n], offset]); + offset += numseqMatch[n].length; + } + } + numseq.push(["", code.length]); + var offset = 0; + for (var i = 0; i < numseq.length; i++) { + var seq = numseq[i]; + var seqlen = seq[0].length; + if (seq[1] > 0) { + // extract text sequence before the number sequence + var prevseq = code.substr(offset, seq[1] - offset); + var textseq: [string, number][] = []; + // get text sequences + var textseqMatch = prevseq.match(/([\x09\x0a\x0d\x20-\x7e]{5,})/g); + if (textseqMatch == null) { + textseq = []; + } else { + // add offset to each matched line + for (var n = 0; n < textseqMatch.length; n++) { + var txtOffset = prevseq.indexOf(textseqMatch[n]); + textseq.push([textseqMatch[n], txtOffset]); + } + } + textseq.push(["", prevseq.length]); + var txtoffset = 0; + for (var j = 0; j < textseq.length; j++) { + var txtseq = textseq[j]; + var txtseqlen = txtseq[0].length; + if (txtseq[1] > 0) { + // extract byte sequence before the text sequence + var prevtxtseq = prevseq.substr(txtoffset, txtseq[1] - txtoffset); + if (prevtxtseq.length > 0) { + // add BYTE sequence + if ( + prevtxtseq.length == 1 && + sequence_array.length > 0 && + sequence_array[sequence_array.length - 1][0] == 900 + ) { + sequence_array.push([913, prevtxtseq]); + } else if (prevtxtseq.length % 6 == 0) { + sequence_array.push([924, prevtxtseq]); + } else { + sequence_array.push([901, prevtxtseq]); + } + } + } + if (txtseqlen > 0) { + // add numeric sequence + sequence_array.push([900, txtseq[0]]); + } + txtoffset = txtseq[1] + txtseqlen; + } + } + if (seqlen > 0) { + // add numeric sequence + sequence_array.push([902, seq[0]]); + } + offset = seq[1] + seqlen; + } + return sequence_array; +}; + +function getCompaction(mode: number, code: string, addmode?: boolean): number[] { + addmode = addmode ?? true; + var cw: number[] = []; // array of codewords to return + switch (mode) { + case 900: { + // Text Compaction mode latch + var submode = 0; // default Alpha sub-mode + var txtarr: number[] = []; // array of characters and sub-mode switching characters + var codelen = code.length; + for (var i = 0; i < codelen; ++i) { + var chval = _ord(code.charAt(i)); + var k: number | string | false; + if ( + (k = _array_search(chval, textsubmodes[submode])) !== + false + ) { + // we are on the same sub-mode + txtarr.push(k as number); + } else { + // the sub-mode is changed + for (var s = 0; s < 4; ++s) { + // search new sub-mode + if ( + s != submode && + (k = _array_search(chval, textsubmodes[s])) !== + false + ) { + // s is the new submode + if ( + (i + 1 == codelen || + (i + 1 < codelen && + _array_search( + _ord(code.charAt(i + 1)), + textsubmodes[submode] + ) !== false)) && + (s == 3 || (s == 0 && submode == 1)) + ) { + // shift (temporary change only for this char) + if (s == 3) { + // shift to puntuaction + txtarr.push(29); + } else { + // shift from lower to alpha + txtarr.push(27); + } + } else { + // latch + txtarr = txtarr.concat(textlatch[("" + submode + s) as TextLatchKeys]); + // set new submode + submode = s; + } + // add characted code to array + txtarr.push(k as number); + break; + } + } + } + } + var txtarrlen = txtarr.length; + if (txtarrlen % 2 != 0) { + // add padding + txtarr.push(29); + ++txtarrlen; + } + // calculate codewords + for (var i = 0; i < txtarrlen; i += 2) { + cw.push(30 * parseInt(String(txtarr[i])) + parseInt(String(txtarr[i + 1]))); + } + break; + } + case 901: + case 924: { + // Byte Compaction mode latch + var rest: string; + var sublen: number; + var codelen: number; + while ((codelen = code.length) > 0) { + if (codelen > 6) { + rest = code.substring(6); + code = code.substring(0, 6); + sublen = 6; + } else { + rest = ""; + sublen = code.length; + } + if (sublen == 6) { + var t = bcmul("" + _ord(code.charAt(0)), "1099511627776"); + t = bcadd(t, bcmul("" + _ord(code.charAt(1)), "4294967296")); + t = bcadd(t, bcmul("" + _ord(code.charAt(2)), "16777216")); + t = bcadd(t, bcmul("" + _ord(code.charAt(3)), "65536")); + t = bcadd(t, bcmul("" + _ord(code.charAt(4)), "256")); + t = bcadd(t, "" + _ord(code.charAt(5))); + // tmp array for the 6 bytes block + var cw6: number[] = []; + do { + var d = _my_bcmod(t, "900"); + t = bcdiv(t, "900"); + // prepend the value to the beginning of the array + cw6.unshift(d); + } while (t != "0"); + // append the result array at the end + cw = cw.concat(cw6); + } else { + for (var i = 0; i < sublen; ++i) { + cw.push(_ord(code.charAt(i))); + } + } + code = rest; + } + break; + } + case 902: { + // Numeric Compaction mode latch + var rest: string; + var codelen: number; + while ((codelen = code.length) > 0) { + if (codelen > 44) { + rest = code.substring(44); + code = code.substring(0, 44); + } else { + rest = ""; + } + var t = "1" + code; + do { + var d = _my_bcmod(t, "900"); + t = bcdiv(t, "900"); + cw.unshift(d); + } while (t != "0"); + code = rest; + } + break; + } + case 913: { + // Byte Compaction mode shift + cw.push(_ord(code)); + break; + } + } + if (addmode) { + // add the compaction mode codeword at the beginning + cw.unshift(mode); + } + return cw; +}; + +function getErrorCorrectionLevel(ecl: number, numcw: number): number { + // get maximum correction level + var maxecl = 8; // starting error level + var maxerrsize = 928 - numcw; // available codewords for error + while (maxecl > 0) { + var errsize = 2 << ecl; + if (maxerrsize >= errsize) { + break; + } + --maxecl; + } + // check for automatic levels + if (ecl < 0 || ecl > 8) { + if (numcw < 41) { + ecl = 2; + } else if (numcw < 161) { + ecl = 3; + } else if (numcw < 321) { + ecl = 4; + } else if (numcw < 864) { + ecl = 5; + } else { + ecl = maxecl; + } + } + if (ecl > maxecl) { + ecl = maxecl; + } + return ecl; +}; + +function getErrorCorrection(cw: number[], ecl: number): number[] { + // get error correction coefficients + var ecc = rsfactors[ecl]; + // number of error correction factors + var eclsize = 2 << ecl; + // maximum index for rsfactors[ecl] + var eclmaxid = eclsize - 1; + // initialize array of error correction codewords + var ecw = _array_fill(0, eclsize, 0) as number[]; + // for each data codeword + for (var k = 0; k < cw.length; k++) { + var t1 = (cw[k] + ecw[eclmaxid]) % 929; + for (var j = eclmaxid; j > 0; --j) { + var t2 = t1 * ecc[j] % 929; + var t3 = 929 - t2; + ecw[j] = (ecw[j - 1] + t3) % 929; + } + t2 = t1 * ecc[0] % 929; + t3 = 929 - t2; + ecw[0] = t3 % 929; + } + for (var j = 0; j < ecw.length; j++) { + if (ecw[j] != 0) { + ecw[j] = 929 - ecw[j]; + } + } + ecw = ecw.reverse(); + return ecw; +}; + +/** + * + * Functions from phpjs.org + * + */ +function _array_fill(start_index: number, num: number, mixed_val: number): number[] | Record { + var key: number, + tmp_arr: Record = {}; + + if (start_index == 0) { + var tmpArray: number[] = []; + for (var i = 0; i < num; i++) { + tmpArray.push(mixed_val); + } + return tmpArray; + } + + if (!isNaN(start_index) && !isNaN(num)) { + for (key = 0; key < num; key++) { + tmp_arr[key + start_index] = mixed_val; + } + } + + return tmp_arr; +}; + +function _str_repeat(input: string, multiplier: number): string { + // http://kevin.vanzonneveld.net + // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: Jonas Raoni Soares Silva (http://www.jsfromhell.com) + // + improved by: Ian Carter (http://euona.com/) + // * example 1: str_repeat('-=', 10); + // * returns 1: '-=-=-=-=-=-=-=-=-=-=' + var y = ""; + while (true) { + if (multiplier & 1) { + y += input; + } + multiplier >>= 1; + if (multiplier) { + input += input; + } else { + break; + } + } + return y; +}; + +function _intval(mixed_var: any, base?: number): number { + // http://kevin.vanzonneveld.net + // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: stensi + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Matteo + // + bugfixed by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Rafał Kukawski (http://kukawski.pl) + // * example 1: intval('Kevin van Zonneveld'); + // * returns 1: 0 + // * example 2: intval(4.2); + // * returns 2: 4 + // * example 3: intval(42, 8); + // * returns 3: 42 + // * example 4: intval('09'); + // * returns 4: 9 + // * example 5: intval('1e', 16); + // * returns 5: 30 + var tmp: number; + + var type = typeof mixed_var; + + if (type === "boolean") { + return +mixed_var; + } else if (type === "string") { + tmp = parseInt(mixed_var, base || 10); + return isNaN(tmp) || !isFinite(tmp) ? 0 : tmp; + } else if (type === "number" && isFinite(mixed_var)) { + return mixed_var | 0; + } else { + return 0; + } +}; + +function _sprintf(format: string, ...args: any[]): string { + // http://kevin.vanzonneveld.net + // + original by: Ash Searle (http://hexmen.com/blog/) + // + namespaced by: Michael White (http://getsprink.com) + // + tweaked by: Jack + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Paulo Freitas + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: Dj + // + improved by: Allidylls + // * example 1: sprintf("%01.2f", 123.1); + // * returns 1: 123.10 + // * example 2: sprintf("[%10s]", 'monkey'); + // * returns 2: '[ monkey]' + // * example 3: sprintf("[%'#10s]", 'monkey'); + // * returns 3: '[####monkey]' + // * example 4: sprintf("%d", 123456789012345); + // * returns 4: '123456789012345' + var regex = /%%|%(\d+\$)?([-+\'#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuideEfFgG])/g; + var a = [format, ...args], + i = 0; + format = a[i++]; + + // pad() + var pad = function (str: string, len: number, chr?: string, leftJustify?: boolean): string { + if (!chr) { + chr = " "; + } + var padding = str.length >= len + ? "" + : Array((1 + len - str.length) >>> 0).join(chr); + return leftJustify ? str + padding : padding + str; + }; + + // justify() + var justify = function ( + value: string, + prefix: string, + leftJustify: boolean, + minWidth: number, + zeroPad: boolean, + customPadChar?: string + ): string { + var diff = minWidth - value.length; + if (diff > 0) { + if (leftJustify || !zeroPad) { + value = pad(value, minWidth, customPadChar, leftJustify); + } else { + value = + value.slice(0, prefix.length) + + pad("", diff, "0", true) + + value.slice(prefix.length); + } + } + return value; + }; + + // formatBaseX() + var formatBaseX = function ( + value: number, + base: number, + prefix: string | boolean, + leftJustify: boolean, + minWidth: number, + precision: number, + zeroPad: boolean + ): string { + // Note: casts negative numbers to positive ones + var number = value >>> 0; + var prefixStr = + (prefix && + number && + ({ + "2": "0b", + "8": "0", + "16": "0x" + } as Record)[String(base)]) || + ""; + value = number; + var valueStr = prefixStr + pad(number.toString(base), precision || 0, "0", false); + return justify(valueStr, prefixStr, leftJustify, minWidth, zeroPad); + }; + + // formatString() + var formatString = function ( + value: string, + leftJustify: boolean, + minWidth: number, + precision: number | null, + zeroPad: boolean, + customPadChar?: string + ): string { + if (precision != null) { + value = value.slice(0, precision); + } + return justify( + value, + "", + leftJustify, + minWidth, + zeroPad, + customPadChar + ); + }; + + // doFormat() + var doFormat = function ( + substring: string, + valueIndex: string | undefined, + flags: string, + minWidth: string, + _: any, + precision: string, + type: string + ): string { + var number: number; + var prefix: string; + var method: string; + var textTransform: string; + var value: any; + + if (substring == "%%") { + return "%"; + } + + // parse flags + var leftJustify = false, + positivePrefix = "", + zeroPad = false, + prefixBaseX: string | boolean = false, + customPadChar = " "; + var flagsl = flags.length; + for (var j = 0; flags && j < flagsl; j++) { + switch (flags.charAt(j)) { + case " ": + positivePrefix = " "; + break; + case "+": + positivePrefix = "+"; + break; + case "-": + leftJustify = true; + break; + case "'": + customPadChar = flags.charAt(j + 1); + break; + case "0": + zeroPad = true; + break; + case "#": + prefixBaseX = true; + break; + } + } + + // parameters may be null, undefined, empty-string or real valued + // we want to ignore null, undefined and empty-string values + var minWidthNum = 0; + if (!minWidth) { + minWidthNum = 0; + } else if (minWidth == "*") { + minWidthNum = +a[i++]; + } else if (minWidth.charAt(0) == "*") { + minWidthNum = +a[parseInt(minWidth.slice(1, -1))]; + } else { + minWidthNum = +minWidth; + } + + // Note: undocumented perl feature: + if (minWidthNum < 0) { + minWidthNum = -minWidthNum; + leftJustify = true; + } + + if (!isFinite(minWidthNum)) { + throw new Error("sprintf: (minimum-)width must be finite"); + } + + var precisionNum: number | undefined; + if (!precision) { + precisionNum = "fFeE".indexOf(type) > -1 + ? 6 + : type == "d" ? 0 : undefined; + } else if (precision == "*") { + precisionNum = +a[i++]; + } else if (precision.charAt(0) == "*") { + precisionNum = +a[parseInt(precision.slice(1, -1))]; + } else { + precisionNum = +precision; + } + + // grab value using valueIndex if required? + value = valueIndex ? a[parseInt(valueIndex.slice(0, -1))] : a[i++]; + + switch (type) { + case "s": + return formatString( + String(value), + leftJustify, + minWidthNum, + precisionNum === undefined ? null : precisionNum, + zeroPad, + customPadChar + ); + case "c": + return formatString( + String.fromCharCode(+value), + leftJustify, + minWidthNum, + precisionNum === undefined ? null : precisionNum, + zeroPad + ); + case "b": + return formatBaseX( + value, + 2, + prefixBaseX, + leftJustify, + minWidthNum, + precisionNum || 0, + zeroPad + ); + case "o": + return formatBaseX( + value, + 8, + prefixBaseX, + leftJustify, + minWidthNum, + precisionNum || 0, + zeroPad + ); + case "x": + return formatBaseX( + value, + 16, + prefixBaseX, + leftJustify, + minWidthNum, + precisionNum || 0, + zeroPad + ); + case "X": + return formatBaseX( + value, + 16, + prefixBaseX, + leftJustify, + minWidthNum, + precisionNum || 0, + zeroPad + ).toUpperCase(); + case "u": + return formatBaseX( + value, + 10, + prefixBaseX, + leftJustify, + minWidthNum, + precisionNum || 0, + zeroPad + ); + case "i": + case "d": + number = +value || 0; + number = Math.round(number - number % 1); // Plain Math.round doesn't just truncate + prefix = number < 0 ? "-" : positivePrefix; + value = + prefix + pad(String(Math.abs(number)), precisionNum || 0, "0", false); + return justify(value, prefix, leftJustify, minWidthNum, zeroPad); + case "e": + case "E": + case "f": // Should handle locales (as per setlocale) + case "F": + case "g": + case "G": + number = +value; + prefix = number < 0 ? "-" : positivePrefix; + method = ["toExponential", "toFixed", "toPrecision"][ + "efg".indexOf(type.toLowerCase()) + ]; + textTransform = ["toString", "toUpperCase"][ + "eEfFgG".indexOf(type) % 2 + ]; + value = prefix + Math.abs(number)[method as 'toExponential' | 'toFixed' | 'toPrecision'](precisionNum); + return justify(value, prefix, leftJustify, minWidthNum, zeroPad)[ + textTransform as 'toString' | 'toUpperCase' + ](); + default: + return substring; + } + }; + + return format.replace(regex, doFormat as any); +}; + +// function _preg_split(pattern: RegExp | string, subject: string, limit?: number, flags?: string | number): (string | [string, number])[] { +// // http://kevin.vanzonneveld.net +// // + original by: Marco Marchiò +// // * example 1: preg_split(/[\s,]+/, 'hypertext language, programming'); +// // * returns 1: ['hypertext', 'language', 'programming'] +// // * example 2: preg_split('//', 'string', -1, 'PREG_SPLIT_NO_EMPTY'); +// // * returns 2: ['s', 't', 'r', 'i', 'n', 'g'] +// // * example 3: var str = 'hypertext language programming'; +// // * example 3: preg_split('/ /', str, -1, 'PREG_SPLIT_OFFSET_CAPTURE'); +// // * returns 3: [['hypertext', 0], ['language', 10], ['programming', 19]] +// // * example 4: preg_split('/( )/', '1 2 3 4 5 6 7 8', 4, 'PREG_SPLIT_DELIM_CAPTURE'); +// // * returns 4: ['1', ' ', '2', ' ', '3', ' ', '4 5 6 7 8'] +// // * example 5: preg_split('/( )/', '1 2 3 4 5 6 7 8', 4, (2 | 4)); +// // * returns 5: [['1', 0], [' ', 1], ['2', 2], [' ', 3], ['3', 4], [' ', 5], ['4 5 6 7 8', 6]] + +// limit = limit || 0; +// flags = flags || ""; // Limit and flags are optional + +// var result: RegExpExecArray | null, +// ret: (string | [string, number])[] = [], +// index = 0, +// i = 0, +// noEmpty = false, +// delim = false, +// offset = false, +// OPTS: Record = {}, +// optTemp = 0; + +// var patternRegex: RegExp; +// if (typeof pattern === 'string') { +// var regexpBody = /^\/(.*)\/\w*$/.exec(pattern)?.[1] || pattern; +// var regexpFlags = /^\/.*\/(\w*)$/.exec(pattern)?.[1] || ''; +// patternRegex = new RegExp(regexpBody, regexpFlags + (regexpFlags.indexOf("g") !== -1 ? "" : "g")); +// } else { +// var regexpBody = pattern.source; +// var regexpFlags = pattern.flags || ''; +// // Non-global regexp causes an infinite loop when executing the while, +// // so if it's not global, copy the regexp and add the "g" modifier. +// patternRegex = pattern.global +// ? pattern +// : new RegExp( +// regexpBody, +// regexpFlags + (regexpFlags.indexOf("g") !== -1 ? "" : "g") +// ); +// } + +// OPTS = { +// PREG_SPLIT_NO_EMPTY: 1, +// PREG_SPLIT_DELIM_CAPTURE: 2, +// PREG_SPLIT_OFFSET_CAPTURE: 4 +// }; +// if (typeof flags !== "number") { +// // Allow for a single string or an array of string flags +// var flagsArr = [].concat(flags as any); +// for (i = 0; i < flagsArr.length; i++) { +// // Resolve string input to bitwise e.g. 'PREG_SPLIT_OFFSET_CAPTURE' becomes 4 +// if (OPTS[flagsArr[i]]) { +// optTemp = optTemp | OPTS[flagsArr[i]]; +// } +// } +// flags = optTemp; +// } +// noEmpty = !!((flags as number) & OPTS.PREG_SPLIT_NO_EMPTY); +// delim = !!((flags as number) & OPTS.PREG_SPLIT_DELIM_CAPTURE); +// offset = !!((flags as number) & OPTS.PREG_SPLIT_OFFSET_CAPTURE); + +// var _filter = function (str: string, strindex: number): void { +// // If the match is empty and the PREG_SPLIT_NO_EMPTY flag is set don't add it +// if (noEmpty && !str.length) { +// return; +// } +// // If the PREG_SPLIT_OFFSET_CAPTURE flag is set +// // transform the match into an array and add the index at position 1 +// if (offset) { +// ret.push([str, strindex]); +// } else { +// ret.push(str); +// } +// }; +// // Special case for empty regexp +// if (!regexpBody) { +// var resultSplit = subject.split(""); +// for (i = 0; i < resultSplit.length; i++) { +// _filter(resultSplit[i], i); +// } +// return ret; +// } +// // Exec the pattern and get the result +// while ((result = patternRegex.exec(subject))) { +// // Stop if the limit is 1 +// if (limit === 1) { +// break; +// } +// // Take the correct portion of the string and filter the match +// _filter(subject.slice(index, result.index), index); +// index = result.index + result[0].length; +// // If the PREG_SPLIT_DELIM_CAPTURE flag is set, every capture match must be included in the results array +// if (delim) { +// // Convert the regexp result into a normal array +// var resarr = Array.prototype.slice.call(result); +// for (i = 1; i < resarr.length; i++) { +// if (result[i] !== undefined) { +// _filter(result[i], result.index + result[0].indexOf(result[i])); +// } +// } +// } +// limit--; +// } +// // Filter last match +// _filter(subject.slice(index, subject.length), index); +// return ret; +// }; + +function _ord(string: string): number { + return string.charCodeAt(0); +}; + +function _array_search(needle: any, haystack: any, argStrict?: boolean): string | number | false { + // http://kevin.vanzonneveld.net + // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: array_search('zonneveld', {firstname: 'kevin', middle: 'van', surname: 'zonneveld'}); + // * returns 1: 'surname' + // * example 2: ini_set('phpjs.return_phpjs_arrays', 'on'); + // * example 2: var ordered_arr = array({3:'value'}, {2:'value'}, {'a':'value'}, {'b':'value'}); + // * example 2: var key = array_search(/val/g, ordered_arr); // or var key = ordered_arr.search(/val/g); + // * returns 2: '3' + + var strict = !!argStrict, + key: string | number = ""; + + if ( + haystack && + typeof haystack === "object" && + (haystack as any).change_key_case + ) { + // Duck-type check for our own array()-created PHPJS_Array + return (haystack as any).search(needle, argStrict); + } + if (typeof needle === "object" && (needle as RegExp).exec) { + // Duck-type for RegExp + if (!strict) { + // Let's consider case sensitive searches as strict + var needleRegex = needle as RegExp; + var flags = + "i" + + (needleRegex.global ? "g" : "") + + (needleRegex.multiline ? "m" : "") + + ((needleRegex as any).sticky ? "y" : ""); // sticky is FF only + needle = new RegExp(needleRegex.source, flags); + } + for (key in haystack) { + if ((needle as RegExp).test(haystack[key])) { + return key; + } + } + return false; + } + + for (key in haystack) { + if ( + (strict && haystack[key] === needle) || + (!strict && haystack[key] == needle) + ) { + return key; + } + } + + return false; +}; + +function _my_bcmod(x: string, y: string): number { + // how many numbers to take at once? carefull not to exceed (int) + var take = 5; + var mod = ""; + do { + var a = parseInt(mod + "" + x.substring(0, take)); + x = x.substring(take); + mod = String(a % parseInt(y)); + } while (x.length); + + return parseInt(mod); +} + +/** + * Creates a PDF417 object + * @param code (string) code to represent using PDF417 + * @param ecl (int) error correction level (0-8); default -1 = automatic correction level + * @param aspectratio (float) the width to height of the symbol (excluding quiet zones) + */ +export function generateBarcode(code: string, ecl?: number, aspectratio?: number) { + + code = unescape(encodeURIComponent(code)); // covert UTF-8 to ISO-8859-1 + ecl = ecl || -1; + aspectratio = aspectratio || 2; + if (code === "") { + throw new Error("PDF417 init: empty input code"); + } + // get the input sequence array + let sequence = getInputSequences(code); + let codewords: number[] = []; // array of code-words + for (var i = 0; i < sequence.length; i++) { + var cw = getCompaction(sequence[i][0], sequence[i][1], true); + codewords = codewords.concat(cw); + } + if (codewords[0] == 900) { + // Text Alpha is the default mode, so remove the first code + codewords.shift(); + } + // count number of codewords + var numcw = codewords.length; + if (numcw > 925) { + // reached maximum data codeword capacity + throw new Error("PDF417 init: maximum data codeword capacity exceeded"); + } + + // set error correction level + ecl = getErrorCorrectionLevel(ecl, numcw); + // number of codewords for error correction + var errsize = 2 << ecl; + // calculate number of columns (number of codewords per row) and rows + var nce = numcw + errsize + 1; + var cols = Math.round( + (Math.sqrt(4761 + 68 * aspectratio * ROWHEIGHT * nce) - 69) / 34 + ); + // adjust cols + if (cols < 1) { + cols = 1; + } else if (cols > 30) { + cols = 30; + } + var rows = Math.ceil(nce / cols); + var size = cols * rows; + // adjust rows + if (rows < 3 || rows > 90) { + if (rows < 3) { + rows = 3; + } else if (rows > 90) { + rows = 90; + } + cols = Math.ceil(size / rows); + size = cols * rows; + } + if (size > 928) { + // set dimensions to get maximum capacity + if ( + Math.abs(aspectratio - 17 * 29 / 32) < + Math.abs(aspectratio - 17 * 16 / 58) + ) { + cols = 29; + rows = 32; + } else { + cols = 16; + rows = 58; + } + size = 928; + } + // calculate padding + var pad = size - nce; + if (pad > 0) { + if (size - rows == nce) { + --rows; + size -= rows; + } else { + // add pading + codewords = codewords.concat(_array_fill(0, pad, 900) as number[]); + } + } + + // Symbol Length Descriptor (number of data codewords including Symbol Length Descriptor and pad codewords) + var sld = size - errsize; + // add symbol length description + codewords.unshift(sld); + // calculate error correction + var ecw = getErrorCorrection(codewords, ecl); + // add error correction codewords + codewords = codewords.concat(ecw); + // add horizontal quiet zones to start and stop patterns + var pstart = _str_repeat("0", QUIETH) + start_pattern; + var pstop = stop_pattern + "" + _str_repeat("0", QUIETH); + + const barcode_array: BarcodeArray = { + num_rows: rows * ROWHEIGHT + 2 * QUIETV, + num_cols: (cols + 2) * 17 + 35 + 2 * QUIETH, + bcode: [] + }; + + var empty_row: number[] = []; + // build rows for vertical quiet zone + if (QUIETV > 0) { + empty_row = _array_fill(0, barcode_array.num_cols, 0) as number[]; + for (var i = 0; i < QUIETV; ++i) { + // add vertical quiet rows + barcode_array.bcode.push(empty_row); + } + } + + var L: number = 0; + var k = 0; // codeword index + var cid = 0; // initial cluster + // for each row + for (var r = 0; r < rows; ++r) { + // row start code + var row = pstart; + switch (cid) { + case 0: { + L = 30 * _intval(r / 3) + _intval((rows - 1) / 3); + break; + } + case 1: { + L = 30 * _intval(r / 3) + ecl * 3 + (rows - 1) % 3; + break; + } + case 2: { + L = 30 * _intval(r / 3) + (cols - 1); + break; + } + } + // left row indicator + row += _sprintf("%17b", clusters[cid][L]); + // for each column + for (var c = 0; c < cols; ++c) { + row += _sprintf("%17b", clusters[cid][codewords[k]]); + ++k; + } + switch (cid) { + case 0: { + L = 30 * _intval(r / 3) + (cols - 1); + break; + } + case 1: { + L = 30 * _intval(r / 3) + _intval((rows - 1) / 3); + break; + } + case 2: { + L = 30 * _intval(r / 3) + ecl * 3 + (rows - 1) % 3; + break; + } + } + // right row indicator + row += _sprintf("%17b", clusters[cid][L]); + // row stop code + row += pstop; + // convert the string to array + const arow = row.split(''); + // duplicate row to get the desired height + for (var h = 0; h < ROWHEIGHT; ++h) { + barcode_array.bcode.push(arow.map(x => parseInt(x))); + } + ++cid; + if (cid > 2) { + cid = 0; + } + } + if (QUIETV > 0) { + for (var i = 0; i < QUIETV; ++i) { + // add vertical quiet rows + barcode_array.bcode.push(empty_row); + } + } + + return (barcode_array); +}; + + diff --git a/app/lib/pdf/renderBarcode.ts b/app/lib/pdf/renderBarcode.ts new file mode 100644 index 0000000..f614fd6 --- /dev/null +++ b/app/lib/pdf/renderBarcode.ts @@ -0,0 +1,51 @@ +import { BarcodeArray } from './pdf417'; + +/** + * Renders a PDF417 barcode matrix to a canvas and returns it as a data URL. + * + * This function creates an HTML canvas element, draws the barcode by iterating through + * the barcode matrix, and converts the canvas to a base64-encoded PNG data URL that + * can be used as an image source. + * + * @param barcodeMatrix - The barcode array generated by the PDF417 encoder containing + * the barcode matrix data with dimensions and binary code values + * @param blockWidth - The width in pixels of each individual barcode module (bar/space unit) + * @param blockHeight - The height in pixels of each individual barcode module (bar/space unit) + * + * @returns A data URL string (base64-encoded PNG) representing the rendered barcode image, + * suitable for use in an HTML img src attribute + * + * @example + * ```typescript + * const pdf417 = createPDF417(); + * pdf417.init("Hello World", 2, 2); + * const barcodeArray = pdf417.getBarcodeArray(); + * const dataUrl = renderBarcode(barcodeArray, 2, 4); + * // dataUrl can now be used: + * ``` + */ +export function renderBarcode(barcodeMatrix: BarcodeArray, blockWidth: number, blockHeight: number) { + + const canvas = document.createElement('canvas'); + canvas.width = barcodeMatrix.num_cols * blockWidth; + canvas.height = barcodeMatrix.num_rows * blockHeight; + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + + let positionY = 0; + for (let row = 0; row < barcodeMatrix.num_rows; row += 1) { + let positionX = 0; + + for (let col = 0; col < barcodeMatrix.num_cols; col += 1) { + if (barcodeMatrix.bcode[row][col] === 1) { + ctx.fillStyle = '#000'; + } else { + ctx.fillStyle = '#FFF'; + } + ctx.fillRect(positionX, positionY, blockWidth, blockHeight); + positionX += blockWidth; + } + positionY += blockHeight; + } + + return canvas.toDataURL(); +} \ No newline at end of file diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx index 7aa5dcc..6956707 100644 --- a/app/ui/BillEditForm.tsx +++ b/app/ui/BillEditForm.tsx @@ -10,7 +10,7 @@ import { formatYearMonth } from "../lib/format"; import { DecodeResult, findDecodePdf417 } from "../lib/pdf/barcodeDecoderWasm"; import { useLocale, useTranslations } from "next-intl"; import { InfoBox } from "./InfoBox"; -import { Pdf417BarcodeWasm } from "./Pdf417BarcodeWasm"; +import { Pdf417Barcode } from "./Pdf417Barcode"; // Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment // This is a workaround for that @@ -203,7 +203,7 @@ export const BillEditForm: FC = ({ location, bill }) => { hub3aText ?

{t.rich('barcode-disclaimer', { br: () =>
})}

: null diff --git a/app/ui/Pdf417Barcode.tsx b/app/ui/Pdf417Barcode.tsx new file mode 100644 index 0000000..3b2b8a3 --- /dev/null +++ b/app/ui/Pdf417Barcode.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { useState, useEffect, FC } from 'react'; +import { generateBarcode } from '../lib/pdf/pdf417'; +import { renderBarcode } from '../lib/pdf/renderBarcode'; + +export const Pdf417Barcode:FC<{hub3aText:string, className?: string, errorCorrectionLevel?: number}> = ({ hub3aText: hub3a_text, className, errorCorrectionLevel = 3}) => { + const [bitmapData, setBitmapData] = useState(undefined); + + console.log("Rendering Pdf417Barcode with hub3a_text:", hub3a_text); + + useEffect(() => { + const aspectRatio = 3; + + const barcodeMatrix = generateBarcode(hub3a_text, errorCorrectionLevel , aspectRatio); + const bitmap = renderBarcode(barcodeMatrix, 1, 1); + setBitmapData(bitmap); + }, [hub3a_text]); + + // Don't render until bitmap is generated (prevents hydration mismatch) + if (!bitmapData) { + return ( +
+ +
+ ); + } + + return ( + // eslint-disable-next-line @next/next/no-img-element + PDF417 Barcode + ); +} \ No newline at end of file diff --git a/app/ui/PrintPreview.tsx b/app/ui/PrintPreview.tsx index 214d6a8..27ca889 100644 --- a/app/ui/PrintPreview.tsx +++ b/app/ui/PrintPreview.tsx @@ -1,7 +1,7 @@ 'use client'; import { PrintBarcodeData } from '../lib/actions/printActions'; -import { Pdf417BarcodeWasm } from './Pdf417BarcodeWasm'; +import { Pdf417Barcode } from './Pdf417Barcode'; export interface PrintPreviewProps { data: PrintBarcodeData[]; @@ -132,7 +132,7 @@ export const PrintPreview: React.FC = ({ data, year, month, t
{ - item.hub3aText ? : null + item.hub3aText ? : null }
diff --git a/app/ui/ViewBillCard.tsx b/app/ui/ViewBillCard.tsx index 8df55d2..05a4652 100644 --- a/app/ui/ViewBillCard.tsx +++ b/app/ui/ViewBillCard.tsx @@ -8,7 +8,7 @@ import { useRouter } from "next/navigation"; import { formatYearMonth } from "../lib/format"; import { useTranslations } from "next-intl"; import { uploadProofOfPayment } from "../lib/actions/billActions"; -import { Pdf417BarcodeWasm } from "./Pdf417BarcodeWasm"; +import { Pdf417Barcode } from "./Pdf417Barcode"; export interface ViewBillCardProps { location: BillingLocation; @@ -111,7 +111,7 @@ export const ViewBillCard: FC = ({ location, bill, shareId }) hub3aText ?

{t.rich('barcode-disclaimer', { br: () =>
})}

: null diff --git a/app/ui/ViewLocationCard.tsx b/app/ui/ViewLocationCard.tsx index 21a12fc..7c34a9a 100644 --- a/app/ui/ViewLocationCard.tsx +++ b/app/ui/ViewLocationCard.tsx @@ -13,7 +13,7 @@ import { LinkIcon } from "@heroicons/react/24/outline"; import { uploadUtilBillsProofOfPayment } from "../lib/actions/locationActions"; import QRCode from "react-qr-code"; import { TicketIcon } from "@heroicons/react/24/solid"; -import { Pdf417BarcodeWasm } from "./Pdf417BarcodeWasm"; +import { Pdf417Barcode } from "./Pdf417Barcode"; export interface ViewLocationCardProps { location: BillingLocation; @@ -153,7 +153,7 @@ export const ViewLocationCard: FC = ({ location, userSett
  • {t("payment-reference-label")}
    {paymentParams.PozivNaBroj}
  • : null