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