Skip to content

Instantly share code, notes, and snippets.

@IanSimpson
Created May 28, 2015 11:03
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save IanSimpson/96355bb98d396c4c49cb to your computer and use it in GitHub Desktop.
Save IanSimpson/96355bb98d396c4c49cb to your computer and use it in GitHub Desktop.
PHP IMB Barcode Converter
<?
/**
* IMB Converter
* =============
* This class takes a numeric code (in the valid format) and returns a string in ATDF format, ready to be printed
* with the USPS IMB font
*
* Code extracted from TCPDF and modified to operate standalone. See http://www.tcpdf.org
*
* Usage:
* include('imb.php');
* echo IMB::Convert('10556924967913040800554519513');
* // That's it!
*
*/
class IMB {
/**
* IMB - Intelligent Mail Barcode - Onecode - USPS-B-3200
* (requires PHP bcmath extension)
* Intelligent Mail barcode is a 65-bar code for use on mail in the United States.
* The fields are described as follows:<ul><li>The Barcode Identifier shall be assigned by USPS to encode the presort identification that is currently printed in human readable form on the optional endorsement line (OEL) as well as for future USPS use. This shall be two digits, with the second digit in the range of 0–4. The allowable encoding ranges shall be 00–04, 10–14, 20–24, 30–34, 40–44, 50–54, 60–64, 70–74, 80–84, and 90–94.</li><li>The Service Type Identifier shall be assigned by USPS for any combination of services requested on the mailpiece. The allowable encoding range shall be 000http://it2.php.net/manual/en/function.dechex.php–999. Each 3-digit value shall correspond to a particular mail class with a particular combination of service(s). Each service program, such as OneCode Confirm and OneCode ACS, shall provide the list of Service Type Identifier values.</li><li>The Mailer or Customer Identifier shall be assigned by USPS as a unique, 6 or 9 digit number that identifies a business entity. The allowable encoding range for the 6 digit Mailer ID shall be 000000- 899999, while the allowable encoding range for the 9 digit Mailer ID shall be 900000000-999999999.</li><li>The Serial or Sequence Number shall be assigned by the mailer for uniquely identifying and tracking mailpieces. The allowable encoding range shall be 000000000–999999999 when used with a 6 digit Mailer ID and 000000-999999 when used with a 9 digit Mailer ID. e. The Delivery Point ZIP Code shall be assigned by the mailer for routing the mailpiece. This shall replace POSTNET for routing the mailpiece to its final delivery point. The length may be 0, 5, 9, or 11 digits. The allowable encoding ranges shall be no ZIP Code, 00000–99999, 000000000–999999999, and 00000000000–99999999999.</li></ul>
* @param $code (string) code to print, separate the ZIP (routing code) from the rest using a minus char '-' (BarcodeID_ServiceTypeID_MailerID_SerialNumber-RoutingCode)
* @return array barcode representation.
* @protected
*/
static function Convert($code) {
$asc_chr = array(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);
$dsc_chr = array(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);
$asc_pos = array(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);
$dsc_pos = array(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);
$tracking_number = substr($code,0,20);
$routing_code = substr($code,20);
// Conversion of Routing Code
switch (strlen($routing_code)) {
case 0: {
$binary_code = 0;
break;
}
case 5: {
$binary_code = bcadd($routing_code, '1');
break;
}
case 9: {
$binary_code = bcadd($routing_code, '100001');
break;
}
case 11: {
$binary_code = bcadd($routing_code, '1000100001');
break;
}
default: {
return false;
break;
}
}
$binary_code = bcmul($binary_code, 10);
$binary_code = bcadd($binary_code, $tracking_number[0]);
$binary_code = bcmul($binary_code, 5);
$binary_code = bcadd($binary_code, $tracking_number[1]);
$binary_code .= substr($tracking_number, 2, 18);
// convert to hexadecimal
$binary_code = self::dec_to_hex($binary_code);
// pad to get 13 bytes
$binary_code = str_pad($binary_code, 26, '0', STR_PAD_LEFT);
// convert string to array of bytes
$binary_code_arr = chunk_split($binary_code, 2, "\r");
$binary_code_arr = substr($binary_code_arr, 0, -1);
$binary_code_arr = explode("\r", $binary_code_arr);
// calculate frame check sequence
$fcs = self::imb_crc11fcs($binary_code_arr);
// exclude first 2 bits from first byte
$first_byte = sprintf('%2s', dechex((hexdec($binary_code_arr[0]) << 2) >> 2));
$binary_code_102bit = $first_byte.substr($binary_code, 2);
// convert binary data to codewords
$codewords = array();
$data = self::hex_to_dec($binary_code_102bit);
$codewords[0] = bcmod($data, 636) * 2;
$data = bcdiv($data, 636);
for ($i = 1; $i < 9; ++$i) {
$codewords[$i] = bcmod($data, 1365);
$data = bcdiv($data, 1365);
}
$codewords[9] = $data;
if (($fcs >> 10) == 1) {
$codewords[9] += 659;
}
// generate lookup tables
$table2of13 = self::imb_tables(2, 78);
$table5of13 = self::imb_tables(5, 1287);
// convert codewords to characters
$characters = array();
$bitmask = 512;
foreach($codewords as $k => $val) {
if ($val <= 1286) {
$chrcode = $table5of13[$val];
} else {
$chrcode = $table2of13[($val - 1287)];
}
if (($fcs & $bitmask) > 0) {
// bitwise invert
$chrcode = ((~$chrcode) & 8191);
}
$characters[] = $chrcode;
$bitmask /= 2;
}
$characters = array_reverse($characters);
// build bars
$k = 0;
$out = array();
for ($i = 0; $i <= 64; ++$i) {
$asc = (($characters[$asc_chr[$i]] & pow(2, $asc_pos[$i])) > 0);
$dsc = (($characters[$dsc_chr[$i]] & pow(2, $dsc_pos[$i])) > 0);
if ($asc AND $dsc) {
// full bar (F)
$c = 'F';
} elseif ($asc) {
// ascender (A)
$c = 'A';
} elseif ($dsc) {
// descender (D)
$c = 'D';
} else {
// tracker (T)
$c = 'T';
}
$out[] = $c;
}
return implode('',$out);
}
/**
* Convert large integer number to hexadecimal representation.
* (requires PHP bcmath extension)
* @param $number (string) number to convert specified as a string
* @return string hexadecimal representation
*/
static public function dec_to_hex($number) {
$i = 0;
$hex = array();
if($number == 0) {
return '00';
}
while($number > 0) {
if($number == 0) {
array_push($hex, '0');
} else {
array_push($hex, strtoupper(dechex(bcmod($number, '16'))));
$number = bcdiv($number, '16', 0);
}
}
$hex = array_reverse($hex);
return implode($hex);
}
/**
* Convert large hexadecimal number to decimal representation (string).
* (requires PHP bcmath extension)
* @param $hex (string) hexadecimal number to convert specified as a string
* @return string hexadecimal representation
*/
static public function hex_to_dec($hex) {
$dec = 0;
$bitval = 1;
$len = strlen($hex);
for($pos = ($len - 1); $pos >= 0; --$pos) {
$dec = bcadd($dec, bcmul(hexdec($hex{$pos}), $bitval));
$bitval = bcmul($bitval, 16);
}
return $dec;
}
/**
* Intelligent Mail Barcode calculation of Frame Check Sequence
* @param $code_arr (string) array of hexadecimal values (13 bytes holding 102 bits right justified).
* @return int 11 bit Frame Check Sequence as integer (decimal base)
* @protected
*/
static protected function imb_crc11fcs($code_arr) {
$genpoly = 0x0F35; // generator polynomial
$fcs = 0x07FF; // Frame Check Sequence
// do most significant byte skipping the 2 most significant bits
$data = hexdec($code_arr[0]) << 5;
for ($bit = 2; $bit < 8; ++$bit) {
if (($fcs ^ $data) & 0x400) {
$fcs = ($fcs << 1) ^ $genpoly;
} else {
$fcs = ($fcs << 1);
}
$fcs &= 0x7FF;
$data <<= 1;
}
// do rest of bytes
for ($byte = 1; $byte < 13; ++$byte) {
$data = hexdec($code_arr[$byte]) << 3;
for ($bit = 0; $bit < 8; ++$bit) {
if (($fcs ^ $data) & 0x400) {
$fcs = ($fcs << 1) ^ $genpoly;
} else {
$fcs = ($fcs << 1);
}
$fcs &= 0x7FF;
$data <<= 1;
}
}
return $fcs;
}
/**
* generate Nof13 tables used for Intelligent Mail Barcode
* @param $n (int) is the type of table: 2 for 2of13 table, 5 for 5of13table
* @param $size (int) size of table (78 for n=2 and 1287 for n=5)
* @return array requested table
* @protected
*/
static protected function imb_tables($n, $size) {
$table = array();
$lli = 0; // LUT lower index
$lui = $size - 1; // LUT upper index
for ($count = 0; $count < 8192; ++$count) {
$bit_count = 0;
for ($bit_index = 0; $bit_index < 13; ++$bit_index) {
$bit_count += intval(($count & (1 << $bit_index)) != 0);
}
// if we don't have the right number of bits on, go on to the next value
if ($bit_count == $n) {
$reverse = (self::imb_reverse_us($count) >> 3);
// if the reverse is less than count, we have already visited this pair before
if ($reverse >= $count) {
// If count is symmetric, place it at the first free slot from the end of the list.
// Otherwise, place it at the first free slot from the beginning of the list AND place $reverse ath the next free slot from the beginning of the list
if ($reverse == $count) {
$table[$lui] = $count;
--$lui;
} else {
$table[$lli] = $count;
++$lli;
$table[$lli] = $reverse;
++$lli;
}
}
}
}
return $table;
}
/**
* Reverse unsigned short value
* @param $num (int) value to reversr
* @return int reversed value
* @protected
*/
static protected function imb_reverse_us($num) {
$rev = 0;
for ($i = 0; $i < 16; ++$i) {
$rev <<= 1;
$rev |= ($num & 1);
$num >>= 1;
}
return $rev;
}
}
@seratne
Copy link

seratne commented Apr 17, 2017

I added a small change to this which creates a very nice performance benefit when it's ran in a loop by not having to calculate the 2of13 and 5of13 tables for every iteration, since those are independent of the input. https://gist.github.com/seratne/2d875facb16441d349bc8c0ddad9620c

@tylerdevs
Copy link

Ian, if possible could you show how to use the code to go the other way and decode the characters back to digits?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment