usps4s.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. #copyright ReportLab Inc. 2000-2016
  2. #see license.txt for license details
  3. from __future__ import print_function
  4. __version__='3.3.0'
  5. __all__ = ('USPS_4State',)
  6. from reportlab.lib.colors import black
  7. from reportlab.graphics.barcode.common import Barcode
  8. from reportlab.lib.utils import asNative
  9. def nhex(i):
  10. 'normalized hex'
  11. r = hex(i)
  12. r = r[:2]+r[2:].lower()
  13. if r.endswith('l'): r = r[:-1]
  14. return r
  15. class USPS_4State(Barcode):
  16. ''' USPS 4-State OneView (TM) barcode. All info from USPS-B-3200A
  17. '''
  18. _widthSize = 1
  19. _heightSize = 1
  20. _fontSize = 11
  21. _humanReadable = 0
  22. if True:
  23. tops = dict(
  24. F = (0.0625,0.0825),
  25. T = (0.0195,0.0285),
  26. A = (0.0625,0.0825),
  27. D = (0.0195,0.0285),
  28. )
  29. bottoms = dict(
  30. F = (-0.0625,-0.0825),
  31. T = (-0.0195,-0.0285),
  32. D = (-0.0625,-0.0825),
  33. A = (-0.0195,-0.0285),
  34. )
  35. dimensions = dict(
  36. width = (0.015, 0.025),
  37. pitch = (0.0416, 0.050),
  38. hcz = (0.125,0.125),
  39. vcz = (0.028,0.028),
  40. )
  41. else:
  42. tops = dict(
  43. F = (0.067,0.115),
  44. T = (0.021,0.040),
  45. A = (0.067,0.115),
  46. D = (0.021,0.040),
  47. )
  48. bottoms = dict(
  49. F = (-0.067,-0.115),
  50. D = (-0.067,-0.115),
  51. T = (-0.021,-0.040),
  52. A = (-0.021,-0.040),
  53. )
  54. dimensions = dict(
  55. width = (0.015, 0.025),
  56. pitch = (0.0416,0.050),
  57. hcz = (0.125,0.125),
  58. vcz = (0.040,0.040),
  59. )
  60. def __init__(self,value='01234567094987654321',routing='',**kwd):
  61. self._init()
  62. value = str(value) if isinstance(value,int) else asNative(value)
  63. if not routing:
  64. #legal values for combined tracking + routing
  65. if len(value) in (20,25,29,31):
  66. value, routing = value[:20], value[20:]
  67. else:
  68. raise ValueError('value+routing length must be 20, 25, 29 or 31 digits not %d' % len(value))
  69. elif len(routing) not in (5,9,11):
  70. raise ValueError('routing length must be 5, 9 or 11 digits not %d' % len(routing))
  71. self._tracking = value
  72. self._routing = routing
  73. self._setKeywords(**kwd)
  74. def _init(self):
  75. self._bvalue = None
  76. self._codewords = None
  77. self._characters = None
  78. self._barcodes = None
  79. def scale(kind,D,s):
  80. V = D[kind]
  81. return 72*(V[0]*(1-s)+s*V[1])
  82. scale = staticmethod(scale)
  83. def tracking(self,tracking):
  84. self._init()
  85. self._tracking = tracking
  86. tracking = property(lambda self: self._tracking,tracking)
  87. def routing(self,routing):
  88. self._init()
  89. self._routing = routing
  90. routing = property(lambda self: self._routing,routing)
  91. def widthSize(self,value):
  92. self._sized = None
  93. self._widthSize = min(max(0,value),1)
  94. widthSize = property(lambda self: self._widthSize,widthSize)
  95. def heightSize(self,value):
  96. self._sized = None
  97. self._heightSize = value
  98. heightSize = property(lambda self: self._heightSize,heightSize)
  99. def fontSize(self,value):
  100. self._sized = None
  101. self._fontSize = value
  102. fontSize = property(lambda self: self._fontSize,fontSize)
  103. def humanReadable(self,value):
  104. self._sized = None
  105. self._humanReadable = value
  106. humanReadable = property(lambda self: self._humanReadable,humanReadable)
  107. def binary(self):
  108. '''convert the 4 state string values to binary
  109. >>> print(nhex(USPS_4State('01234567094987654321','').binary))
  110. 0x1122103b5c2004b1
  111. >>> print(nhex(USPS_4State('01234567094987654321','01234').binary))
  112. 0xd138a87bab5cf3804b1
  113. >>> print(nhex(USPS_4State('01234567094987654321','012345678').binary))
  114. 0x202bdc097711204d21804b1
  115. >>> print(nhex(USPS_4State('01234567094987654321','01234567891').binary))
  116. 0x16907b2a24abc16a2e5c004b1
  117. '''
  118. value = self._bvalue
  119. if not value:
  120. routing = self.routing
  121. n = len(routing)
  122. try:
  123. if n==0:
  124. value = 0
  125. elif n==5:
  126. value = int(routing)+1
  127. elif n==9:
  128. value = int(routing)+100001
  129. elif n==11:
  130. value = int(routing)+1000100001
  131. else:
  132. raise ValueError
  133. except:
  134. raise ValueError('Problem converting %s, routing code must be 0, 5, 9 or 11 digits' % routing)
  135. tracking = self.tracking
  136. svalue = tracking[0:2]
  137. try:
  138. value *= 10
  139. value += int(svalue[0])
  140. value *= 5
  141. value += int(svalue[1])
  142. except:
  143. raise ValueError('Problem converting %s, barcode identifier must be 2 digits' % svalue)
  144. i = 2
  145. for name,nd in (('special services',3), ('customer identifier',6), ('sequence number',9)):
  146. j = i
  147. i += nd
  148. svalue = tracking[j:i]
  149. try:
  150. if len(svalue)!=nd: raise ValueError
  151. for j in range(nd):
  152. value *= 10
  153. value += int(svalue[j])
  154. except:
  155. raise ValueError('Problem converting %s, %s must be %d digits' % (svalue,name,nd))
  156. self._bvalue = value
  157. return value
  158. binary = property(binary)
  159. def codewords(self):
  160. '''convert binary value into codewords
  161. >>> print(USPS_4State('01234567094987654321','01234567891').codewords)
  162. (673, 787, 607, 1022, 861, 19, 816, 1294, 35, 602)
  163. '''
  164. if not self._codewords:
  165. value = self.binary
  166. A, J = divmod(value,636)
  167. A, I = divmod(A,1365)
  168. A, H = divmod(A,1365)
  169. A, G = divmod(A,1365)
  170. A, F = divmod(A,1365)
  171. A, E = divmod(A,1365)
  172. A, D = divmod(A,1365)
  173. A, C = divmod(A,1365)
  174. A, B = divmod(A,1365)
  175. assert 0<=A<=658, 'improper value %s passed to _2codewords A-->%s' % (hex(int(value)),A)
  176. self._fcs = _crc11(value)
  177. if self._fcs&1024: A += 659
  178. J *= 2
  179. self._codewords = tuple(map(int,(A,B,C,D,E,F,G,H,I,J)))
  180. return self._codewords
  181. codewords = property(codewords)
  182. def table1(self):
  183. self.__class__.table1 = _initNof13Table(5,1287)
  184. return self.__class__.table1
  185. table1 = property(table1)
  186. def table2(self):
  187. self.__class__.table2 = _initNof13Table(2,78)
  188. return self.__class__.table2
  189. table2 = property(table2)
  190. def characters(self):
  191. ''' convert own codewords to characters
  192. >>> print(' '.join(hex(c)[2:] for c in USPS_4State('01234567094987654321','01234567891').characters))
  193. dcb 85c 8e4 b06 6dd 1740 17c6 1200 123f 1b2b
  194. '''
  195. if not self._characters:
  196. codewords = self.codewords
  197. fcs = self._fcs
  198. C = []
  199. aC = C.append
  200. table1 = self.table1
  201. table2 = self.table2
  202. for i in range(10):
  203. cw = codewords[i]
  204. if cw<=1286:
  205. c = table1[cw]
  206. else:
  207. c = table2[cw-1287]
  208. if (fcs>>i)&1:
  209. c = ~c & 0x1fff
  210. aC(c)
  211. self._characters = tuple(C)
  212. return self._characters
  213. characters = property(characters)
  214. def barcodes(self):
  215. '''Get 4 state bar codes for current routing and tracking
  216. >>> print(USPS_4State('01234567094987654321','01234567891').barcodes)
  217. AADTFFDFTDADTAADAATFDTDDAAADDTDTTDAFADADDDTFFFDDTTTADFAAADFTDAADA
  218. '''
  219. if not self._barcodes:
  220. C = self.characters
  221. B = []
  222. aB = B.append
  223. bits2bars = self._bits2bars
  224. for dc,db,ac,ab in self.table4:
  225. aB(bits2bars[((C[dc]>>db)&1)+2*((C[ac]>>ab)&1)])
  226. self._barcodes = ''.join(B)
  227. return self._barcodes
  228. barcodes = property(barcodes)
  229. table4 = ((7, 2, 4, 3), (1, 10, 0, 0), (9, 12, 2, 8), (5, 5, 6, 11),
  230. (8, 9, 3, 1), (0, 1, 5, 12), (2, 5, 1, 8), (4, 4, 9, 11),
  231. (6, 3, 8, 10), (3, 9, 7, 6), (5, 11, 1, 4), (8, 5, 2, 12),
  232. (9, 10, 0, 2), (7, 1, 6, 7), (3, 6, 4, 9), (0, 3, 8, 6),
  233. (6, 4, 2, 7), (1, 1, 9, 9), (7, 10, 5, 2), (4, 0, 3, 8),
  234. (6, 2, 0, 4), (8, 11, 1, 0), (9, 8, 3, 12), (2, 6, 7, 7),
  235. (5, 1, 4, 10), (1, 12, 6, 9), (7, 3, 8, 0), (5, 8, 9, 7),
  236. (4, 6, 2, 10), (3, 4, 0, 5), (8, 4, 5, 7), (7, 11, 1, 9),
  237. (6, 0, 9, 6), (0, 6, 4, 8), (2, 1, 3, 2), (5, 9, 8, 12),
  238. (4, 11, 6, 1), (9, 5, 7, 4), (3, 3, 1, 2), (0, 7, 2, 0),
  239. (1, 3, 4, 1), (6, 10, 3, 5), (8, 7, 9, 4), (2, 11, 5, 6),
  240. (0, 8, 7, 12), (4, 2, 8, 1), (5, 10, 3, 0), (9, 3, 0, 9),
  241. (6, 5, 2, 4), (7, 8, 1, 7), (5, 0, 4, 5), (2, 3, 0, 10),
  242. (6, 12, 9, 2), (3, 11, 1, 6), (8, 8, 7, 9), (5, 4, 0, 11),
  243. (1, 5, 2, 2), (9, 1, 4, 12), (8, 3, 6, 6), (7, 0, 3, 7),
  244. (4, 7, 7, 5), (0, 12, 1, 11), (2, 9, 9, 0), (6, 8, 5, 3),
  245. (3, 10, 8, 2))
  246. _bits2bars = 'T','D','A','F'
  247. horizontalClearZone = property(lambda self: self.scale('hcz',self.dimensions,self.widthScale))
  248. verticalClearZone = property(lambda self: self.scale('vcz',self.dimensions,self.heightScale))
  249. @property
  250. def barWidth(self):
  251. if '_barWidth' in self.__dict__:
  252. return self.__dict__['_barWidth']
  253. return self.scale('width',self.dimensions,self.widthScale)
  254. @barWidth.setter
  255. def barWidth(self,value):
  256. n, x = self.dimensions['width']
  257. self.__dict__['_barWidth'] = 72*min(max(value/72.0,n),x)
  258. @property
  259. def pitch(self):
  260. if '_pitch' in self.__dict__:
  261. return self.__dict__['_pitch']
  262. return self.scale('pitch',self.dimensions,self.widthScale)
  263. @pitch.setter
  264. def pitch(self,value):
  265. n, x = self.dimensions['pitch']
  266. self.__dict__['_pitch'] = 72*min(max(value/72.0,n),x)
  267. @property
  268. def barHeight(self):
  269. if '_barHeight' in self.__dict__:
  270. return self.__dict__['_barHeight']
  271. return self.scale('F',self.tops,self.heightScale) - self.scale('F',self.bottoms,self.heightScale)
  272. @barHeight.setter
  273. def barHeight(self,value):
  274. n = self.tops['F'][0] - self.bottoms['F'][0]
  275. x = self.tops['F'][1] - self.bottoms['F'][1]
  276. value = self.__dict__['_barHeight'] = 72*min(max(value/72.0,n),x)
  277. self.heightSize = (value - n)/(x-n)
  278. widthScale = property(lambda self: min(1,max(0,self.widthSize)))
  279. heightScale = property(lambda self: min(1,max(0,self.heightSize)))
  280. def width(self):
  281. self.computeSize()
  282. return self._width
  283. width = property(width)
  284. def height(self):
  285. self.computeSize()
  286. return self._height
  287. height = property(height)
  288. def computeSize(self):
  289. if not getattr(self,'_sized',None):
  290. ws = self.widthScale
  291. hs = self.heightScale
  292. barHeight = self.barHeight
  293. barWidth = self.barWidth
  294. pitch = self.pitch
  295. hcz = self.horizontalClearZone
  296. vcz = self.verticalClearZone
  297. self._width = 2*hcz + barWidth + 64*pitch
  298. self._height = 2*vcz+barHeight
  299. if self.humanReadable:
  300. self._height += self.fontSize*1.2+vcz
  301. self._sized = True
  302. def wrap(self,aW,aH):
  303. self.computeSize()
  304. return self.width, self.height
  305. def _getBarVInfo(self,y0=0):
  306. vInfo = {}
  307. hs = self.heightScale
  308. for b in ('T','D','A','F'):
  309. y = self.scale(b,self.bottoms,hs)+y0
  310. vInfo[b] = y,self.scale(b,self.tops,hs)+y0 - y
  311. return vInfo
  312. def draw(self):
  313. self.computeSize()
  314. hcz = self.horizontalClearZone
  315. vcz = self.verticalClearZone
  316. bw = self.barWidth
  317. x = hcz
  318. y0 = vcz+self.barHeight*0.5
  319. dw = self.pitch
  320. vInfo = self._getBarVInfo(y0)
  321. for b in self.barcodes:
  322. yb, hb = vInfo[b]
  323. self.rect(x,yb,bw,hb)
  324. x += dw
  325. self.drawHumanReadable()
  326. def value(self):
  327. tracking = self.tracking
  328. routing = self.routing
  329. routing = routing and (routing,) or ()
  330. return ' '.join((tracking[0:2],tracking[2:5],tracking[5:11],tracking[11:])+routing)
  331. value = property(value,lambda self,value: self.__dict__.__setitem__('tracking',value))
  332. def drawHumanReadable(self):
  333. if self.humanReadable:
  334. hcz = self.horizontalClearZone
  335. vcz = self.verticalClearZone
  336. fontName = self.fontName
  337. fontSize = self.fontSize
  338. y = self.barHeight+2*vcz+0.2*fontSize
  339. self.annotate(hcz,y,self.value,fontName,fontSize)
  340. def annotate(self,x,y,text,fontName,fontSize,anchor='middle'):
  341. Barcode.annotate(self,x,y,text,fontName,fontSize,anchor='start')
  342. def _crc11(value):
  343. '''
  344. >>> usps = [USPS_4State('01234567094987654321',x).binary for x in ('','01234','012345678','01234567891')]
  345. >>> print(' '.join(nhex(x) for x in usps))
  346. 0x1122103b5c2004b1 0xd138a87bab5cf3804b1 0x202bdc097711204d21804b1 0x16907b2a24abc16a2e5c004b1
  347. >>> print(' '.join(nhex(_crc11(x)) for x in usps))
  348. 0x51 0x65 0x606 0x751
  349. '''
  350. hexbytes = nhex(int(value))[2:]
  351. hexbytes = '0'*(26-len(hexbytes))+hexbytes
  352. gp = 0x0F35
  353. fcs = 0x07FF
  354. data = int(hexbytes[:2],16)<<5
  355. for b in range(2,8):
  356. if (fcs ^ data)&0x400:
  357. fcs = (fcs<<1)^gp
  358. else:
  359. fcs = fcs<<1
  360. fcs &= 0x7ff
  361. data <<= 1
  362. for x in range(2,2*13,2):
  363. data = int(hexbytes[x:x+2],16)<<3
  364. for b in range(8):
  365. if (fcs ^ data)&0x400:
  366. fcs = (fcs<<1)^gp
  367. else:
  368. fcs = fcs<<1
  369. fcs &= 0x7ff
  370. data <<= 1
  371. return fcs
  372. def _ru13(i):
  373. '''reverse unsigned 13 bit number
  374. >>> print(_ru13(7936), _ru13(31), _ru13(47), _ru13(7808))
  375. 31 7936 7808 47
  376. '''
  377. r = 0
  378. for x in range(13):
  379. r <<= 1
  380. r |= i & 1
  381. i >>= 1
  382. return r
  383. def _initNof13Table(N,lenT):
  384. '''create and return table of 13 bit values with N bits on
  385. >>> T = _initNof13Table(5,1287)
  386. >>> print(' '.join('T[%d]=%d' % (i, T[i]) for i in (0,1,2,3,4,1271,1272,1284,1285,1286)))
  387. T[0]=31 T[1]=7936 T[2]=47 T[3]=7808 T[4]=55 T[1271]=6275 T[1272]=6211 T[1284]=856 T[1285]=744 T[1286]=496
  388. '''
  389. T = lenT*[None]
  390. l = 0
  391. u = lenT-1
  392. for c in range(8192):
  393. bc = 0
  394. for b in range(13):
  395. bc += (c&(1<<b))!=0
  396. if bc!=N: continue
  397. r = _ru13(c)
  398. if r<c: continue #we already looked at this pair
  399. if r==c:
  400. T[u] = c
  401. u -= 1
  402. else:
  403. T[l] = c
  404. l += 1
  405. T[l] = r
  406. l += 1
  407. assert l==(u+1), 'u+1(%d)!=l(%d) for %d of 13 table' % (u+1,l,N)
  408. return T
  409. def _test():
  410. import doctest
  411. return doctest.testmod()
  412. if __name__ == "__main__":
  413. _test()