typstcard/imb.py

139 lines
4 KiB
Python

import datetime
import typing
import imb_table
def get_first_serial() -> int:
"""
Generate a 6-digit serial number for an intelligent mail barcode.
The first 2 digits are the last 2 digits of the julian date, and the next 4
are serial and the next number to use is stored in a temporary file.
This will break if over 10000 serials are requested in a day
"""
# Last 2 digits of the ordinal day
date = datetime.datetime.utcnow().timetuple().tm_yday % 100
try:
serial_file = open("next_serial.txt")
next_serial = int(serial_file.read().strip())
if next_serial // 10000 == date:
first_idx = next_serial % 10000
else:
first_idx = 0
except (ValueError, IndexError, FileNotFoundError):
first_idx = 0
return date * 10000 + first_idx
def write_current_serial(current_serial: int):
serial_file = open("next_serial.txt", "w")
serial_file.write(format(current_serial, "06d"))
def _format_routing(routing: str) -> int:
if len(routing) == 0:
return 0
elif len(routing) == 5:
return int(routing) + 1
elif len(routing) == 9:
return int(routing) + 100000 + 1
elif len(routing) == 11:
return int(routing) + 1000000000 + 100000 + 1
else:
raise ValueError("Routing code must be 0, 5, 9, or 11 characters")
def _generate_crc(data_int: int) -> int:
"""
Do the weird USPS CRC11 which requires precisely 102 bits in
This is done by copying USPS code which is not very optimal
"""
data = data_int.to_bytes(13, "big")
poly = 0xF35
fcs = 0x7FF
current = data[0] << 5
for _ in range(6):
if (fcs ^ current) & 0x400:
fcs = (fcs << 1) ^ poly
else:
fcs = fcs << 1
fcs &= 0x7FF
current <<= 1
for current in data[1:]:
current <<= 3
for _ in range(8):
if (fcs ^ current) & 0x400:
fcs = (fcs << 1) ^ poly
else:
fcs = fcs << 1
fcs &= 0x7FF
current <<= 1
return fcs
def _generate_codewords(data: int, crc: int) -> typing.List[int]:
codewords = []
codewords.append(data % 636)
data //= 636
for i in range(8):
codewords.append(data % 1365)
data //= 1365
codewords.append(data)
codewords.reverse()
codewords[0] += ((crc >> 10) & 1) * 659
codewords[9] *= 2
return codewords
def _generate_characters(codewords: typing.List[int], crc: int) -> typing.List[int]:
characters = []
for idx, codeword in enumerate(codewords):
xor = ((crc >> idx) & 1) * 0x1FFF
characters.append(imb_table.CHARACTER_TABLE[codeword] ^ xor)
return characters
def _get_bit(characters: typing.List[int], character: int, bit: int) -> int:
return (characters[character] >> bit) & 1
def _generate_bars(characters: typing.List[int]) -> str:
s = ""
for bar in range(65):
descender = _get_bit(characters, *imb_table.BAR_TABLE[bar * 2])
ascender = _get_bit(characters, *imb_table.BAR_TABLE[bar * 2 + 1])
s = s + "TADF"[descender * 2 + ascender]
return s
def from_payload(data: int) -> str:
crc = _generate_crc(data)
codewords = _generate_codewords(data, crc)
characters = _generate_characters(codewords, crc)
return _generate_bars(characters)
def _generate_payload(bi: int, stid: int, mailer: int, serial: int, raw_routing: str):
if mailer >= 1000000:
decimal_tracking = int(f"{stid:03d}{mailer:09d}{serial:06d}")
else:
# Why are you using this program?
decimal_tracking = int(f"{stid:03d}{mailer:06d}{serial:09d}")
payload = _format_routing(raw_routing)
# ... what the fuck usps
payload = payload * 10 + (bi // 10)
payload = payload * 5 + (bi % 10)
payload = payload * 10**18 + decimal_tracking
return payload
def generate(bi: int, stid: int, mailer: int, serial: int, raw_routing: str):
return from_payload(_generate_payload(bi, stid, mailer, serial, raw_routing))