[FIX] hw_escops, point_of_sale: backporting fixes from the pos-restaurant branch
[odoo/odoo.git] / addons / hw_escpos / escpos / escpos.py
1 # -*- coding: utf-8 -*-
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 traceback
22 import xml.etree.ElementTree as ET
23 import xml.dom.minidom as minidom
24
25 from PIL import Image
26
27 try:
28     import jcconv
29 except ImportError:
30     jcconv = None
31     print 'ESC/POS: please install jcconv for improved Japanese receipt printing:'
32     print ' # pip install jcconv'
33
34 from constants import *
35 from exceptions import *
36
37 def utfstr(stuff):
38     """ converts stuff to string and does without failing if stuff is a utf8 string """
39     if isinstance(stuff,basestring):
40         return stuff
41     else:
42         return str(stuff)
43
44 class StyleStack:
45     """ 
46     The stylestack is used by the xml receipt serializer to compute the active styles along the xml
47     document. Styles are just xml attributes, there is no css mechanism. But the style applied by
48     the attributes are inherited by deeper nodes.
49     """
50     def __init__(self):
51         self.stack = []
52         self.defaults = {   # default style values
53             'align':     'left',
54             'underline': 'off',
55             'bold':      'off',
56             'size':      'normal',
57             'font'  :    'a',
58             'width':     48,
59             'indent':    0,
60             'tabwidth':  2,
61             'bullet':    ' - ',
62             'line-ratio':0.5,
63             'color':    'black',
64
65             'value-decimals':           2,
66             'value-symbol':             '',
67             'value-symbol-position':    'after',
68             'value-autoint':            'off',
69             'value-decimals-separator':  '.',
70             'value-thousands-separator': ',',
71             'value-width':               0,
72             
73         }
74
75         self.types = { # attribute types, default is string and can be ommitted
76             'width':    'int',
77             'indent':   'int',
78             'tabwidth': 'int',
79             'line-ratio':       'float',
80             'value-decimals':   'int',
81             'value-width':      'int',
82         }
83
84         self.cmds = { 
85             # translation from styles to escpos commands
86             # some style do not correspond to escpos command are used by
87             # the serializer instead
88             'align': {
89                 'left':     TXT_ALIGN_LT,
90                 'right':    TXT_ALIGN_RT,
91                 'center':   TXT_ALIGN_CT,
92             },
93             'underline': {
94                 'off':      TXT_UNDERL_OFF,
95                 'on':       TXT_UNDERL_ON,
96                 'double':   TXT_UNDERL2_ON,
97             },
98             'bold': {
99                 'off':      TXT_BOLD_OFF,
100                 'on':       TXT_BOLD_ON,
101             },
102             'font': {
103                 'a':        TXT_FONT_A,
104                 'b':        TXT_FONT_B,
105             },
106             'size': {
107                 'normal':           TXT_NORMAL,
108                 'double-height':    TXT_2HEIGHT,
109                 'double-width':     TXT_2WIDTH,
110                 'double':           TXT_DOUBLE,
111             },
112             'color': {
113                 'black':    TXT_COLOR_BLACK,
114                 'red':      TXT_COLOR_RED,
115             },
116         }
117
118         self.push(self.defaults) 
119
120     def get(self,style):
121         """ what's the value of a style at the current stack level"""
122         level = len(self.stack) -1
123         while level >= 0:
124             if style in self.stack[level]:
125                 return self.stack[level][style]
126             else:
127                 level = level - 1
128         return None
129
130     def enforce_type(self, attr, val):
131         """converts a value to the attribute's type"""
132         if not attr in self.types:
133             return utfstr(val)
134         elif self.types[attr] == 'int':
135             return int(float(val))
136         elif self.types[attr] == 'float':
137             return float(val)
138         else:
139             return utfstr(val)
140
141     def push(self, style={}):
142         """push a new level on the stack with a style dictionnary containing style:value pairs"""
143         _style = {}
144         for attr in style:
145             if attr in self.cmds and not style[attr] in self.cmds[attr]:
146                 print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr)
147             else:
148                 _style[attr] = self.enforce_type(attr, style[attr])
149         self.stack.append(_style)
150
151     def set(self, style={}):
152         """overrides style values at the current stack level"""
153         _style = {}
154         for attr in style:
155             if attr in self.cmds and not style[attr] in self.cmds[attr]:
156                 print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr)
157             else:
158                 self.stack[-1][attr] = self.enforce_type(attr, style[attr])
159
160     def pop(self):
161         """ pop a style stack level """
162         if len(self.stack) > 1 :
163             self.stack = self.stack[:-1]
164
165     def to_escpos(self):
166         """ converts the current style to an escpos command string """
167         cmd = ''
168         for style in self.cmds:
169             cmd += self.cmds[style][self.get(style)]
170         return cmd
171
172 class XmlSerializer:
173     """ 
174     Converts the xml inline / block tree structure to a string,
175     keeping track of newlines and spacings.
176     The string is outputted asap to the provided escpos driver.
177     """
178     def __init__(self,escpos):
179         self.escpos = escpos
180         self.stack = ['block']
181         self.dirty = False
182
183     def start_inline(self,stylestack=None):
184         """ starts an inline entity with an optional style definition """
185         self.stack.append('inline')
186         if self.dirty:
187             self.escpos._raw(' ')
188         if stylestack:
189             self.style(stylestack)
190
191     def start_block(self,stylestack=None):
192         """ starts a block entity with an optional style definition """
193         if self.dirty:
194             self.escpos._raw('\n')
195             self.dirty = False
196         self.stack.append('block')
197         if stylestack:
198             self.style(stylestack)
199
200     def end_entity(self):
201         """ ends the entity definition. (but does not cancel the active style!) """
202         if self.stack[-1] == 'block' and self.dirty:
203             self.escpos._raw('\n')
204             self.dirty = False
205         if len(self.stack) > 1:
206             self.stack = self.stack[:-1]
207
208     def pre(self,text):
209         """ puts a string of text in the entity keeping the whitespace intact """
210         if text:
211             self.escpos.text(text)
212             self.dirty = True
213
214     def text(self,text):
215         """ puts text in the entity. Whitespace and newlines are stripped to single spaces. """
216         if text:
217             text = utfstr(text)
218             text = text.strip()
219             text = re.sub('\s+',' ',text)
220             if text:
221                 self.dirty = True
222                 self.escpos.text(text)
223
224     def linebreak(self):
225         """ inserts a linebreak in the entity """
226         self.dirty = False
227         self.escpos._raw('\n')
228
229     def style(self,stylestack):
230         """ apply a style to the entity (only applies to content added after the definition) """
231         self.raw(stylestack.to_escpos())
232
233     def raw(self,raw):
234         """ puts raw text or escpos command in the entity without affecting the state of the serializer """
235         self.escpos._raw(raw)
236
237 class XmlLineSerializer:
238     """ 
239     This is used to convert a xml tree into a single line, with a left and a right part.
240     The content is not output to escpos directly, and is intended to be fedback to the
241     XmlSerializer as the content of a block entity.
242     """
243     def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5):
244         self.tabwidth = tabwidth
245         self.indent = indent
246         self.width  = max(0, width - int(tabwidth*indent))
247         self.lwidth = int(self.width*ratio)
248         self.rwidth = max(0, self.width - self.lwidth)
249         self.clwidth = 0
250         self.crwidth = 0
251         self.lbuffer  = ''
252         self.rbuffer  = ''
253         self.left    = True
254
255     def _txt(self,txt):
256         if self.left:
257             if self.clwidth < self.lwidth:
258                 txt = txt[:max(0, self.lwidth - self.clwidth)]
259                 self.lbuffer += txt
260                 self.clwidth += len(txt)
261         else:
262             if self.crwidth < self.rwidth:
263                 txt = txt[:max(0, self.rwidth - self.crwidth)]
264                 self.rbuffer += txt
265                 self.crwidth  += len(txt)
266
267     def start_inline(self,stylestack=None):
268         if (self.left and self.clwidth) or (not self.left and self.crwidth):
269             self._txt(' ')
270
271     def start_block(self,stylestack=None):
272         self.start_inline(stylestack)
273
274     def end_entity(self):
275         pass
276
277     def pre(self,text):
278         if text:
279             self._txt(text)
280     def text(self,text):
281         if text:
282             text = utfstr(text)
283             text = text.strip()
284             text = re.sub('\s+',' ',text)
285             if text:
286                 self._txt(text)
287
288     def linebreak(self):
289         pass
290     def style(self,stylestack):
291         pass
292     def raw(self,raw):
293         pass
294
295     def start_right(self):
296         self.left = False
297
298     def get_line(self):
299         return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer
300     
301
302 class Escpos:
303     """ ESC/POS Printer object """
304     device    = None
305     encoding  = None
306     img_cache = {}
307
308     def _check_image_size(self, size):
309         """ Check and fix the size of the image to 32 bits """
310         if size % 32 == 0:
311             return (0, 0)
312         else:
313             image_border = 32 - (size % 32)
314             if (image_border % 2) == 0:
315                 return (image_border / 2, image_border / 2)
316             else:
317                 return (image_border / 2, (image_border / 2) + 1)
318
319     def _print_image(self, line, size):
320         """ Print formatted image """
321         i = 0
322         cont = 0
323         buffer = ""
324
325        
326         self._raw(S_RASTER_N)
327         buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
328         self._raw(buffer.decode('hex'))
329         buffer = ""
330
331         while i < len(line):
332             hex_string = int(line[i:i+8],2)
333             buffer += "%02X" % hex_string
334             i += 8
335             cont += 1
336             if cont % 4 == 0:
337                 self._raw(buffer.decode("hex"))
338                 buffer = ""
339                 cont = 0
340
341     def _raw_print_image(self, line, size, output=None ):
342         """ Print formatted image """
343         i = 0
344         cont = 0
345         buffer = ""
346         raw = ""
347
348         def __raw(string):
349             if output:
350                 output(string)
351             else:
352                 self._raw(string)
353        
354         raw += S_RASTER_N
355         buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
356         raw += buffer.decode('hex')
357         buffer = ""
358
359         while i < len(line):
360             hex_string = int(line[i:i+8],2)
361             buffer += "%02X" % hex_string
362             i += 8
363             cont += 1
364             if cont % 4 == 0:
365                 raw += buffer.decode("hex")
366                 buffer = ""
367                 cont = 0
368
369         return raw
370
371     def _convert_image(self, im):
372         """ Parse image and prepare it to a printable format """
373         pixels   = []
374         pix_line = ""
375         im_left  = ""
376         im_right = ""
377         switch   = 0
378         img_size = [ 0, 0 ]
379
380
381         if im.size[0] > 512:
382             print  "WARNING: Image is wider than 512 and could be truncated at print time "
383         if im.size[1] > 255:
384             raise ImageSizeError()
385
386         im_border = self._check_image_size(im.size[0])
387         for i in range(im_border[0]):
388             im_left += "0"
389         for i in range(im_border[1]):
390             im_right += "0"
391
392         for y in range(im.size[1]):
393             img_size[1] += 1
394             pix_line += im_left
395             img_size[0] += im_border[0]
396             for x in range(im.size[0]):
397                 img_size[0] += 1
398                 RGB = im.getpixel((x, y))
399                 im_color = (RGB[0] + RGB[1] + RGB[2])
400                 im_pattern = "1X0"
401                 pattern_len = len(im_pattern)
402                 switch = (switch - 1 ) * (-1)
403                 for x in range(pattern_len):
404                     if im_color <= (255 * 3 / pattern_len * (x+1)):
405                         if im_pattern[x] == "X":
406                             pix_line += "%d" % switch
407                         else:
408                             pix_line += im_pattern[x]
409                         break
410                     elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3):
411                         pix_line += im_pattern[-1]
412                         break 
413             pix_line += im_right
414             img_size[0] += im_border[1]
415
416         return (pix_line, img_size)
417
418     def image(self,path_img):
419         """ Open image file """
420         im_open = Image.open(path_img)
421         im = im_open.convert("RGB")
422         # Convert the RGB image in printable image
423         pix_line, img_size = self._convert_image(im)
424         self._print_image(pix_line, img_size)
425
426     def print_base64_image(self,img):
427
428         print 'print_b64_img'
429
430         id = md5.new(img).digest()
431
432         if id not in self.img_cache:
433             print 'not in cache'
434
435             img = img[img.find(',')+1:]
436             f = io.BytesIO('img')
437             f.write(base64.decodestring(img))
438             f.seek(0)
439             img_rgba = Image.open(f)
440             img = Image.new('RGB', img_rgba.size, (255,255,255))
441             img.paste(img_rgba, mask=img_rgba.split()[3]) 
442
443             print 'convert image'
444         
445             pix_line, img_size = self._convert_image(img)
446
447             print 'print image'
448
449             buffer = self._raw_print_image(pix_line, img_size)
450             self.img_cache[id] = buffer
451
452         print 'raw image'
453
454         self._raw(self.img_cache[id])
455
456     def qr(self,text):
457         """ Print QR Code for the provided string """
458         qr_code = qrcode.QRCode(version=4, box_size=4, border=1)
459         qr_code.add_data(text)
460         qr_code.make(fit=True)
461         qr_img = qr_code.make_image()
462         im = qr_img._img.convert("RGB")
463         # Convert the RGB image in printable image
464         self._convert_image(im)
465
466     def barcode(self, code, bc, width=255, height=2, pos='below', font='a'):
467         """ Print Barcode """
468         # Align Bar Code()
469         self._raw(TXT_ALIGN_CT)
470         # Height
471         if height >=2 or height <=6:
472             self._raw(BARCODE_HEIGHT)
473         else:
474             raise BarcodeSizeError()
475         # Width
476         if width >= 1 or width <=255:
477             self._raw(BARCODE_WIDTH)
478         else:
479             raise BarcodeSizeError()
480         # Font
481         if font.upper() == "B":
482             self._raw(BARCODE_FONT_B)
483         else: # DEFAULT FONT: A
484             self._raw(BARCODE_FONT_A)
485         # Position
486         if pos.upper() == "OFF":
487             self._raw(BARCODE_TXT_OFF)
488         elif pos.upper() == "BOTH":
489             self._raw(BARCODE_TXT_BTH)
490         elif pos.upper() == "ABOVE":
491             self._raw(BARCODE_TXT_ABV)
492         else:  # DEFAULT POSITION: BELOW 
493             self._raw(BARCODE_TXT_BLW)
494         # Type 
495         if bc.upper() == "UPC-A":
496             self._raw(BARCODE_UPC_A)
497         elif bc.upper() == "UPC-E":
498             self._raw(BARCODE_UPC_E)
499         elif bc.upper() == "EAN13":
500             self._raw(BARCODE_EAN13)
501         elif bc.upper() == "EAN8":
502             self._raw(BARCODE_EAN8)
503         elif bc.upper() == "CODE39":
504             self._raw(BARCODE_CODE39)
505         elif bc.upper() == "ITF":
506             self._raw(BARCODE_ITF)
507         elif bc.upper() == "NW7":
508             self._raw(BARCODE_NW7)
509         else:
510             raise BarcodeTypeError()
511         # Print Code
512         if code:
513             self._raw(code)
514         else:
515             raise exception.BarcodeCodeError()
516
517     def receipt(self,xml):
518         """
519         Prints an xml based receipt definition
520         """
521
522         def strclean(string):
523             if not string:
524                 string = ''
525             string = string.strip()
526             string = re.sub('\s+',' ',string)
527             return string
528
529         def format_value(value, decimals=3, width=0, decimals_separator='.', thousands_separator=',', autoint=False, symbol='', position='after'):
530             decimals = max(0,int(decimals))
531             width    = max(0,int(width))
532             value    = float(value)
533
534             if autoint and math.floor(value) == value:
535                 decimals = 0
536             if width == 0:
537                 width = ''
538
539             if thousands_separator:
540                 formatstr = "{:"+str(width)+",."+str(decimals)+"f}"
541             else:
542                 formatstr = "{:"+str(width)+"."+str(decimals)+"f}"
543
544
545             ret = formatstr.format(value)
546             ret = ret.replace(',','COMMA')
547             ret = ret.replace('.','DOT')
548             ret = ret.replace('COMMA',thousands_separator)
549             ret = ret.replace('DOT',decimals_separator)
550
551             if symbol:
552                 if position == 'after':
553                     ret = ret + symbol
554                 else:
555                     ret = symbol + ret
556             return ret
557
558         def print_elem(stylestack, serializer, elem, indent=0):
559
560             elem_styles = {
561                 'h1': {'bold': 'on', 'size':'double'},
562                 'h2': {'size':'double'},
563                 'h3': {'bold': 'on', 'size':'double-height'},
564                 'h4': {'size': 'double-height'},
565                 'h5': {'bold': 'on'},
566                 'em': {'font': 'b'},
567                 'b':  {'bold': 'on'},
568             }
569
570             stylestack.push()
571             if elem.tag in elem_styles:
572                 stylestack.set(elem_styles[elem.tag])
573             stylestack.set(elem.attrib)
574
575             if elem.tag in ('p','div','section','article','receipt','header','footer','li','h1','h2','h3','h4','h5'):
576                 serializer.start_block(stylestack)
577                 serializer.text(elem.text)
578                 for child in elem:
579                     print_elem(stylestack,serializer,child)
580                     serializer.start_inline(stylestack)
581                     serializer.text(child.tail)
582                     serializer.end_entity()
583                 serializer.end_entity()
584
585             elif elem.tag in ('span','em','b','left','right'):
586                 serializer.start_inline(stylestack)
587                 serializer.text(elem.text)
588                 for child in elem:
589                     print_elem(stylestack,serializer,child)
590                     serializer.start_inline(stylestack)
591                     serializer.text(child.tail)
592                     serializer.end_entity()
593                 serializer.end_entity()
594
595             elif elem.tag == 'value':
596                 serializer.start_inline(stylestack)
597                 serializer.pre(format_value( 
598                                               elem.text,
599                                               decimals=stylestack.get('value-decimals'),
600                                               width=stylestack.get('value-width'),
601                                               decimals_separator=stylestack.get('value-decimals-separator'),
602                                               thousands_separator=stylestack.get('value-thousands-separator'),
603                                               autoint=(stylestack.get('value-autoint') == 'on'),
604                                               symbol=stylestack.get('value-symbol'),
605                                               position=stylestack.get('value-symbol-position') 
606                                             ))
607                 serializer.end_entity()
608
609             elif elem.tag == 'line':
610                 width = stylestack.get('width')
611                 if stylestack.get('size') in ('double', 'double-width'):
612                     width = width / 2
613
614                 lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio'))
615                 serializer.start_block(stylestack)
616                 for child in elem:
617                     if child.tag == 'left':
618                         print_elem(stylestack,lineserializer,child,indent=indent)
619                     elif child.tag == 'right':
620                         lineserializer.start_right()
621                         print_elem(stylestack,lineserializer,child,indent=indent)
622                 serializer.pre(lineserializer.get_line())
623                 serializer.end_entity()
624
625             elif elem.tag == 'ul':
626                 serializer.start_block(stylestack)
627                 bullet = stylestack.get('bullet')
628                 for child in elem:
629                     if child.tag == 'li':
630                         serializer.style(stylestack)
631                         serializer.raw(' ' * indent * stylestack.get('tabwidth') + bullet)
632                     print_elem(stylestack,serializer,child,indent=indent+1)
633                 serializer.end_entity()
634
635             elif elem.tag == 'ol':
636                 cwidth = len(str(len(elem))) + 2
637                 i = 1
638                 serializer.start_block(stylestack)
639                 for child in elem:
640                     if child.tag == 'li':
641                         serializer.style(stylestack)
642                         serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth))
643                         i = i + 1
644                     print_elem(stylestack,serializer,child,indent=indent+1)
645                 serializer.end_entity()
646
647             elif elem.tag == 'pre':
648                 serializer.start_block(stylestack)
649                 serializer.pre(elem.text)
650                 serializer.end_entity()
651
652             elif elem.tag == 'hr':
653                 width = stylestack.get('width')
654                 if stylestack.get('size') in ('double', 'double-width'):
655                     width = width / 2
656                 serializer.start_block(stylestack)
657                 serializer.text('-'*width)
658                 serializer.end_entity()
659
660             elif elem.tag == 'br':
661                 serializer.linebreak()
662
663             elif elem.tag == 'img':
664                 if 'src' in elem.attrib and 'data:' in elem.attrib['src']:
665                     self.print_base64_image(elem.attrib['src'])
666
667             elif elem.tag == 'barcode' and 'encoding' in elem.attrib:
668                 serializer.start_block(stylestack)
669                 self.barcode(strclean(elem.text),elem.attrib['encoding'])
670                 serializer.end_entity()
671
672             elif elem.tag == 'cut':
673                 self.cut()
674             elif elem.tag == 'partialcut':
675                 self.cut(mode='part')
676             elif elem.tag == 'cashdraw':
677                 self.cashdraw(2)
678                 self.cashdraw(5)
679
680             stylestack.pop()
681
682         try:
683             stylestack      = StyleStack() 
684             serializer      = XmlSerializer(self)
685             root            = ET.fromstring(xml)
686
687             self._raw(stylestack.to_escpos())
688
689             print_elem(stylestack,serializer,root)
690
691             if 'open-cashdrawer' in root.attrib and root.attrib['open-cashdrawer'] == 'true':
692                 self.cashdraw(2)
693                 self.cashdraw(5)
694             if not 'cut' in root.attrib or root.attrib['cut'] == 'true' :
695                 self.cut()
696
697         except Exception as e:
698             errmsg = str(e)+'\n'+'-'*48+'\n'+traceback.format_exc() + '-'*48+'\n'
699             self.text(errmsg)
700             self.cut()
701
702             raise e
703
704     def text(self,txt):
705         """ Print Utf8 encoded alpha-numeric text """
706         if not txt:
707             return
708         try:
709             txt = txt.decode('utf-8')
710         except:
711             try:
712                 txt = txt.decode('utf-16')
713             except:
714                 pass
715
716         self.extra_chars = 0
717         
718         def encode_char(char):  
719             """ 
720             Encodes a single utf-8 character into a sequence of 
721             esc-pos code page change instructions and character declarations 
722             """ 
723             char_utf8 = char.encode('utf-8')
724             encoded  = ''
725             encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character
726             encodings = {
727                     # TODO use ordering to prevent useless switches
728                     # TODO Support other encodings not natively supported by python ( Thai, Khazakh, Kanjis )
729                     'cp437': TXT_ENC_PC437,
730                     'cp850': TXT_ENC_PC850,
731                     'cp852': TXT_ENC_PC852,
732                     'cp857': TXT_ENC_PC857,
733                     'cp858': TXT_ENC_PC858,
734                     'cp860': TXT_ENC_PC860,
735                     'cp863': TXT_ENC_PC863,
736                     'cp865': TXT_ENC_PC865,
737                     'cp866': TXT_ENC_PC866,
738                     'cp862': TXT_ENC_PC862,
739                     'cp720': TXT_ENC_PC720,
740                     'iso8859_2': TXT_ENC_8859_2,
741                     'iso8859_7': TXT_ENC_8859_7,
742                     'iso8859_9': TXT_ENC_8859_9,
743                     'cp1254'   : TXT_ENC_WPC1254,
744                     'cp1255'   : TXT_ENC_WPC1255,
745                     'cp1256'   : TXT_ENC_WPC1256,
746                     'cp1257'   : TXT_ENC_WPC1257,
747                     'cp1258'   : TXT_ENC_WPC1258,
748                     'katakana' : TXT_ENC_KATAKANA,
749             }
750             remaining = copy.copy(encodings)
751
752             if not encoding :
753                 encoding = 'cp437'
754
755             while True: # Trying all encoding until one succeeds
756                 try:
757                     if encoding == 'katakana': # Japanese characters
758                         if jcconv:
759                             # try to convert japanese text to a half-katakanas 
760                             kata = jcconv.kata2half(jcconv.hira2kata(char_utf8))
761                             if kata != char_utf8:
762                                 self.extra_chars += len(kata.decode('utf-8')) - 1
763                                 # the conversion may result in multiple characters
764                                 return encode_str(kata.decode('utf-8')) 
765                         else:
766                              kata = char_utf8
767                         
768                         if kata in TXT_ENC_KATAKANA_MAP:
769                             encoded = TXT_ENC_KATAKANA_MAP[kata]
770                             break
771                         else: 
772                             raise ValueError()
773                     else:
774                         encoded = char.encode(encoding)
775                         break
776
777                 except ValueError: #the encoding failed, select another one and retry
778                     if encoding in remaining:
779                         del remaining[encoding]
780                     if len(remaining) >= 1:
781                         encoding = remaining.items()[0][0]
782                     else:
783                         encoding = 'cp437'
784                         encoded  = '\xb1'    # could not encode, output error character
785                         break;
786
787             if encoding != self.encoding:
788                 # if the encoding changed, remember it and prefix the character with
789                 # the esc-pos encoding change sequence
790                 self.encoding = encoding
791                 encoded = encodings[encoding] + encoded
792
793             return encoded
794         
795         def encode_str(txt):
796             buffer = ''
797             for c in txt:
798                 buffer += encode_char(c)
799             return buffer
800
801         txt = encode_str(txt)
802
803         # if the utf-8 -> codepage conversion inserted extra characters, 
804         # remove double spaces to try to restore the original string length
805         # and prevent printing alignment issues
806         while self.extra_chars > 0: 
807             dspace = txt.find('  ')
808             if dspace > 0:
809                 txt = txt[:dspace] + txt[dspace+1:]
810                 self.extra_chars -= 1
811             else:
812                 break
813
814         self._raw(txt)
815         
816     def set(self, align='left', font='a', type='normal', width=1, height=1):
817         """ Set text properties """
818         # Align
819         if align.upper() == "CENTER":
820             self._raw(TXT_ALIGN_CT)
821         elif align.upper() == "RIGHT":
822             self._raw(TXT_ALIGN_RT)
823         elif align.upper() == "LEFT":
824             self._raw(TXT_ALIGN_LT)
825         # Font
826         if font.upper() == "B":
827             self._raw(TXT_FONT_B)
828         else:  # DEFAULT FONT: A
829             self._raw(TXT_FONT_A)
830         # Type
831         if type.upper() == "B":
832             self._raw(TXT_BOLD_ON)
833             self._raw(TXT_UNDERL_OFF)
834         elif type.upper() == "U":
835             self._raw(TXT_BOLD_OFF)
836             self._raw(TXT_UNDERL_ON)
837         elif type.upper() == "U2":
838             self._raw(TXT_BOLD_OFF)
839             self._raw(TXT_UNDERL2_ON)
840         elif type.upper() == "BU":
841             self._raw(TXT_BOLD_ON)
842             self._raw(TXT_UNDERL_ON)
843         elif type.upper() == "BU2":
844             self._raw(TXT_BOLD_ON)
845             self._raw(TXT_UNDERL2_ON)
846         elif type.upper == "NORMAL":
847             self._raw(TXT_BOLD_OFF)
848             self._raw(TXT_UNDERL_OFF)
849         # Width
850         if width == 2 and height != 2:
851             self._raw(TXT_NORMAL)
852             self._raw(TXT_2WIDTH)
853         elif height == 2 and width != 2:
854             self._raw(TXT_NORMAL)
855             self._raw(TXT_2HEIGHT)
856         elif height == 2 and width == 2:
857             self._raw(TXT_2WIDTH)
858             self._raw(TXT_2HEIGHT)
859         else: # DEFAULT SIZE: NORMAL
860             self._raw(TXT_NORMAL)
861
862
863     def cut(self, mode=''):
864         """ Cut paper """
865         # Fix the size between last line and cut
866         # TODO: handle this with a line feed
867         self._raw("\n\n\n\n\n\n")
868         if mode.upper() == "PART":
869             self._raw(PAPER_PART_CUT)
870         else: # DEFAULT MODE: FULL CUT
871             self._raw(PAPER_FULL_CUT)
872
873
874     def cashdraw(self, pin):
875         """ Send pulse to kick the cash drawer """
876         if pin == 2:
877             self._raw(CD_KICK_2)
878         elif pin == 5:
879             self._raw(CD_KICK_5)
880         else:
881             raise CashDrawerError()
882
883
884     def hw(self, hw):
885         """ Hardware operations """
886         if hw.upper() == "INIT":
887             self._raw(HW_INIT)
888         elif hw.upper() == "SELECT":
889             self._raw(HW_SELECT)
890         elif hw.upper() == "RESET":
891             self._raw(HW_RESET)
892         else: # DEFAULT: DOES NOTHING
893             pass
894
895
896     def control(self, ctl):
897         """ Feed control sequences """
898         if ctl.upper() == "LF":
899             self._raw(CTL_LF)
900         elif ctl.upper() == "FF":
901             self._raw(CTL_FF)
902         elif ctl.upper() == "CR":
903             self._raw(CTL_CR)
904         elif ctl.upper() == "HT":
905             self._raw(CTL_HT)
906         elif ctl.upper() == "VT":
907             self._raw(CTL_VT)