[WIP] hw_escpos: printing client-side defined xml templates
[odoo/odoo.git] / addons / hw_escpos / escpos / escpos.py
1 #!/usr/bin/python
2 '''
3 @author: Manuel F Martinez <manpaz@bashlinux.com>
4 @organization: Bashlinux
5 @copyright: Copyright (c) 2012 Bashlinux
6 @license: GPL
7 '''
8
9 try: 
10     import qrcode
11 except ImportError:
12     qrcode = None
13
14 import time
15 import copy
16 import io
17 import base64
18 import math
19 import md5
20 import re
21 import xml.etree.ElementTree as ET
22
23 from PIL import Image
24
25 try:
26     import jcconv
27 except ImportError:
28     jcconv = None
29     print 'ESC/POS: please install jcconv for improved Japanese receipt printing:'
30     print ' # pip install jcconv'
31
32 from constants import *
33 from exceptions import *
34
35 # class RBuffer:
36 #     def __init__(self,width=40,margin=10,tabwidth=2,indent=0):
37 #         self.width = width 
38 #         self.margin = margin
39 #         self.tabwidth = tabwidth
40 #         self.indent = indent
41 #         self.lines  = []
42
43 class Escpos:
44     """ ESC/POS Printer object """
45     device    = None
46     encoding  = None
47     img_cache = {}
48
49     def _check_image_size(self, size):
50         """ Check and fix the size of the image to 32 bits """
51         if size % 32 == 0:
52             return (0, 0)
53         else:
54             image_border = 32 - (size % 32)
55             if (image_border % 2) == 0:
56                 return (image_border / 2, image_border / 2)
57             else:
58                 return (image_border / 2, (image_border / 2) + 1)
59
60     def _print_image(self, line, size):
61         """ Print formatted image """
62         i = 0
63         cont = 0
64         buffer = ""
65
66        
67         self._raw(S_RASTER_N)
68         buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
69         self._raw(buffer.decode('hex'))
70         buffer = ""
71
72         while i < len(line):
73             hex_string = int(line[i:i+8],2)
74             buffer += "%02X" % hex_string
75             i += 8
76             cont += 1
77             if cont % 4 == 0:
78                 self._raw(buffer.decode("hex"))
79                 buffer = ""
80                 cont = 0
81
82     def _raw_print_image(self, line, size, output=None ):
83         """ Print formatted image """
84         i = 0
85         cont = 0
86         buffer = ""
87         raw = ""
88
89         def __raw(string):
90             if output:
91                 output(string)
92             else:
93                 self._raw(string)
94        
95         raw += S_RASTER_N
96         buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
97         raw += buffer.decode('hex')
98         buffer = ""
99
100         while i < len(line):
101             hex_string = int(line[i:i+8],2)
102             buffer += "%02X" % hex_string
103             i += 8
104             cont += 1
105             if cont % 4 == 0:
106                 raw += buffer.decode("hex")
107                 buffer = ""
108                 cont = 0
109
110         return raw
111
112     def _convert_image(self, im):
113         """ Parse image and prepare it to a printable format """
114         pixels   = []
115         pix_line = ""
116         im_left  = ""
117         im_right = ""
118         switch   = 0
119         img_size = [ 0, 0 ]
120
121
122         if im.size[0] > 512:
123             print  "WARNING: Image is wider than 512 and could be truncated at print time "
124         if im.size[1] > 255:
125             raise ImageSizeError()
126
127         im_border = self._check_image_size(im.size[0])
128         for i in range(im_border[0]):
129             im_left += "0"
130         for i in range(im_border[1]):
131             im_right += "0"
132
133         for y in range(im.size[1]):
134             img_size[1] += 1
135             pix_line += im_left
136             img_size[0] += im_border[0]
137             for x in range(im.size[0]):
138                 img_size[0] += 1
139                 RGB = im.getpixel((x, y))
140                 im_color = (RGB[0] + RGB[1] + RGB[2])
141                 im_pattern = "1X0"
142                 pattern_len = len(im_pattern)
143                 switch = (switch - 1 ) * (-1)
144                 for x in range(pattern_len):
145                     if im_color <= (255 * 3 / pattern_len * (x+1)):
146                         if im_pattern[x] == "X":
147                             pix_line += "%d" % switch
148                         else:
149                             pix_line += im_pattern[x]
150                         break
151                     elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3):
152                         pix_line += im_pattern[-1]
153                         break 
154             pix_line += im_right
155             img_size[0] += im_border[1]
156
157         return (pix_line, img_size)
158
159     def image(self,path_img):
160         """ Open image file """
161         im_open = Image.open(path_img)
162         im = im_open.convert("RGB")
163         # Convert the RGB image in printable image
164         pix_line, img_size = self._convert_image(im)
165         self._print_image(pix_line, img_size)
166
167     def print_base64_image(self,img):
168
169         print 'print_b64_img'
170
171         id = md5.new(img).digest()
172
173         if id not in self.img_cache:
174             print 'not in cache'
175
176             img = img[img.find(',')+1:]
177             f = io.BytesIO('img')
178             f.write(base64.decodestring(img))
179             f.seek(0)
180             img_rgba = Image.open(f)
181             img = Image.new('RGB', img_rgba.size, (255,255,255))
182             img.paste(img_rgba, mask=img_rgba.split()[3]) 
183
184             print 'convert image'
185         
186             pix_line, img_size = self._convert_image(img)
187
188             print 'print image'
189
190             buffer = self._raw_print_image(pix_line, img_size)
191             self.img_cache[id] = buffer
192
193         print 'raw image'
194
195         self._raw(self.img_cache[id])
196
197     def qr(self,text):
198         """ Print QR Code for the provided string """
199         qr_code = qrcode.QRCode(version=4, box_size=4, border=1)
200         qr_code.add_data(text)
201         qr_code.make(fit=True)
202         qr_img = qr_code.make_image()
203         im = qr_img._img.convert("RGB")
204         # Convert the RGB image in printable image
205         self._convert_image(im)
206
207     def barcode(self, code, bc, width=255, height=2, pos='below', font='a'):
208         """ Print Barcode """
209         # Align Bar Code()
210         self._raw(TXT_ALIGN_CT)
211         # Height
212         if height >=2 or height <=6:
213             self._raw(BARCODE_HEIGHT)
214         else:
215             raise BarcodeSizeError()
216         # Width
217         if width >= 1 or width <=255:
218             self._raw(BARCODE_WIDTH)
219         else:
220             raise BarcodeSizeError()
221         # Font
222         if font.upper() == "B":
223             self._raw(BARCODE_FONT_B)
224         else: # DEFAULT FONT: A
225             self._raw(BARCODE_FONT_A)
226         # Position
227         if pos.upper() == "OFF":
228             self._raw(BARCODE_TXT_OFF)
229         elif pos.upper() == "BOTH":
230             self._raw(BARCODE_TXT_BTH)
231         elif pos.upper() == "ABOVE":
232             self._raw(BARCODE_TXT_ABV)
233         else:  # DEFAULT POSITION: BELOW 
234             self._raw(BARCODE_TXT_BLW)
235         # Type 
236         if bc.upper() == "UPC-A":
237             self._raw(BARCODE_UPC_A)
238         elif bc.upper() == "UPC-E":
239             self._raw(BARCODE_UPC_E)
240         elif bc.upper() == "EAN13":
241             self._raw(BARCODE_EAN13)
242         elif bc.upper() == "EAN8":
243             self._raw(BARCODE_EAN8)
244         elif bc.upper() == "CODE39":
245             self._raw(BARCODE_CODE39)
246         elif bc.upper() == "ITF":
247             self._raw(BARCODE_ITF)
248         elif bc.upper() == "NW7":
249             self._raw(BARCODE_NW7)
250         else:
251             raise BarcodeTypeError()
252         # Print Code
253         if code:
254             self._raw(code)
255         else:
256             raise exception.BarcodeCodeError()
257
258     def receipt(self,xml):
259         root = ET.fromstring(xml)
260         open_cashdrawer = False
261         cut_receipt  = True
262
263         width  = 48
264         ratio  = 0.5
265         indent = 0
266         tabwidth = 2
267
268         if root.tag != 'receipt':
269             print 'Error: expecting receipt xml and not '+str(root.tag)
270             return
271         if 'open-cashdrawer' in root.attrib:
272             open_cashdrawer = root.attrib['open-cashdrawer'] == 'true'
273         if 'cut' in root.attrib:
274             cut_receipt = root.attrib['cut'] == 'true'
275
276 #        def justify(string,width):
277 #            words = string.split()
278 #            lines = []
279 #            line  = ''
280 #            linew = 0
281 #            for w in words:
282 #                if len(w) <= width - len(line):
283 #                    if len(line) > 0:
284 #                        line += ' '
285 #                    line += w
286 #                else:
287 #                    if len(line) > 0:
288 #                        lines.append(line.ljust(width))
289 #                        line = ''
290 #                    line += w
291 #            if len(line) > 0:
292 #                lines.append(line.ljust(width))
293 #            return lines
294 #
295 #        def strindent(indent,string):
296 #            ind = tabwidth * indent
297 #            rem = width - ind
298 #            lines = justify(string,rem)
299 #            txt = ''
300 #            for l in lines:
301 #                txt += ' '*ind + '\n'
302 #
303 #            return txt
304
305         def strclean(string):
306             if not string:
307                 string = ''
308             string = string.strip()
309             string = re.sub('\s+',' ',string)
310             return string
311
312
313         def print_ul(elem, indent=0):
314             bullets = ['-','-']
315             bullet  = ' '+bullets[indent % 2]+' '
316             if elem.tag == 'ul':
317                 for lelem in elem:
318                     if lelem.tag == 'li':
319                         self.text(' ' * indent * tabwidth + bullet + strclean(lelem.text)+'\n')
320                     elif lelem.tag == 'ul':
321                         print_ul(lelem,indent+1)
322                     elif lelem.tag == 'ol':
323                         print_old(lelem,indent+1)
324
325         def print_ol(elem, indent=0):
326             cwidth = len(str(len(elem))) + 2
327             i = 1
328             for lelem in elem:
329                 if lelem.tag == 'li':
330                     self.text(' ' * indent * tabwidth + ' ' + (str(i)+'.').ljust(cwidth)+strclean(lelem.text)+'\n')
331                     i += 1
332                 elif lelem.tag == 'ul':
333                     print_ul(lelem,indent+1)
334                 elif lelem.tag == 'ol':
335                     print_ol(lelem,indent+1)
336         
337         
338         for elem in root:
339             if elem.tag == 'line':
340                 left = strclean(elem.text)
341                 right = ''
342                 for lelem in elem:
343                     if lelem.tag == 'left':
344                         left = strclean(lelem.text)
345                         print 'left:'+left
346                     elif lelem.tag == 'right':
347                         right = strclean(lelem.text)
348                         print 'right:'+right
349                 lwidth = int(width * ratio)
350                 rwidth = width - lwidth
351                 lwidth = lwidth - indent
352
353                 left = left[:lwidth]
354                 if len(left) != lwidth:
355                     left = left + ' ' * (lwidth - len(left))
356
357                 right = right[-rwidth:]
358                 if len(right) != rwidth:
359                     right = ' ' * (rwidth - len(right)) + right
360                 line = ' ' * indent + left + right + '\n'
361                 print line
362                 self.text(line)
363             elif elem.tag == 'img':
364                 if src in elem.attrib and 'data:' in elem.attrib['src']:
365                     self.print_base64_image(elem.attrib['src'])
366             elif elem.tag == 'p':
367                 self.text(strclean(elem.text)+'\n')
368             elif elem.tag == 'h1':
369                 self.set(align='left', font='a', type='b', width=2, height=2)
370                 self._raw('\x1b\x21\x30')
371                 self._raw('\x1b\x45\x01')
372                 self.text(strclean(elem.text)+'\n')
373                 self.set()
374             elif elem.tag == 'h2':
375                 self.set(align='left', font='a', type='bu', width=1, height=2)
376                 self._raw('\x1b\x21\x30')
377                 self.text(strclean(elem.text)+'\n')
378                 self.set()
379             elif elem.tag == 'h3':
380                 self.set(align='left', font='a', type='u', width=1, height=2)
381                 self._raw('\x1b\x45\x01')
382                 self.text(strclean(elem.text)+'\n')
383                 self.set()
384             elif elem.tag == 'h4':
385                 self.set(align='left', font='a', type='bu', width=1, height=2)
386                 self.text(strclean(elem.text)+'\n')
387                 self.set()
388             elif elem.tag == 'h5':
389                 self.set(align='left', font='a', type='u', width=1, height=1)
390                 self._raw('\x1b\x45\x01')
391                 self.text(strclean(elem.text)+'\n')
392                 self.set()
393             elif elem.tag == 'pre':
394                 self.text(elem.text)
395             elif elem.tag == 'cut':
396                 self.cut()
397             elif elem.tag == 'ul':
398                 print_ul(elem)
399             elif elem.tag == 'ol':
400                 print_ol(elem)
401             elif elem.tag == 'hr':
402                 self.text('-'*width+'\n')
403             elif elem.tag == 'br':
404                 self.text('\n')
405             elif elem.tag == 'barcode' and 'encoding' in elem.attrib:
406                 self.barcode(strclean(elem.text),elem.attrib['encoding'])
407             elif elem.tag == 'partialcut':
408                 self.cut(mode='part')
409             elif elem.tag == 'cashdraw':
410                 self.cashdraw(2)
411                 self.cashdraw(5)
412
413         if cut_receipt:
414             self.cut()
415         if open_cashdrawer:
416             self.cashdraw(2)
417             self.cashdraw(5)
418
419
420     def text(self,txt):
421         """ Print Utf8 encoded alpha-numeric text """
422         if not txt:
423             return
424         try:
425             txt = txt.decode('utf-8')
426         except:
427             try:
428                 txt = txt.decode('utf-16')
429             except:
430                 pass
431
432         self.extra_chars = 0
433         
434         def encode_char(char):  
435             """ 
436             Encodes a single utf-8 character into a sequence of 
437             esc-pos code page change instructions and character declarations 
438             """ 
439             char_utf8 = char.encode('utf-8')
440             encoded  = ''
441             encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character
442             encodings = {
443                     # TODO use ordering to prevent useless switches
444                     # TODO Support other encodings not natively supported by python ( Thai, Khazakh, Kanjis )
445                     'cp437': TXT_ENC_PC437,
446                     'cp850': TXT_ENC_PC850,
447                     'cp852': TXT_ENC_PC852,
448                     'cp857': TXT_ENC_PC857,
449                     'cp858': TXT_ENC_PC858,
450                     'cp860': TXT_ENC_PC860,
451                     'cp863': TXT_ENC_PC863,
452                     'cp865': TXT_ENC_PC865,
453                     'cp866': TXT_ENC_PC866,
454                     'cp862': TXT_ENC_PC862,
455                     'cp720': TXT_ENC_PC720,
456                     'iso8859_2': TXT_ENC_8859_2,
457                     'iso8859_7': TXT_ENC_8859_7,
458                     'iso8859_9': TXT_ENC_8859_9,
459                     'cp1254'   : TXT_ENC_WPC1254,
460                     'cp1255'   : TXT_ENC_WPC1255,
461                     'cp1256'   : TXT_ENC_WPC1256,
462                     'cp1257'   : TXT_ENC_WPC1257,
463                     'cp1258'   : TXT_ENC_WPC1258,
464                     'katakana' : TXT_ENC_KATAKANA,
465             }
466             remaining = copy.copy(encodings)
467
468             if not encoding :
469                 encoding = 'cp437'
470
471             while True: # Trying all encoding until one succeeds
472                 try:
473                     if encoding == 'katakana': # Japanese characters
474                         if jcconv:
475                             # try to convert japanese text to a half-katakanas 
476                             kata = jcconv.kata2half(jcconv.hira2kata(char_utf8))
477                             if kata != char_utf8:
478                                 self.extra_chars += len(kata.decode('utf-8')) - 1
479                                 # the conversion may result in multiple characters
480                                 return encode_str(kata.decode('utf-8')) 
481                         else:
482                              kata = char_utf8
483                         
484                         if kata in TXT_ENC_KATAKANA_MAP:
485                             encoded = TXT_ENC_KATAKANA_MAP[kata]
486                             break
487                         else: 
488                             raise ValueError()
489                     else:
490                         encoded = char.encode(encoding)
491                         break
492
493                 except ValueError: #the encoding failed, select another one and retry
494                     if encoding in remaining:
495                         del remaining[encoding]
496                     if len(remaining) >= 1:
497                         encoding = remaining.items()[0][0]
498                     else:
499                         encoding = 'cp437'
500                         encoded  = '\xb1'    # could not encode, output error character
501                         break;
502
503             if encoding != self.encoding:
504                 # if the encoding changed, remember it and prefix the character with
505                 # the esc-pos encoding change sequence
506                 self.encoding = encoding
507                 encoded = encodings[encoding] + encoded
508
509             return encoded
510         
511         def encode_str(txt):
512             buffer = ''
513             for c in txt:
514                 buffer += encode_char(c)
515             return buffer
516
517         txt = encode_str(txt)
518
519         # if the utf-8 -> codepage conversion inserted extra characters, 
520         # remove double spaces to try to restore the original string length
521         # and prevent printing alignment issues
522         while self.extra_chars > 0: 
523             dspace = txt.find('  ')
524             if dspace > 0:
525                 txt = txt[:dspace] + txt[dspace+1:]
526                 self.extra_chars -= 1
527             else:
528                 break
529
530         self._raw(txt)
531         
532     def set(self, align='left', font='a', type='normal', width=1, height=1):
533         """ Set text properties """
534         # Align
535         if align.upper() == "CENTER":
536             self._raw(TXT_ALIGN_CT)
537         elif align.upper() == "RIGHT":
538             self._raw(TXT_ALIGN_RT)
539         elif align.upper() == "LEFT":
540             self._raw(TXT_ALIGN_LT)
541         # Font
542         if font.upper() == "B":
543             self._raw(TXT_FONT_B)
544         else:  # DEFAULT FONT: A
545             self._raw(TXT_FONT_A)
546         # Type
547         if type.upper() == "B":
548             self._raw(TXT_BOLD_ON)
549             self._raw(TXT_UNDERL_OFF)
550         elif type.upper() == "U":
551             self._raw(TXT_BOLD_OFF)
552             self._raw(TXT_UNDERL_ON)
553         elif type.upper() == "U2":
554             self._raw(TXT_BOLD_OFF)
555             self._raw(TXT_UNDERL2_ON)
556         elif type.upper() == "BU":
557             self._raw(TXT_BOLD_ON)
558             self._raw(TXT_UNDERL_ON)
559         elif type.upper() == "BU2":
560             self._raw(TXT_BOLD_ON)
561             self._raw(TXT_UNDERL2_ON)
562         elif type.upper == "NORMAL":
563             self._raw(TXT_BOLD_OFF)
564             self._raw(TXT_UNDERL_OFF)
565         # Width
566         if width == 2 and height != 2:
567             self._raw(TXT_NORMAL)
568             self._raw(TXT_2WIDTH)
569         elif height == 2 and width != 2:
570             self._raw(TXT_NORMAL)
571             self._raw(TXT_2HEIGHT)
572         elif height == 2 and width == 2:
573             self._raw(TXT_2WIDTH)
574             self._raw(TXT_2HEIGHT)
575         else: # DEFAULT SIZE: NORMAL
576             self._raw(TXT_NORMAL)
577
578
579     def cut(self, mode=''):
580         """ Cut paper """
581         # Fix the size between last line and cut
582         # TODO: handle this with a line feed
583         self._raw("\n\n\n\n\n\n")
584         if mode.upper() == "PART":
585             self._raw(PAPER_PART_CUT)
586         else: # DEFAULT MODE: FULL CUT
587             self._raw(PAPER_FULL_CUT)
588
589
590     def cashdraw(self, pin):
591         """ Send pulse to kick the cash drawer """
592         if pin == 2:
593             self._raw(CD_KICK_2)
594         elif pin == 5:
595             self._raw(CD_KICK_5)
596         else:
597             raise CashDrawerError()
598
599
600     def hw(self, hw):
601         """ Hardware operations """
602         if hw.upper() == "INIT":
603             self._raw(HW_INIT)
604         elif hw.upper() == "SELECT":
605             self._raw(HW_SELECT)
606         elif hw.upper() == "RESET":
607             self._raw(HW_RESET)
608         else: # DEFAULT: DOES NOTHING
609             pass
610
611
612     def control(self, ctl):
613         """ Feed control sequences """
614         if ctl.upper() == "LF":
615             self._raw(CTL_LF)
616         elif ctl.upper() == "FF":
617             self._raw(CTL_FF)
618         elif ctl.upper() == "CR":
619             self._raw(CTL_CR)
620         elif ctl.upper() == "HT":
621             self._raw(CTL_HT)
622         elif ctl.upper() == "VT":
623             self._raw(CTL_VT)