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