imb-encode
Version:
Encode Intelligent Mail Barcodes (IMB) for USPS
613 lines (540 loc) • 20.1 kB
JavaScript
/**
* 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.
*/
;
// -----------------------------------------------------------------------------
// 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,
};