UNPKG

imb-encode

Version:

Encode Intelligent Mail Barcodes (IMB) for USPS

613 lines (540 loc) 20.1 kB
/** * index.js * * A Node.js port of the USPS Intelligent Mail Barcode (IMB) encoder, * replicating much of the logic from the original usps4cb.c code. * * Exports a function encodeIMB(trackingString, routingString) that returns * a 65-character string containing 'A', 'D', 'F', 'T' representing the * full IMB (4-state) barcode. */ "use strict"; // ----------------------------------------------------------------------------- // Error codes corresponding to the ones in EncAPI.h (for reference): // ----------------------------------------------------------------------------- const USPS_FSB_ENCODER_API_SUCCESS = 0; const USPS_FSB_ENCODER_API_SELFTEST_FAILED = 1; const USPS_FSB_ENCODER_API_BAR_STRING_IS_NULL = 2; const USPS_FSB_ENCODER_API_BYTE_CONVERSION_FAILED = 3; const USPS_FSB_ENCODER_API_RETRIEVE_TABLE_FAILED = 4; const USPS_FSB_ENCODER_API_CODEWORD_CONVERSION_FAILED = 5; const USPS_FSB_ENCODER_API_CHARACTER_RANGE_ERROR = 6; const USPS_FSB_ENCODER_API_TRACK_STRING_IS_NULL = 7; const USPS_FSB_ENCODER_API_ROUTE_STRING_IS_NULL = 8; const USPS_FSB_ENCODER_API_TRACK_STRING_BAD_LENGTH = 9; const USPS_FSB_ENCODER_API_TRACK_STRING_HAS_INVALID_DATA = 10; const USPS_FSB_ENCODER_API_TRACK_STRING_HAS_INVALID_DIGIT2 = 11; const USPS_FSB_ENCODER_API_ROUTE_STRING_BAD_LENGTH = 12; const USPS_FSB_ENCODER_API_ROUTE_STRING_HAS_INVALID_DATA = 13; // ----------------------------------------------------------------------------- // Internal tables, constants, and state // ----------------------------------------------------------------------------- // In C, "BOOLEAN" is int, but let's just use boolean in JS // We'll replicate 'TRUE' and 'FALSE' usage in logic. function TRUE() { return true; } function FALSE() { return false; } const TABLE_2_OF_13_SIZE = 78; const TABLE_5_OF_13_SIZE = 1287; // The original code defines these arrays as static in C: const BarTopCharacterIndexArray = [ 4,0,2,6,3, 5,1,9,8,7, 1,2,0,6,4, 8,2,9,5,3, 0,1,3,7,4, 6,8,9,2,0, 5,1,9,4,3, 8,6,7,1,2, 4,3,9,5,7, 8,3,0,2,1, 4,0,9,1,7, 0,2,4,6,3, 7,1,9,5,8 ]; const BarBottomCharacterIndexArray = [ 7,1,9,5,8, 0,2,4,6,3, 5,8,9,7,3, 0,6,1,7,4, 6,8,9,2,5, 1,7,5,4,3, 8,7,6,0,2, 5,4,9,3,0, 1,6,8,2,0, 4,5,9,6,7, 5,2,6,3,8, 5,1,9,8,7, 4,0,2,6,3 ]; const BarTopCharacterShiftArray = [ 3,0,8,11,1,12,8,11,10,6, 4,12,2,7,9, 6,7,9,2,8, 4,0,12,7,10, 9,0,7,10,5, 7,9,6,8,2,12,1,4,2,0, 1,5,4,6,12, 1,0,9,4,7, 5,10,2,6,9, 11,2,12,6,7, 5,11,0,3,2 ]; const BarBottomCharacterShiftArray = [ 2,10,12,5,9, 1,5,4,3,9, 11,5,10,1,6, 3,4,1,10,0, 2,11,8,6,1, 12,3,8,6,4, 4,11,0,6,1, 9,11,5,3,7, 3,10,7,11,8, 2,10,3,5,8, 0,3,12,11,8, 4,5,1,3,0, 7,12,9,8,10 ]; // 2-of-13 and 5-of-13 tables let Table2of13 = new Array(TABLE_2_OF_13_SIZE).fill(0); let Table5of13 = new Array(TABLE_5_OF_13_SIZE).fill(0); let Table2of13InitializedFlag = false; let Table5of13InitializedFlag = false; // For self-test checking let EncoderSelfTestedFlag = false; // ----------------------------------------------------------------------------- // Helper function: Reverse bits in a 16-bit short // ----------------------------------------------------------------------------- function reverseShort(input) { let reverse = 0; let x = input & 0xffff; for (let i = 0; i < 16; i++) { reverse <<= 1; reverse |= (x & 1); x >>= 1; } return reverse & 0xffff; } // ----------------------------------------------------------------------------- // Helper function: Initialize a table of all values in [0..2^13) that have // exactly N bits set. The result is stored in TableNof13. // ----------------------------------------------------------------------------- function initializeNof13Table(TableNof13, N, TableLength) { let LUT_LowerIndex = 0; let LUT_UpperIndex = TableLength - 1; for (let count = 0; count < 8192; count++) { let bitCount = 0; for (let bitIndex = 0; bitIndex < 13; bitIndex++) { if (count & (1 << bitIndex)) { bitCount++; } } if (bitCount !== N) { continue; } let reversed = reverseShort(count) >> 3; let symmetricFlag = (count === reversed); // If the reverse is less than count, we've already visited this pair if (reversed < count) { continue; } if (symmetricFlag) { TableNof13[LUT_UpperIndex] = count; LUT_UpperIndex--; } else { TableNof13[LUT_LowerIndex] = count; LUT_LowerIndex++; TableNof13[LUT_LowerIndex] = reversed; LUT_LowerIndex++; } } // We expect LUT_LowerIndex === LUT_UpperIndex+1 return (LUT_LowerIndex === LUT_UpperIndex + 1); } // ----------------------------------------------------------------------------- // Helper function: getNof13Table(N) -> returns the pointer to either the // 2-of-13 or 5-of-13 table, and how many entries it has. // ----------------------------------------------------------------------------- function getNof13Table(N) { if (N === 2) { if (!Table2of13InitializedFlag) { if (!initializeNof13Table(Table2of13, 2, TABLE_2_OF_13_SIZE)) { return { success: false }; } Table2of13InitializedFlag = true; } return { success: true, table: Table2of13, length: TABLE_2_OF_13_SIZE }; } else if (N === 5) { if (!Table5of13InitializedFlag) { if (!initializeNof13Table(Table5of13, 5, TABLE_5_OF_13_SIZE)) { return { success: false }; } Table5of13InitializedFlag = true; } return { success: true, table: Table5of13, length: TABLE_5_OF_13_SIZE }; } return { success: false }; } // ----------------------------------------------------------------------------- // Byte/array manipulation from the original code // (We store data in a ByteArray[13], then do multi-base conversions). // ----------------------------------------------------------------------------- function multiplyBytesByShort(byteArray, numberOfBytes, multiplicand) { if (!byteArray || numberOfBytes < 1) return false; let carry32 = 0; for (let i = numberOfBytes - 1; i > 0; i -= 2) { // combine two bytes let temp32 = (byteArray[i] & 0xff) | ((byteArray[i - 1] & 0xff) << 8); temp32 = temp32 * multiplicand + carry32; byteArray[i] = temp32 & 0xff; byteArray[i - 1] = (temp32 >> 8) & 0xff; carry32 = temp32 >>> 16; } // If there's one byte left if (numberOfBytes % 2 === 1) { let temp32 = (byteArray[0] & 0xff) * multiplicand + carry32; byteArray[0] = temp32 & 0xff; } return true; } function divideBytesByShort(byteArray, numberOfBytes, divisor, remainderObj) { if (!byteArray || numberOfBytes < 2 || divisor === 0) return false; let remainder32 = 0; let i = 0; // If we have an odd number of bytes, handle the first byte alone if (numberOfBytes % 2 === 1) { let temp32 = byteArray[0] & 0xff; remainder32 = temp32 % divisor; temp32 = Math.floor(temp32 / divisor); byteArray[0] = temp32 & 0xff; i = 1; } // now handle pairs of bytes for (; i < numberOfBytes; i += 2) { let slice = (remainder32 << 16) >>> 0; slice |= (byteArray[i] << 8); slice |= byteArray[i + 1]; remainder32 = slice % divisor; slice = Math.floor(slice / divisor); byteArray[i] = (slice >> 8) & 0xff; byteArray[i + 1] = slice & 0xff; } remainderObj.value = remainder32; return true; } function addShortToBytes(byteArray, numberOfBytes, addend) { if (!byteArray || numberOfBytes < 1) return false; let low = (byteArray[numberOfBytes - 1] & 0xff); let high = (byteArray[numberOfBytes - 2] & 0xff); let temp32 = (high << 8) | low; temp32 += addend; byteArray[numberOfBytes - 1] = temp32 & 0xff; byteArray[numberOfBytes - 2] = (temp32 >> 8) & 0xff; let carry32 = (temp32 > 0xffff) ? 1 : 0; let i = numberOfBytes - 3; while (carry32 === 1 && i >= 0) { let sum = (byteArray[i] & 0xff) + carry32; byteArray[i] = sum & 0xff; carry32 = sum > 0xff ? 1 : 0; i--; } return true; } function convertFromBytesToMultiBase(byteArray, numberOfBytes, numberArray, numberOfNumbers) { // Each numberArray[i] has {Base, Number} // We'll do the divisions from right to left for (let i = numberOfNumbers - 1; i >= 0; i--) { let remainderObj = { value: 0 }; if (!divideBytesByShort(byteArray, numberOfBytes, numberArray[i].Base, remainderObj)) { return false; } numberArray[i].Number = remainderObj.value; } return true; } function convertFromMultiBaseToBytes(numberArray, numberOfNumbers, byteArray, numberOfBytes) { // Start with all zero in byteArray for (let i = 0; i < numberOfBytes; i++) { byteArray[i] = 0; } for (let i = 0; i < numberOfNumbers; i++) { if (!multiplyBytesByShort(byteArray, numberOfBytes, numberArray[i].Base)) { return false; } if (!addShortToBytes(byteArray, numberOfBytes, numberArray[i].Number)) { return false; } } return true; } // ----------------------------------------------------------------------------- // CRC 11-bit generation // ----------------------------------------------------------------------------- function generateCRC11FrameCheckSequence(byteArray) { let generatorPolynomial = 0x0F35; let frameCheckSequence = 0x07FF; // 11 bits on let data = (byteArray[0] << 5) & 0xffff; // leftmost byte skipping most sig bit // first 6 bits for (let bit = 2; bit < 8; bit++) { if (((frameCheckSequence ^ data) & 0x400) !== 0) { frameCheckSequence = ((frameCheckSequence << 1) ^ generatorPolynomial) & 0x7ff; } else { frameCheckSequence = ((frameCheckSequence << 1)) & 0x7ff; } data <<= 1; } // now the remaining 12 bytes let idx = 1; for (; idx < 13; idx++) { data = (byteArray[idx] << 3) & 0xffff; for (let bit = 0; bit < 8; bit++) { if (((frameCheckSequence ^ data) & 0x400) !== 0) { frameCheckSequence = ((frameCheckSequence << 1) ^ generatorPolynomial) & 0x7ff; } else { frameCheckSequence = ((frameCheckSequence << 1)) & 0x7ff; } data <<= 1; } } return frameCheckSequence & 0x7ff; } // ----------------------------------------------------------------------------- // The core "Encode" logic from the original C code, but in JS // ----------------------------------------------------------------------------- function encodeTrackingAndRouting(trackingString, routingString) { // We'll replicate all logic from the original "Encode" function in usps4cb.c // Returns either success code or an error code, plus the resulting 65 bars. // ByteArray of length 13 for intermediate arithmetic let ByteArray = new Array(13).fill(0); // We'll store up to 12 digits of ZIP code in these arrays (base 10). const ZipArray = []; const AddArray = []; for (let i = 0; i < 12; i++) { ZipArray.push({ Base: 10, Number: 0 }); AddArray.push({ Base: 10, Number: 0 }); } // 1) Convert routing (ZIP) input into ByteArray with embedded length const routeLen = routingString.length; switch (routeLen) { case 0: // do nothing break; case 5: for (let i = 0; i < 5; i++) { ZipArray[i + 7].Number = parseInt(routingString[i], 10); } AddArray[11].Number = 1; // +1 break; case 9: for (let i = 0; i < 9; i++) { ZipArray[i + 3].Number = parseInt(routingString[i], 10); } AddArray[11].Number = 1; // +1 AddArray[6].Number = 1; // +100000 break; case 11: for (let i = 0; i < 11; i++) { ZipArray[i + 1].Number = parseInt(routingString[i], 10); } AddArray[11].Number = 1; // +1 AddArray[6].Number = 1; // +100000 AddArray[2].Number = 1; // +1000000000 break; default: return { rc: USPS_FSB_ENCODER_API_ROUTE_STRING_BAD_LENGTH, barString: "" }; } // Add the "length" embed for (let i = 11; ; i--) { ZipArray[i].Number += AddArray[i].Number; if (i === 0) { break; } if (ZipArray[i].Number >= 10) { ZipArray[i].Number -= 10; ZipArray[i - 1].Number += 1; } } // Convert from 12 base-10 digits into ByteArray if (!convertFromMultiBaseToBytes(ZipArray, 12, ByteArray, 13)) { return { rc: USPS_FSB_ENCODER_API_BYTE_CONVERSION_FAILED, barString: "" }; } // 2) Incorporate the tracking string // The tracking string is 20 digits, with second digit 0-4 // The original code does: // MultiplyBytesByShort(ByteArray, 13, 10) // AddShortToBytes(ByteArray, 13, (TrackingStringPtr[0] - '0')) // MultiplyBytesByShort(ByteArray, 13, 5) // AddShortToBytes(ByteArray, 13, (TrackingStringPtr[1] - '0')) // for digitIndex = 2..19: multiply by 10, add next digit multiplyBytesByShort(ByteArray, 13, 10); addShortToBytes(ByteArray, 13, parseInt(trackingString[0], 10)); multiplyBytesByShort(ByteArray, 13, 5); addShortToBytes(ByteArray, 13, parseInt(trackingString[1], 10)); for (let i = 2; i < 20; i++) { multiplyBytesByShort(ByteArray, 13, 10); addShortToBytes(ByteArray, 13, parseInt(trackingString[i], 10)); } // 3) Generate the CRC 11-bit let fcs = generateCRC11FrameCheckSequence(ByteArray); // 4) Convert to "codewords" in a base that allows 5-of-13 or 2-of-13 // We'll create an array of 10 codewords const CodewordArray = []; for (let i = 0; i < 10; i++) { CodewordArray.push({ Base: TABLE_5_OF_13_SIZE + TABLE_2_OF_13_SIZE, Number: 0 }); } // per the original code: first codeword base=659, last codeword base=636 CodewordArray[0].Base = 659; CodewordArray[9].Base = 636; if (!convertFromBytesToMultiBase(ByteArray, 13, CodewordArray, 10)) { return { rc: USPS_FSB_ENCODER_API_CODEWORD_CONVERSION_FAILED, barString: "" }; } // check ranges if (CodewordArray[0].Number >= 659) { return { rc: USPS_FSB_ENCODER_API_CODEWORD_CONVERSION_FAILED, barString: "" }; } if (CodewordArray[9].Number >= 636) { return { rc: USPS_FSB_ENCODER_API_CODEWORD_CONVERSION_FAILED, barString: "" }; } // orientation bit in rightmost codeword (Char J) CodewordArray[9].Number *= 2; // leftmost FCS bit in the leftmost codeword if ((fcs >> 10) !== 0) { CodewordArray[0].Number += 659; } // 5) Convert from "codewords" to 13-bit characters let r2 = getNof13Table(5); if (!r2.success) { return { rc: USPS_FSB_ENCODER_API_RETRIEVE_TABLE_FAILED, barString: "" }; } let table5 = r2.table; let r1 = getNof13Table(2); if (!r1.success) { return { rc: USPS_FSB_ENCODER_API_RETRIEVE_TABLE_FAILED, barString: "" }; } let table2 = r1.table; const CharacterArray = []; for (let i = 0; i < 10; i++) { CharacterArray.push({ Base: 8192, Number: 0 }); } for (let i = 0; i < 10; i++) { let cw = CodewordArray[i].Number; let boundary = TABLE_5_OF_13_SIZE; if (cw >= boundary + TABLE_2_OF_13_SIZE) { return { rc: USPS_FSB_ENCODER_API_CHARACTER_RANGE_ERROR, barString: "" }; } else if (cw >= boundary) { // belongs to 2-of-13 CharacterArray[i].Number = table2[cw - boundary]; } else { // belongs to 5-of-13 CharacterArray[i].Number = table5[cw]; } } // 6) Insert the rest of FCS bits (0..9) by flipping bits for 5-of-13 => 8-of-13 // (the 11th bit was stored in the leftmost codeword above) for (let i = 0; i < 10; i++) { if (fcs & (1 << i)) { CharacterArray[i].Number = (~CharacterArray[i].Number) & 0x1fff; } } // 7) Map 13-bit characters to bar positions const BarTopArray = new Array(65).fill(0); const BarBottomArray = new Array(65).fill(0); for (let i = 0; i < 65; i++) { let topChar = CharacterArray[BarTopCharacterIndexArray[i]].Number; let topShift = BarTopCharacterShiftArray[i]; let bottomChar = CharacterArray[BarBottomCharacterIndexArray[i]].Number; let bottomShift = BarBottomCharacterShiftArray[i]; BarTopArray[i] = (topChar >> topShift) & 1; BarBottomArray[i] = (bottomChar >> bottomShift) & 1; } // 8) Convert to T / D / A / F let barChars = []; for (let i = 0; i < 65; i++) { if (BarTopArray[i] === 0) { if (BarBottomArray[i] === 0) { barChars.push("T"); } else { barChars.push("D"); } } else { if (BarBottomArray[i] === 0) { barChars.push("A"); } else { barChars.push("F"); } } } return { rc: USPS_FSB_ENCODER_API_SUCCESS, barString: barChars.join("") }; } // ----------------------------------------------------------------------------- // The user-facing function: encodeIMB(trackingString, routingString) // ----------------------------------------------------------------------------- // trackingString must be exactly 20 digits, with second digit 0–4 // routingString must be 0, 5, 9, or 11 digits. function encodeIMB(trackingString, routingString) { // Validate inputs if (trackingString == null) { throw new Error("TRACK_STRING_IS_NULL"); } if (routingString == null) { throw new Error("ROUTE_STRING_IS_NULL"); } if (trackingString.length !== 20) { throw new Error("TRACK_STRING_BAD_LENGTH (must be exactly 20 digits)"); } // check for digits for (let i = 0; i < 20; i++) { if (trackingString[i] < "0" || trackingString[i] > "9") { throw new Error("TRACK_STRING_HAS_INVALID_DATA (non-digit in trackingString)"); } } // second digit 0-4 if (trackingString[1] < "0" || trackingString[1] > "4") { throw new Error("TRACK_STRING_HAS_INVALID_DIGIT2 (second digit must be 0–4)"); } // routing length must be 0, 5, 9, or 11 const validRoutingLengths = [0, 5, 9, 11]; if (!validRoutingLengths.includes(routingString.length)) { throw new Error("ROUTE_STRING_BAD_LENGTH (must be 0,5,9,or 11 digits)"); } // check routing digits for (let i = 0; i < routingString.length; i++) { if (routingString[i] < "0" || routingString[i] > "9") { throw new Error("ROUTE_STRING_HAS_INVALID_DATA (non-digit in routingString)"); } } // If all is well, call the internal "encodeTrackingAndRouting" const { rc, barString } = encodeTrackingAndRouting(trackingString, routingString); if (rc !== USPS_FSB_ENCODER_API_SUCCESS) { throw new Error("IMB Encoding error code: " + rc); } return barString; } // ----------------------------------------------------------------------------- // Optional self-test functionality (similar to the original code's SELF_TEST). // You can invoke `node index.js --selftest` to run it. // ----------------------------------------------------------------------------- function selfTest() { // The following tests are taken from the specification doc in the C code: const cases = [ { tracking: "01234567094987654321", routing: "", expected: "ATTFATTDTTADTAATTDTDTATTDAFDDFADFDFTFFFFFTATFAAAATDFFTDAADFTFDTDT", }, { tracking: "01234567094987654321", routing: "01234", expected: "DTTAFADDTTFTDTFTFDTDDADADAFADFATDDFTAAAFDTTADFAAATDFDTDFADDDTDFFT", }, { tracking: "01234567094987654321", routing: "012345678", expected: "ADFTTAFDTTTTFATTADTAAATFTFTATDAAAFDDADATATDTDTTDFDTDATADADTDFFTFA", }, { tracking: "01234567094987654321", routing: "01234567891", expected: "AADTFFDFTDADTAADAATFDTDDAAADDTDTTDAFADADDDTFFFDDTTTADFAAADFTDAADA", }, ]; for (let i = 0; i < cases.length; i++) { let c = cases[i]; try { let result = encodeIMB(c.tracking, c.routing); if (result !== c.expected) { console.error(`Self-test case #${i} FAILED`); console.error(`Got: ${result}`); console.error(`Expected: ${c.expected}`); return false; } } catch (err) { console.error(`Self-test case #${i} threw an error: ${err.message}`); return false; } } console.log("All self-test cases passed!"); return true; } // If called directly via CLI with --selftest, run selfTest if (require.main === module) { if (process.argv.includes("--selftest")) { const ok = selfTest(); if (!ok) { process.exit(1); } } } // Export the main function module.exports = { encodeIMB, };