1 # -*- coding: utf-8 -*-
3 @author: Manuel F Martinez <manpaz@bashlinux.com>
4 @organization: Bashlinux
5 @copyright: Copyright (c) 2012 Bashlinux
19 import xml.etree.ElementTree as ET
20 import xml.dom.minidom as minidom
24 _logger = logging.getLogger(__name__)
30 _logger.warning('ESC/POS: please install jcconv for improved Japanese receipt printing:\n # pip install jcconv')
36 _logger.warning('ESC/POS: please install the qrcode python module for qrcode printing in point of sale receipts:\n # pip install qrcode')
38 from constants import *
39 from exceptions import *
42 """ converts stuff to string and does without failing if stuff is a utf8 string """
43 if isinstance(stuff,basestring):
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.
56 self.defaults = { # default style values
71 'value-symbol-position': 'after',
72 'value-autoint': 'off',
73 'value-decimals-separator': '.',
74 'value-thousands-separator': ',',
79 self.types = { # attribute types, default is string and can be ommitted
83 'line-ratio': 'float',
84 'value-decimals': 'int',
89 # translation from styles to escpos commands
90 # some style do not correspond to escpos command are used by
91 # the serializer instead
94 'right': TXT_ALIGN_RT,
95 'center': TXT_ALIGN_CT,
98 'off': TXT_UNDERL_OFF,
100 'double': TXT_UNDERL2_ON,
111 'normal': TXT_NORMAL,
112 'double-height': TXT_2HEIGHT,
113 'double-width': TXT_2WIDTH,
114 'double': TXT_DOUBLE,
117 'black': TXT_COLOR_BLACK,
118 'red': TXT_COLOR_RED,
122 self.push(self.defaults)
125 """ what's the value of a style at the current stack level"""
126 level = len(self.stack) -1
128 if style in self.stack[level]:
129 return self.stack[level][style]
134 def enforce_type(self, attr, val):
135 """converts a value to the attribute's type"""
136 if not attr in self.types:
138 elif self.types[attr] == 'int':
139 return int(float(val))
140 elif self.types[attr] == 'float':
145 def push(self, style={}):
146 """push a new level on the stack with a style dictionnary containing style:value pairs"""
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)
152 _style[attr] = self.enforce_type(attr, style[attr])
153 self.stack.append(_style)
155 def set(self, style={}):
156 """overrides style values at the current stack level"""
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)
162 self.stack[-1][attr] = self.enforce_type(attr, style[attr])
165 """ pop a style stack level """
166 if len(self.stack) > 1 :
167 self.stack = self.stack[:-1]
170 """ converts the current style to an escpos command string """
172 for style in self.cmds:
173 cmd += self.cmds[style][self.get(style)]
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.
182 def __init__(self,escpos):
184 self.stack = ['block']
187 def start_inline(self,stylestack=None):
188 """ starts an inline entity with an optional style definition """
189 self.stack.append('inline')
191 self.escpos._raw(' ')
193 self.style(stylestack)
195 def start_block(self,stylestack=None):
196 """ starts a block entity with an optional style definition """
198 self.escpos._raw('\n')
200 self.stack.append('block')
202 self.style(stylestack)
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')
209 if len(self.stack) > 1:
210 self.stack = self.stack[:-1]
213 """ puts a string of text in the entity keeping the whitespace intact """
215 self.escpos.text(text)
219 """ puts text in the entity. Whitespace and newlines are stripped to single spaces. """
223 text = re.sub('\s+',' ',text)
226 self.escpos.text(text)
229 """ inserts a linebreak in the entity """
231 self.escpos._raw('\n')
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())
238 """ puts raw text or escpos command in the entity without affecting the state of the serializer """
239 self.escpos._raw(raw)
241 class XmlLineSerializer:
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.
247 def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5):
248 self.tabwidth = tabwidth
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)
261 if self.clwidth < self.lwidth:
262 txt = txt[:max(0, self.lwidth - self.clwidth)]
264 self.clwidth += len(txt)
266 if self.crwidth < self.rwidth:
267 txt = txt[:max(0, self.rwidth - self.crwidth)]
269 self.crwidth += len(txt)
271 def start_inline(self,stylestack=None):
272 if (self.left and self.clwidth) or (not self.left and self.crwidth):
275 def start_block(self,stylestack=None):
276 self.start_inline(stylestack)
278 def end_entity(self):
288 text = re.sub('\s+',' ',text)
294 def style(self,stylestack):
299 def start_right(self):
303 return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer
307 """ ESC/POS Printer object """
312 def _check_image_size(self, size):
313 """ Check and fix the size of the image to 32 bits """
317 image_border = 32 - (size % 32)
318 if (image_border % 2) == 0:
319 return (image_border / 2, image_border / 2)
321 return (image_border / 2, (image_border / 2) + 1)
323 def _print_image(self, line, size):
324 """ Print formatted image """
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'))
336 hex_string = int(line[i:i+8],2)
337 buffer += "%02X" % hex_string
341 self._raw(buffer.decode("hex"))
345 def _raw_print_image(self, line, size, output=None ):
346 """ Print formatted image """
359 buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
360 raw += buffer.decode('hex')
364 hex_string = int(line[i:i+8],2)
365 buffer += "%02X" % hex_string
369 raw += buffer.decode("hex")
375 def _convert_image(self, im):
376 """ Parse image and prepare it to a printable format """
386 print "WARNING: Image is wider than 512 and could be truncated at print time "
388 raise ImageSizeError()
390 im_border = self._check_image_size(im.size[0])
391 for i in range(im_border[0]):
393 for i in range(im_border[1]):
396 for y in range(im.size[1]):
399 img_size[0] += im_border[0]
400 for x in range(im.size[0]):
402 RGB = im.getpixel((x, y))
403 im_color = (RGB[0] + RGB[1] + RGB[2])
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
412 pix_line += im_pattern[x]
414 elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3):
415 pix_line += im_pattern[-1]
418 img_size[0] += im_border[1]
420 return (pix_line, img_size)
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)
430 def print_base64_image(self,img):
432 print 'print_b64_img'
434 id = md5.new(img).digest()
436 if id not in self.img_cache:
439 img = img[img.find(',')+1:]
440 f = io.BytesIO('img')
441 f.write(base64.decodestring(img))
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])
452 print 'convert image'
454 pix_line, img_size = self._convert_image(img)
458 buffer = self._raw_print_image(pix_line, img_size)
459 self.img_cache[id] = buffer
463 self._raw(self.img_cache[id])
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)
475 def barcode(self, code, bc, width=255, height=2, pos='below', font='a'):
476 """ Print Barcode """
478 self._raw(TXT_ALIGN_CT)
480 if height >=2 or height <=6:
481 self._raw(BARCODE_HEIGHT)
483 raise BarcodeSizeError()
485 if width >= 1 or width <=255:
486 self._raw(BARCODE_WIDTH)
488 raise BarcodeSizeError()
490 if font.upper() == "B":
491 self._raw(BARCODE_FONT_B)
492 else: # DEFAULT FONT: A
493 self._raw(BARCODE_FONT_A)
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)
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)
519 raise BarcodeTypeError()
524 raise exception.BarcodeCodeError()
526 def receipt(self,xml):
528 Prints an xml based receipt definition
531 def strclean(string):
534 string = string.strip()
535 string = re.sub('\s+',' ',string)
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))
543 if autoint and math.floor(value) == value:
548 if thousands_separator:
549 formatstr = "{:"+str(width)+",."+str(decimals)+"f}"
551 formatstr = "{:"+str(width)+"."+str(decimals)+"f}"
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)
561 if position == 'after':
567 def print_elem(stylestack, serializer, elem, indent=0):
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'},
580 if elem.tag in elem_styles:
581 stylestack.set(elem_styles[elem.tag])
582 stylestack.set(elem.attrib)
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)
588 print_elem(stylestack,serializer,child)
589 serializer.start_inline(stylestack)
590 serializer.text(child.tail)
591 serializer.end_entity()
592 serializer.end_entity()
594 elif elem.tag in ('span','em','b','left','right'):
595 serializer.start_inline(stylestack)
596 serializer.text(elem.text)
598 print_elem(stylestack,serializer,child)
599 serializer.start_inline(stylestack)
600 serializer.text(child.tail)
601 serializer.end_entity()
602 serializer.end_entity()
604 elif elem.tag == 'value':
605 serializer.start_inline(stylestack)
606 serializer.pre(format_value(
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')
616 serializer.end_entity()
618 elif elem.tag == 'line':
619 width = stylestack.get('width')
620 if stylestack.get('size') in ('double', 'double-width'):
623 lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio'))
624 serializer.start_block(stylestack)
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()
634 elif elem.tag == 'ul':
635 serializer.start_block(stylestack)
636 bullet = stylestack.get('bullet')
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()
644 elif elem.tag == 'ol':
645 cwidth = len(str(len(elem))) + 2
647 serializer.start_block(stylestack)
649 if child.tag == 'li':
650 serializer.style(stylestack)
651 serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth))
653 print_elem(stylestack,serializer,child,indent=indent+1)
654 serializer.end_entity()
656 elif elem.tag == 'pre':
657 serializer.start_block(stylestack)
658 serializer.pre(elem.text)
659 serializer.end_entity()
661 elif elem.tag == 'hr':
662 width = stylestack.get('width')
663 if stylestack.get('size') in ('double', 'double-width'):
665 serializer.start_block(stylestack)
666 serializer.text('-'*width)
667 serializer.end_entity()
669 elif elem.tag == 'br':
670 serializer.linebreak()
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'])
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()
681 elif elem.tag == 'cut':
683 elif elem.tag == 'partialcut':
684 self.cut(mode='part')
685 elif elem.tag == 'cashdraw':
692 stylestack = StyleStack()
693 serializer = XmlSerializer(self)
694 root = ET.fromstring(xml.encode('utf-8'))
696 self._raw(stylestack.to_escpos())
698 print_elem(stylestack,serializer,root)
700 if 'open-cashdrawer' in root.attrib and root.attrib['open-cashdrawer'] == 'true':
703 if not 'cut' in root.attrib or root.attrib['cut'] == 'true' :
706 except Exception as e:
707 errmsg = str(e)+'\n'+'-'*48+'\n'+traceback.format_exc() + '-'*48+'\n'
714 """ Print Utf8 encoded alpha-numeric text """
718 txt = txt.decode('utf-8')
721 txt = txt.decode('utf-16')
727 def encode_char(char):
729 Encodes a single utf-8 character into a sequence of
730 esc-pos code page change instructions and character declarations
732 char_utf8 = char.encode('utf-8')
734 encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character
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,
759 remaining = copy.copy(encodings)
764 while True: # Trying all encoding until one succeeds
766 if encoding == 'katakana': # Japanese characters
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'))
777 if kata in TXT_ENC_KATAKANA_MAP:
778 encoded = TXT_ENC_KATAKANA_MAP[kata]
783 encoded = char.encode(encoding)
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]
793 encoded = '\xb1' # could not encode, output error character
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
807 buffer += encode_char(c)
810 txt = encode_str(txt)
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(' ')
818 txt = txt[:dspace] + txt[dspace+1:]
819 self.extra_chars -= 1
825 def set(self, align='left', font='a', type='normal', width=1, height=1):
826 """ Set text properties """
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)
835 if font.upper() == "B":
836 self._raw(TXT_FONT_B)
837 else: # DEFAULT FONT: A
838 self._raw(TXT_FONT_A)
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)
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)
872 def cut(self, mode=''):
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)
883 def cashdraw(self, pin):
884 """ Send pulse to kick the cash drawer """
890 raise CashDrawerError()
894 """ Hardware operations """
895 if hw.upper() == "INIT":
897 elif hw.upper() == "SELECT":
899 elif hw.upper() == "RESET":
901 else: # DEFAULT: DOES NOTHING
905 def control(self, ctl):
906 """ Feed control sequences """
907 if ctl.upper() == "LF":
909 elif ctl.upper() == "FF":
911 elif ctl.upper() == "CR":
913 elif ctl.upper() == "HT":
915 elif ctl.upper() == "VT":