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