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