eanbc.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. __all__=(
  2. 'Ean13BarcodeWidget','isEanString',
  3. 'Ean8BarcodeWidget', 'UPCA', 'Ean5BarcodeWidget', 'ISBNBarcodeWidget',
  4. )
  5. from reportlab.graphics.shapes import Group, String, Rect
  6. from reportlab.lib import colors
  7. from reportlab.pdfbase.pdfmetrics import stringWidth
  8. from reportlab.lib.validators import isNumber, isColor, isString, Validator, isBoolean, NoneOr
  9. from reportlab.lib.attrmap import *
  10. from reportlab.graphics.charts.areas import PlotArea
  11. from reportlab.lib.units import mm
  12. from reportlab.lib.utils import asNative
  13. #work out a list of manufacturer codes....
  14. _eanNumberSystems = [
  15. ('00-13', 'USA & Canada'),
  16. ('20-29', 'In-Store Functions'),
  17. ('30-37', 'France'),
  18. ('40-44', 'Germany'),
  19. ('45', 'Japan (also 49)'),
  20. ('46', 'Russian Federation'),
  21. ('471', 'Taiwan'),
  22. ('474', 'Estonia'),
  23. ('475', 'Latvia'),
  24. ('477', 'Lithuania'),
  25. ('479', 'Sri Lanka'),
  26. ('480', 'Philippines'),
  27. ('482', 'Ukraine'),
  28. ('484', 'Moldova'),
  29. ('485', 'Armenia'),
  30. ('486', 'Georgia'),
  31. ('487', 'Kazakhstan'),
  32. ('489', 'Hong Kong'),
  33. ('49', 'Japan (JAN-13)'),
  34. ('50', 'United Kingdom'),
  35. ('520', 'Greece'),
  36. ('528', 'Lebanon'),
  37. ('529', 'Cyprus'),
  38. ('531', 'Macedonia'),
  39. ('535', 'Malta'),
  40. ('539', 'Ireland'),
  41. ('54', 'Belgium & Luxembourg'),
  42. ('560', 'Portugal'),
  43. ('569', 'Iceland'),
  44. ('57', 'Denmark'),
  45. ('590', 'Poland'),
  46. ('594', 'Romania'),
  47. ('599', 'Hungary'),
  48. ('600-601', 'South Africa'),
  49. ('609', 'Mauritius'),
  50. ('611', 'Morocco'),
  51. ('613', 'Algeria'),
  52. ('619', 'Tunisia'),
  53. ('622', 'Egypt'),
  54. ('625', 'Jordan'),
  55. ('626', 'Iran'),
  56. ('64', 'Finland'),
  57. ('690-692', 'China'),
  58. ('70', 'Norway'),
  59. ('729', 'Israel'),
  60. ('73', 'Sweden'),
  61. ('740', 'Guatemala'),
  62. ('741', 'El Salvador'),
  63. ('742', 'Honduras'),
  64. ('743', 'Nicaragua'),
  65. ('744', 'Costa Rica'),
  66. ('746', 'Dominican Republic'),
  67. ('750', 'Mexico'),
  68. ('759', 'Venezuela'),
  69. ('76', 'Switzerland'),
  70. ('770', 'Colombia'),
  71. ('773', 'Uruguay'),
  72. ('775', 'Peru'),
  73. ('777', 'Bolivia'),
  74. ('779', 'Argentina'),
  75. ('780', 'Chile'),
  76. ('784', 'Paraguay'),
  77. ('785', 'Peru'),
  78. ('786', 'Ecuador'),
  79. ('789', 'Brazil'),
  80. ('80-83', 'Italy'),
  81. ('84', 'Spain'),
  82. ('850', 'Cuba'),
  83. ('858', 'Slovakia'),
  84. ('859', 'Czech Republic'),
  85. ('860', 'Yugloslavia'),
  86. ('869', 'Turkey'),
  87. ('87', 'Netherlands'),
  88. ('880', 'South Korea'),
  89. ('885', 'Thailand'),
  90. ('888', 'Singapore'),
  91. ('890', 'India'),
  92. ('893', 'Vietnam'),
  93. ('899', 'Indonesia'),
  94. ('90-91', 'Austria'),
  95. ('93', 'Australia'),
  96. ('94', 'New Zealand'),
  97. ('955', 'Malaysia'),
  98. ('977', 'International Standard Serial Number for Periodicals (ISSN)'),
  99. ('978', 'International Standard Book Numbering (ISBN)'),
  100. ('979', 'International Standard Music Number (ISMN)'),
  101. ('980', 'Refund receipts'),
  102. ('981-982', 'Common Currency Coupons'),
  103. ('99', 'Coupons')
  104. ]
  105. manufacturerCodes = {}
  106. for (k, v) in _eanNumberSystems:
  107. words = k.split('-')
  108. if len(words)==2:
  109. fromCode = int(words[0])
  110. toCode = int(words[1])
  111. for code in range(fromCode, toCode+1):
  112. manufacturerCodes[code] = v
  113. else:
  114. manufacturerCodes[int(k)] = v
  115. def nDigits(n):
  116. class _ndigits(Validator):
  117. def test(self,x):
  118. return type(x) is str and len(x)<=n and len([c for c in x if c in "0123456789"])==n
  119. return _ndigits()
  120. class Ean13BarcodeWidget(PlotArea):
  121. codeName = "EAN13"
  122. _attrMap = AttrMap(BASE=PlotArea,
  123. value = AttrMapValue(nDigits(12), desc='the number'),
  124. fontName = AttrMapValue(isString, desc='fontName'),
  125. fontSize = AttrMapValue(isNumber, desc='font size'),
  126. x = AttrMapValue(isNumber, desc='x-coord'),
  127. y = AttrMapValue(isNumber, desc='y-coord'),
  128. barFillColor = AttrMapValue(isColor, desc='bar color'),
  129. barHeight = AttrMapValue(isNumber, desc='Height of bars.'),
  130. barWidth = AttrMapValue(isNumber, desc='Width of bars.'),
  131. barStrokeWidth = AttrMapValue(isNumber, desc='Width of bar borders.'),
  132. barStrokeColor = AttrMapValue(isColor, desc='Color of bar borders.'),
  133. textColor = AttrMapValue(isColor, desc='human readable text color'),
  134. humanReadable = AttrMapValue(isBoolean, desc='if human readable'),
  135. quiet = AttrMapValue(isBoolean, desc='if quiet zone to be used'),
  136. lquiet = AttrMapValue(isBoolean, desc='left quiet zone length'),
  137. rquiet = AttrMapValue(isBoolean, desc='right quiet zone length'),
  138. )
  139. _digits=12
  140. _start_right = 7 #for ean-13 left = [0:7] right=[7:13]
  141. _nbars = 113
  142. barHeight = 25.93*mm #millimeters
  143. barWidth = (37.29/_nbars)*mm
  144. humanReadable = 1
  145. _0csw = 1
  146. _1csw = 3
  147. #Left Hand Digits.
  148. _left = ( ("0001101", "0011001", "0010011", "0111101",
  149. "0100011", "0110001", "0101111", "0111011",
  150. "0110111", "0001011",
  151. ), #odd left hand digits
  152. ("0100111", "0110011", "0011011", "0100001",
  153. "0011101", "0111001", "0000101", "0010001",
  154. "0001001", "0010111"), #even left hand digits
  155. )
  156. _right = ("1110010", "1100110", "1101100", "1000010",
  157. "1011100", "1001110", "1010000", "1000100",
  158. "1001000", "1110100")
  159. quiet = 1
  160. rquiet = lquiet = None
  161. _tail = "101"
  162. _sep = "01010"
  163. _lhconvert={
  164. "0": (0,0,0,0,0,0),
  165. "1": (0,0,1,0,1,1),
  166. "2": (0,0,1,1,0,1),
  167. "3": (0,0,1,1,1,0),
  168. "4": (0,1,0,0,1,1),
  169. "5": (0,1,1,0,0,1),
  170. "6": (0,1,1,1,0,0),
  171. "7": (0,1,0,1,0,1),
  172. "8": (0,1,0,1,1,0),
  173. "9": (0,1,1,0,1,0)
  174. }
  175. fontSize = 8 #millimeters
  176. fontName = 'Helvetica'
  177. textColor = barFillColor = colors.black
  178. barStrokeColor = None
  179. barStrokeWidth = 0
  180. x = 0
  181. y = 0
  182. def __init__(self,value='123456789012',**kw):
  183. value = str(value) if isinstance(value,int) else asNative(value)
  184. self.value=max(self._digits-len(value),0)*'0'+value[:self._digits]
  185. for k, v in kw.items():
  186. setattr(self, k, v)
  187. width = property(lambda self: self.barWidth*(self._nbars-18+self._calc_quiet(self.lquiet)+self._calc_quiet(self.rquiet)))
  188. def wrap(self,aW,aH):
  189. return self.width,self.barHeight
  190. def _encode_left(self,s,a):
  191. cp = self._lhconvert[s[0]] #convert the left hand numbers
  192. _left = self._left
  193. z = ord('0')
  194. for i,c in enumerate(s[1:self._start_right]):
  195. a(_left[cp[i]][ord(c)-z])
  196. def _short_bar(self,i):
  197. i += 9 - self._lquiet
  198. return self.humanReadable and ((12<i<55) or (57<i<101))
  199. def _calc_quiet(self,v):
  200. if self.quiet:
  201. if v is None:
  202. v = 9
  203. else:
  204. x = float(max(v,0))/self.barWidth
  205. v = int(x)
  206. if v-x>0: v += 1
  207. else:
  208. v = 0
  209. return v
  210. def draw(self):
  211. g = Group()
  212. gAdd = g.add
  213. barWidth = self.barWidth
  214. width = self.width
  215. barHeight = self.barHeight
  216. x = self.x
  217. y = self.y
  218. gAdd(Rect(x,y,width,barHeight,fillColor=None,strokeColor=None,strokeWidth=0))
  219. s = self.value+self._checkdigit(self.value)
  220. self._lquiet = lquiet = self._calc_quiet(self.lquiet)
  221. rquiet = self._calc_quiet(self.rquiet)
  222. b = [lquiet*'0',self._tail] #the signal string
  223. a = b.append
  224. self._encode_left(s,a)
  225. a(self._sep)
  226. z = ord('0')
  227. _right = self._right
  228. for c in s[self._start_right:]:
  229. a(_right[ord(c)-z])
  230. a(self._tail)
  231. a(rquiet*'0')
  232. fontSize = self.fontSize
  233. barFillColor = self.barFillColor
  234. barStrokeWidth = self.barStrokeWidth
  235. barStrokeColor = self.barStrokeColor
  236. fth = fontSize*1.2
  237. b = ''.join(b)
  238. lrect = None
  239. for i,c in enumerate(b):
  240. if c=="1":
  241. dh = self._short_bar(i) and fth or 0
  242. yh = y+dh
  243. if lrect and lrect.y==yh:
  244. lrect.width += barWidth
  245. else:
  246. lrect = Rect(x,yh,barWidth,barHeight-dh,fillColor=barFillColor,strokeWidth=barStrokeWidth,strokeColor=barStrokeColor)
  247. gAdd(lrect)
  248. else:
  249. lrect = None
  250. x += barWidth
  251. if self.humanReadable: self._add_human_readable(s,gAdd)
  252. return g
  253. def _add_human_readable(self,s,gAdd):
  254. barWidth = self.barWidth
  255. fontSize = self.fontSize
  256. textColor = self.textColor
  257. fontName = self.fontName
  258. fth = fontSize*1.2
  259. # draw the num below the line.
  260. c = s[0]
  261. w = stringWidth(c,fontName,fontSize)
  262. x = self.x+barWidth*(self._lquiet-8)
  263. y = self.y + 0.2*fth
  264. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor))
  265. x = self.x + (33-9+self._lquiet)*barWidth
  266. c = s[1:7]
  267. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
  268. x += 47*barWidth
  269. c = s[7:]
  270. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
  271. def _checkdigit(cls,num):
  272. z = ord('0')
  273. iSum = cls._0csw*sum([(ord(x)-z) for x in num[::2]]) \
  274. + cls._1csw*sum([(ord(x)-z) for x in num[1::2]])
  275. return chr(z+((10-(iSum%10))%10))
  276. _checkdigit=classmethod(_checkdigit)
  277. class Ean8BarcodeWidget(Ean13BarcodeWidget):
  278. codeName = "EAN8"
  279. _attrMap = AttrMap(BASE=Ean13BarcodeWidget,
  280. value = AttrMapValue(nDigits(7), desc='the number'),
  281. )
  282. _start_right = 4 #for ean-13 left = [0:7] right=[7:13]
  283. _nbars = 85
  284. _digits=7
  285. _0csw = 3
  286. _1csw = 1
  287. def _encode_left(self,s,a):
  288. cp = self._lhconvert[s[0]] #convert the left hand numbers
  289. _left = self._left[0]
  290. z = ord('0')
  291. for i,c in enumerate(s[0:self._start_right]):
  292. a(_left[ord(c)-z])
  293. def _short_bar(self,i):
  294. i += 9 - self._lquiet
  295. return self.humanReadable and ((12<i<41) or (43<i<73))
  296. def _add_human_readable(self,s,gAdd):
  297. barWidth = self.barWidth
  298. fontSize = self.fontSize
  299. textColor = self.textColor
  300. fontName = self.fontName
  301. fth = fontSize*1.2
  302. # draw the num below the line.
  303. y = self.y + 0.2*fth
  304. x = (26.5-9+self._lquiet)*barWidth
  305. c = s[0:4]
  306. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
  307. x = (59.5-9+self._lquiet)*barWidth
  308. c = s[4:]
  309. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
  310. class UPCA(Ean13BarcodeWidget):
  311. codeName = "UPCA"
  312. _attrMap = AttrMap(BASE=Ean13BarcodeWidget,
  313. value = AttrMapValue(nDigits(11), desc='the number'),
  314. )
  315. _start_right = 6
  316. _digits = 11
  317. _0csw = 3
  318. _1csw = 1
  319. _nbars = 1+7*11+2*3+5
  320. #these methods contributed by Kyle Macfarlane
  321. #https://bitbucket.org/kylemacfarlane/
  322. def _encode_left(self,s,a):
  323. cp = self._lhconvert[s[0]] #convert the left hand numbers
  324. _left = self._left[0]
  325. z = ord('0')
  326. for i,c in enumerate(s[0:self._start_right]):
  327. a(_left[ord(c)-z])
  328. def _short_bar(self,i):
  329. i += 9 - self._lquiet
  330. return self.humanReadable and ((18<i<55) or (57<i<93))
  331. def _add_human_readable(self,s,gAdd):
  332. barWidth = self.barWidth
  333. fontSize = self.fontSize
  334. textColor = self.textColor
  335. fontName = self.fontName
  336. fth = fontSize*1.2
  337. # draw the num below the line.
  338. c = s[0]
  339. w = stringWidth(c,fontName,fontSize)
  340. x = self.x+barWidth*(self._lquiet-8)
  341. y = self.y + 0.2*fth
  342. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor))
  343. x = self.x + (38-9+self._lquiet)*barWidth
  344. c = s[1:6]
  345. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
  346. x += 36*barWidth
  347. c = s[6:11]
  348. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
  349. x += 32*barWidth
  350. c = s[11]
  351. gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor))
  352. class Ean5BarcodeWidget(Ean13BarcodeWidget):
  353. """
  354. EAN-5 barcodes can print the human readable price, set:
  355. price=True
  356. """
  357. codeName = "EAN5"
  358. _attrMap = AttrMap(BASE=Ean13BarcodeWidget,
  359. price=AttrMapValue(isBoolean,
  360. desc='whether to display the price or not'),
  361. value=AttrMapValue(nDigits(5), desc='the number'),
  362. )
  363. _nbars = 48
  364. _digits = 5
  365. _sep = '01'
  366. _tail = '01011'
  367. _0csw = 3
  368. _1csw = 9
  369. _lhconvert = {
  370. "0": (1, 1, 0, 0, 0),
  371. "1": (1, 0, 1, 0, 0),
  372. "2": (1, 0, 0, 1, 0),
  373. "3": (1, 0, 0, 0, 1),
  374. "4": (0, 1, 1, 0, 0),
  375. "5": (0, 0, 1, 1, 0),
  376. "6": (0, 0, 0, 1, 1),
  377. "7": (0, 1, 0, 1, 0),
  378. "8": (0, 1, 0, 0, 1),
  379. "9": (0, 0, 1, 0, 1)
  380. }
  381. def _checkdigit(cls, num):
  382. z = ord('0')
  383. iSum = cls._0csw * sum([(ord(x) - z) for x in num[::2]]) \
  384. + cls._1csw * sum([(ord(x) - z) for x in num[1::2]])
  385. return chr(z + iSum % 10)
  386. def _encode_left(self, s, a):
  387. check = self._checkdigit(s)
  388. cp = self._lhconvert[check]
  389. _left = self._left
  390. _sep = self._sep
  391. z = ord('0')
  392. full_code = []
  393. for i, c in enumerate(s):
  394. full_code.append(_left[cp[i]][ord(c) - z])
  395. a(_sep.join(full_code))
  396. def _short_bar(self, i):
  397. i += 9 - self._lquiet
  398. return self.humanReadable and ((12 < i < 41) or (43 < i < 73))
  399. def _add_human_readable(self, s, gAdd):
  400. barWidth = self.barWidth
  401. fontSize = self.fontSize
  402. textColor = self.textColor
  403. fontName = self.fontName
  404. fth = fontSize * 1.2
  405. # draw the num below the line.
  406. y = self.y + 0.2 * fth
  407. x = self.x + (self._nbars + self._lquiet * 2) * barWidth / 2
  408. gAdd(String(x, y, s, fontName=fontName, fontSize=fontSize,
  409. fillColor=textColor, textAnchor='middle'))
  410. price = getattr(self,'price',None)
  411. if price:
  412. price = None
  413. if s[0] in '3456':
  414. price = '$'
  415. elif s[0] in '01':
  416. price = asNative(b'\xc2\xa3')
  417. if price is None:
  418. return
  419. price += s[1:3] + '.' + s[3:5]
  420. y += self.barHeight
  421. gAdd(String(x, y, price, fontName=fontName, fontSize=fontSize,
  422. fillColor=textColor, textAnchor='middle'))
  423. def draw(self):
  424. g = Group()
  425. gAdd = g.add
  426. barWidth = self.barWidth
  427. width = self.width
  428. barHeight = self.barHeight
  429. x = self.x
  430. y = self.y
  431. gAdd(Rect(x, y, width, barHeight, fillColor=None, strokeColor=None,
  432. strokeWidth=0))
  433. s = self.value
  434. self._lquiet = lquiet = self._calc_quiet(self.lquiet)
  435. rquiet = self._calc_quiet(self.rquiet)
  436. b = [lquiet * '0' + self._tail] # the signal string
  437. a = b.append
  438. self._encode_left(s, a)
  439. a(rquiet * '0')
  440. fontSize = self.fontSize
  441. barFillColor = self.barFillColor
  442. barStrokeWidth = self.barStrokeWidth
  443. barStrokeColor = self.barStrokeColor
  444. fth = fontSize * 1.2
  445. b = ''.join(b)
  446. lrect = None
  447. for i, c in enumerate(b):
  448. if c == "1":
  449. dh = fth
  450. yh = y + dh
  451. if lrect and lrect.y == yh:
  452. lrect.width += barWidth
  453. else:
  454. lrect = Rect(x, yh, barWidth, barHeight - dh,
  455. fillColor=barFillColor,
  456. strokeWidth=barStrokeWidth,
  457. strokeColor=barStrokeColor)
  458. gAdd(lrect)
  459. else:
  460. lrect = None
  461. x += barWidth
  462. if self.humanReadable:
  463. self._add_human_readable(s, gAdd)
  464. return g
  465. class ISBNBarcodeWidget(Ean13BarcodeWidget):
  466. """
  467. ISBN Barcodes optionally print the EAN-5 supplemental price
  468. barcode (with the price in dollars or pounds). Set price to a string
  469. that follows the EAN-5 for ISBN spec:
  470. leading digit 0, 1 = GBP
  471. 3 = AUD
  472. 4 = NZD
  473. 5 = USD
  474. 6 = CAD
  475. next 4 digits = price between 00.00 and 99.98, i.e.:
  476. price='52499' # $24.99 USD
  477. """
  478. codeName = 'ISBN'
  479. _attrMap = AttrMap(BASE=Ean13BarcodeWidget,
  480. price=AttrMapValue(
  481. NoneOr(nDigits(5)),
  482. desc='None or the price to display'),
  483. )
  484. def draw(self):
  485. g = Ean13BarcodeWidget.draw(self)
  486. price = getattr(self,'price',None)
  487. if not price:
  488. return g
  489. bounds = g.getBounds()
  490. x = bounds[2]
  491. pricecode = Ean5BarcodeWidget(x=x, value=price, price=True,
  492. humanReadable=True,
  493. barHeight=self.barHeight, quiet=self.quiet)
  494. g.add(pricecode)
  495. return g
  496. def _add_human_readable(self, s, gAdd):
  497. Ean13BarcodeWidget._add_human_readable(self,s, gAdd)
  498. barWidth = self.barWidth
  499. barHeight = self.barHeight
  500. fontSize = self.fontSize
  501. textColor = self.textColor
  502. fontName = self.fontName
  503. fth = fontSize * 1.2
  504. y = self.y + 0.2 * fth + barHeight
  505. x = self._lquiet * barWidth
  506. isbn = 'ISBN '
  507. segments = [s[0:3], s[3:4], s[4:9], s[9:12], s[12]]
  508. isbn += '-'.join(segments)
  509. gAdd(String(x, y, isbn, fontName=fontName, fontSize=fontSize,
  510. fillColor=textColor))