package com.postalpresort.imb;
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
/**
* This class implements the IMB barcode generation algorithm detailed in
* https://ribbs.usps.gov/intelligentmail_mailpieces/documents/tech_guides/SPUSPSG.pdf
*
* There are 7 steps to generation detailed in section 3 of the reference:
* 3.2.1 Conversion of fields to binary data
* 3.2.1.1 Conversion of routing code
* 3.2.1.2 Conversion of tracking code
* 3.2.2 Generation of 11-bit CRC on binary data
* 3.2.3 Conversion of binary data to code words
* 3.2.4 Inserting additional information into code words
* 3.2.5 Conversion of code words to characters
*/
public class IMBGenerator {
int[] code5of13Chars;
int[] code2of13Chars;
public IMBGenerator() {
code5of13Chars = initializeNof13Table(5, 1286);
code2of13Chars = initializeNof13Table(2, 77);
}
/*
* The N of 13 tables are generated using this function. This is based on the code in Appendix D.
*/
private int[] initializeNof13Table(int n, int length) {
int[] t = new int[length + 1];
int lowerIndex = 0;
int upperIndex = length;
/* Count up to 2^13 - 1 and find all those values that have N bits on */
for (int count = 0; count < 8192; count++) {
BigInteger bi = new BigInteger(Integer.toString(count));
if (bi.bitCount() != n) continue;
// Reverse bits
int reversed = 0;
int reverseCount = count;
for (int i = 0; i < 13; i++) {
reversed = reversed << 1;
reversed = reversed | (reverseCount & 0x1);
reverseCount = reverseCount >> 1;
}
reversed = (reversed) & 0xFFFF;
if (reversed < count) continue;
if (count == reversed) {
t[upperIndex] = count;
upperIndex--;
} else {
t[lowerIndex] = count;
lowerIndex++;
t[lowerIndex] = reversed;
lowerIndex++;
}
}
// if (lowerIndex != (upperIndex + 1)) System.exit(9);
return t;
}
/*
* The routing code shall be converted from a 0-, 5-, 9-, or 11-digit string to an integer value in the range
* of 0 to 101,000,100,000 by applying the following algorithm:
* If the routing code is 0 digits long, Value = 0
* If the routing code is 5 digits long, Value = (5-digit string converted to integer) + 1
* If the routing code is 9 digits long, Value = (9-digit string converted to integer) + 100000 + 1
* If the routing code is 11 digits long, Value = (11-digit string converted to integer) + 1000000000 + 100000 + 1
*/
BigInteger binaryEncode(String barcodeId, String serviceType, String mailerId, String serialNumber, String zipCode) throws Exception {
// Sanity checks
if (barcodeId.length() != 2) throw new Exception("Barcode ID must be 2 digits in length");
if (!barcodeId.matches("\\d[0-4]")) throw new Exception("Barcode ID must match \\d[0-4]");
if (serviceType.length() != 3) throw new Exception("Service type must be 3 digits in length");
if (!serviceType.matches("\\d+")) throw new Exception("Service type must be numeric");
if (!(mailerId.length() == 6 || mailerId.length() == 9)) throw new Exception("Mailer ID must be 6 or 9 digits in length");
if (!mailerId.matches("\\d+")) throw new Exception("Mailer ID must be numeric");
if (!(serialNumber.length() == 6 || serialNumber.length() == 9)) throw new Exception("Serial number must be 6 or 9 digits in length");
if (!serialNumber.matches("\\d+")) throw new Exception("Serial number must be numeric");
if (mailerId.length() + serialNumber.length() != 15) throw new Exception("Mailer ID and Serial number length must be 15 digits together");
if (!(zipCode.length() == 0 || zipCode.length() == 5 || zipCode.length() == 9 || zipCode.length() == 11)) {
throw new Exception("Routing code must be 0, 5, 9, or 11 digits long");
}
// If the routing code is 0 digits long, Value = 0
BigInteger binaryData = BigInteger.ZERO;
// If the routing code is 5 digits long, Value = (5-digit string converted to integer) + 1
if (zipCode.length() >= 5) binaryData = binaryData.add(new BigInteger(zipCode)).add(BigInteger.ONE);
// If the routing code is 9 digits long, Value = (9-digit string converted to integer) + 100000 + 1
if (zipCode.length() >= 9) binaryData = binaryData.add(new BigInteger("100000"));
// If the routing code is 11 digits long, Value = (11-digit string converted to integer) + 1000000000 + 100000 + 1
if (zipCode.length() == 11) binaryData = binaryData.add(new BigInteger("1000000000"));
// Multiply the Binary Data field by 10 decimal, and then add the first Tracking Code digit (left digit of
// Barcode Identifier).
binaryData = binaryData.multiply(BigInteger.TEN);
binaryData = binaryData.add(new BigInteger(barcodeId.substring(0, 1)));
// Multiply the Binary Data field by 5 decimal, and then add the second Tracking Code digit (right digit of
// Barcode Identifier, which is limited to value of 0 to 4).
binaryData = binaryData.multiply(new BigInteger("5"));
binaryData = binaryData.add(new BigInteger(barcodeId.substring(1, 2)));
// For each of the remaining 18 Tracking Code digits (from left to right, each of which can range from 0 to 9),
// multiply the Binary Data Field by 10 decimal, then add the Tracking Code digit.
for (int i = 0; i < serviceType.length(); i++) {
binaryData = binaryData.multiply(BigInteger.TEN);
binaryData = binaryData.add(new BigInteger(serviceType.substring(i, i + 1)));
}
// cont...
for (int i = 0; i < mailerId.length(); i++) {
binaryData = binaryData.multiply(BigInteger.TEN);
binaryData = binaryData.add(new BigInteger(mailerId.substring(i, i + 1)));
}
// cont...
for (int i = 0; i < serialNumber.length(); i++) {
binaryData = binaryData.multiply(BigInteger.TEN);
binaryData = binaryData.add(new BigInteger(serialNumber.substring(i, i + 1)));
}
return binaryData;
}
/*
The Binary Data shall then be converted to several bases. The rightmost Codeword (J) shall be base 636.
The leftmost Codeword (A) shall use 659 values (0–658). Codewords B through I shall be base 1365. Ten
Codewords shall be generated, with the first (or leftmost) considered the most significant. The leftmost
2 bits of the 104-bit binary data shall be excluded from this conversion. The Codewords shall be labeled
(from leftmost to rightmost) A through J.
*/
int[] convertToCodeWords(BigInteger encodedData) {
final BigDecimal div1 = new BigDecimal(636);
final BigDecimal div2 = new BigDecimal(1365);
int[] codeWords = new int[10];
BigDecimal d0 = new BigDecimal(encodedData);
BigDecimal[] d1 = d0.divideAndRemainder(div1);
codeWords[9] = d1[1].intValue();
for (int i = 8; i > 0; i--) {
d1 = d1[0].divideAndRemainder(div2);
codeWords[i] = d1[1].intValue();
}
codeWords[0] = d1[0].intValue();
return codeWords;
}
/*
* Codeword J shall be modified to contain orientation information. Codeword A shall be modified to contain
* the most significant FCS bit (bit 10).
*/
int[] insertAdditionalCodewordData(int[] codeWords, int fcs) {
// Codeword J shall be converted from 0 through 635 to even numbers in the range 0 through 1270
codeWords[9] = codeWords[9] * 2;
// If the most significant bit of the FCS 11-bit value is a binary 1, Codeword A shall be incremented by 659.
BigInteger i = new BigInteger(Integer.toString(fcs));
if (i.testBit(10)) codeWords[0] = codeWords[0] + 659;
return codeWords;
}
byte[] bigIntTo13Byte(BigInteger bigInteger) {
byte[] binaryArray = new byte[13];
byte[] calculatedArray = bigInteger.toByteArray();
int offset = binaryArray.length - calculatedArray.length;
System.arraycopy(calculatedArray, 0, binaryArray, offset, calculatedArray.length);
return binaryArray;
}
/*
An 11-bit CRC Frame Check Sequence (FCS) value shall be generated by applying the Generator Polynomial
(0xF35) to the rightmost 102 bits of the Binary Data. The leftmost 2 bits of the leftmost byte shall be
excluded from the CRC calculation. This 11-bit FCS value shall be set aside for later use. The code in
Appendix C shall be used to generate the CRC. An example is shown in Figure 3.
*/
int calculateFCS(byte[] bytes) {
int polynomial = 0x0f35;
int frameCheck = 0x07ff;
int register = bytes[0] << 5;
for (int bit = 2; bit < 8; bit++) {
if (((frameCheck ^ register) & 0x400) > 0) {
frameCheck = (frameCheck << 1) ^ polynomial;
} else {
frameCheck = (frameCheck << 1);
}
frameCheck = frameCheck & 0x07ff;
register = register << 1;
}
for (int b = 1; b < bytes.length; b++) {
register = bytes[b] << 3;
for (int bit = 0; bit < 8; bit++) {
if (((frameCheck ^ register) & 0x400) > 0) {
frameCheck = (frameCheck << 1) ^ polynomial;
} else {
frameCheck = (frameCheck << 1);
}
frameCheck = frameCheck & 0x07ff;
register = register << 1;
}
}
return frameCheck;
}
/*
The Codewords shall be converted to the Characters in two steps. The Characters shall be labeled from A to
J in accordance with the Codeword from which they were generated. The bits in each Character shall be labeled
from 12 (leftmost and most significant) to 0 (rightmost and least significant). The code in Appendix D can be
used to generate the Codewords to Characters lookup tables.
*/
int[] codeWordsToCharacters(int[] codeWords) {
int[] result = new int[codeWords.length];
for (int i = 0; i < codeWords.length; i++) {
int codeWord = codeWords[i];
/*
Each Codeword shall be converted from a decimal value (ranging from 0 to 1364, except Codeword A,
which ranges from 0 to 1317, and Codeword J, which ranges from 0 to 1270 even) to a 13-bit Character.
If the Codeword has a value from 0 to 1286, the Character shall be determined by indexing into Table I,
in Appendix E, using the Codeword.
If the Codeword has a value from 1287 to 1364, the Character shall be determined by indexing into
Table II, in Appendix E, using the Codeword reduced by 1287 (result from 0 to 77).
*/
if (codeWord < 1287)
result[i] = code5of13Chars[codeWord];
else
result[i] = code2of13Chars[codeWord - 1287];
}
return result;
}
/*
Each Character shall be paired to one of the unused remaining 10 bits of the 11-bit FCS value. If the bit’s
value is 1, the Character shall be bitwise negated; if the bit’s value is 0, the Character shall be left as
is. Mapping of FCS bits to Characters is listed in Table III, in Appendix E. An example of this is shown in
Figure 8.
*/
int[] negateCodeWordsWithFCS(int[] codeWords, int fcs) {
int[] result = Arrays.copyOf(codeWords, codeWords.length);
BigInteger fcsInt = new BigInteger(Integer.toString(fcs));
for (int i = 0; i < 10; i++) {
if (fcsInt.testBit(i)) {
result[i] = ~result[i] & 0x1FFF;
}
}
return result;
}
/*
Part of codeWordsToAscenderDescender, this routine chooses the appropriate character based on the ascender and
descender bits.
Ascender Bit Descender Bit Bar
------------ ------------- -------------
true true full (F)
true false ascender (A)
false true descender (D)
false false neither (T)
*/
private void processCharacter(int[] codeWords, StringBuilder sb, char descender, int descenderBit, char ascender, int ascenderBit) {
final String charIndex = "ABCDEFGHIJ";
int index = charIndex.indexOf(descender);
BigInteger d = BigInteger.valueOf(codeWords[index]);
index = charIndex.indexOf(ascender);
BigInteger a = BigInteger.valueOf(codeWords[index]);
if (d.testBit(descenderBit) && a.testBit(ascenderBit)) {
sb.append("F");
return;
}
if (d.testBit(descenderBit)) {
sb.append("D");
return;
}
if (a.testBit(ascenderBit)) {
sb.append("A");
return;
}
sb.append("T");
}
/*
At this point there are 10 (A–J) Characters of 13 (12–0) bits each, for a total of 130 bits. Each bit shall
correspond to an extender (either an ascender or a descender) in a 65-bar Intelligent Mail barcode. A bit
value of 0 shall represent the absence of the extender, and a bit value of 1 shall represent the presence of
the extender. The bars shall be numbered from 1 (leftmost) to 65 (rightmost). Table IV in Appendix E maps
bars to characters. At this point the barcode shall consist of 65 bars, each of which is in one of four
possible states (see Figure 9).
Bars are constructed left to right, selecting bits from characters. The least significant bit is bit 0. Using
Table IV in Appendix D, the left 5 bars are constructed using the bit specified in the table.
*/
String codeWordsToAscenderDescender(int[] codeWords) {
StringBuilder sb = new StringBuilder();
processCharacter(codeWords, sb, 'H', 2, 'E', 3); // 1
processCharacter(codeWords, sb, 'B', 10, 'A', 0); // 2
processCharacter(codeWords, sb, 'J', 12, 'C', 8); // 3
processCharacter(codeWords, sb, 'F', 5, 'G', 11); // 4
processCharacter(codeWords, sb, 'I', 9, 'D', 1); // 5
processCharacter(codeWords, sb, 'A', 1, 'F', 12); // 6
processCharacter(codeWords, sb, 'C', 5, 'B', 8); // 7
processCharacter(codeWords, sb, 'E', 4, 'J', 11); // 8
processCharacter(codeWords, sb, 'G', 3, 'I', 10); // 9
processCharacter(codeWords, sb, 'D', 9, 'H', 6); // 10
processCharacter(codeWords, sb, 'F', 11, 'B', 4); // 11
processCharacter(codeWords, sb, 'I', 5, 'C', 12); // 12
processCharacter(codeWords, sb, 'J', 10, 'A', 2); // 13
processCharacter(codeWords, sb, 'H', 1, 'G', 7); // 14
processCharacter(codeWords, sb, 'D', 6, 'E', 9); // 15
processCharacter(codeWords, sb, 'A', 3, 'I', 6); // 16
processCharacter(codeWords, sb, 'G', 4, 'C', 7); // 17
processCharacter(codeWords, sb, 'B', 1, 'J', 9); // 18
processCharacter(codeWords, sb, 'H', 10, 'F', 2); // 19
processCharacter(codeWords, sb, 'E', 0, 'D', 8); // 20
processCharacter(codeWords, sb, 'G', 2, 'A', 4); // 21
processCharacter(codeWords, sb, 'I', 11, 'B', 0); // 22
processCharacter(codeWords, sb, 'J', 8, 'D', 12); // 23
processCharacter(codeWords, sb, 'C', 6, 'H', 7); // 24
processCharacter(codeWords, sb, 'F', 1, 'E', 10); // 25
processCharacter(codeWords, sb, 'B', 12, 'G', 9); // 26
processCharacter(codeWords, sb, 'H', 3, 'I', 0); // 27
processCharacter(codeWords, sb, 'F', 8, 'J', 7); // 28
processCharacter(codeWords, sb, 'E', 6, 'C', 10); // 29
processCharacter(codeWords, sb, 'D', 4, 'A', 5); // 30
processCharacter(codeWords, sb, 'I', 4, 'F', 7); // 31
processCharacter(codeWords, sb, 'H', 11, 'B', 9); // 32
processCharacter(codeWords, sb, 'G', 0, 'J', 6); // 33
processCharacter(codeWords, sb, 'A', 6, 'E', 8); // 34
processCharacter(codeWords, sb, 'C', 1, 'D', 2); // 35
processCharacter(codeWords, sb, 'F', 9, 'I', 12); // 36
processCharacter(codeWords, sb, 'E', 11, 'G', 1); // 37
processCharacter(codeWords, sb, 'J', 5, 'H', 4); // 38
processCharacter(codeWords, sb, 'D', 3, 'B', 2); // 39
processCharacter(codeWords, sb, 'A', 7, 'C', 0); // 40
processCharacter(codeWords, sb, 'B', 3, 'E', 1); // 41
processCharacter(codeWords, sb, 'G', 10, 'D', 5); // 42
processCharacter(codeWords, sb, 'I', 7, 'J', 4); // 43
processCharacter(codeWords, sb, 'C', 11, 'F', 6); // 44
processCharacter(codeWords, sb, 'A', 8, 'H', 12); // 45
processCharacter(codeWords, sb, 'E', 2, 'I', 1); // 46
processCharacter(codeWords, sb, 'F', 10, 'D', 0); // 47
processCharacter(codeWords, sb, 'J', 3, 'A', 9); // 48
processCharacter(codeWords, sb, 'G', 5, 'C', 4); // 49
processCharacter(codeWords, sb, 'H', 8, 'B', 7); // 50
processCharacter(codeWords, sb, 'F', 0, 'E', 5); // 51
processCharacter(codeWords, sb, 'C', 3, 'A', 10); // 52
processCharacter(codeWords, sb, 'G', 12, 'J', 2); // 53
processCharacter(codeWords, sb, 'D', 11, 'B', 6); // 54
processCharacter(codeWords, sb, 'I', 8, 'H', 9); // 55
processCharacter(codeWords, sb, 'F', 4, 'A', 11); // 56
processCharacter(codeWords, sb, 'B', 5, 'C', 2); // 57
processCharacter(codeWords, sb, 'J', 1, 'E', 12); // 58
processCharacter(codeWords, sb, 'I', 3, 'G', 6); // 59
processCharacter(codeWords, sb, 'H', 0, 'D', 7); // 60
processCharacter(codeWords, sb, 'E', 7, 'H', 5); // 61
processCharacter(codeWords, sb, 'A', 12, 'B', 11);// 62
processCharacter(codeWords, sb, 'C', 9, 'J', 0); // 63
processCharacter(codeWords, sb, 'G', 8, 'F', 3); // 64
processCharacter(codeWords, sb, 'D', 10, 'I', 2); // 65
return sb.toString();
}
/*
Convert a barcode id, service type, mailer id, serial number, and zip code into an IMB string
*/
public String encodeIMB(String barcodeId, String serviceType, String mailerId, String serialNumber, String zipCode) throws Exception {
BigInteger i = binaryEncode(barcodeId, serviceType, mailerId, serialNumber, zipCode);
int fcs = calculateFCS(bigIntTo13Byte(i));
int[] codeWords = convertToCodeWords(i);
codeWords = insertAdditionalCodewordData(codeWords, fcs);
codeWords = codeWordsToCharacters(codeWords);
codeWords = negateCodeWordsWithFCS(codeWords, fcs);
return codeWordsToAscenderDescender(codeWords);
}
}