1 # -*- coding: utf-8 -*-
3 @author: Manuel F Martinez <manpaz@bashlinux.com>
4 @organization: Bashlinux
5 @copyright: Copyright (c) 2012 Bashlinux
22 import xml.etree.ElementTree as ET
23 import xml.dom.minidom as minidom
31 print 'ESC/POS: please install jcconv for improved Japanese receipt printing:'
32 print ' # pip install jcconv'
34 from constants import *
35 from exceptions import *
38 """ converts stuff to string and does without failing if stuff is a utf8 string """
39 if isinstance(stuff,basestring):
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.
52 self.defaults = { # default style values
66 'value-symbol-position': 'after',
67 'value-autoint': 'off',
68 'value-decimals-separator': '.',
69 'value-thousands-separator': ',',
74 self.types = { # attribute types, default is string and can be ommitted
78 'line-ratio': 'float',
79 'value-decimals': 'int',
84 # translation from styles to escpos commands
85 # some style do not correspond to escpos command are used by
86 # the serializer instead
89 'right': TXT_ALIGN_RT,
90 'center': TXT_ALIGN_CT,
93 'off': TXT_UNDERL_OFF,
95 'double': TXT_UNDERL2_ON,
106 'normal': TXT_NORMAL,
107 'double-height': TXT_2HEIGHT,
108 'double-width': TXT_2WIDTH,
109 'double': TXT_DOUBLE,
113 self.push(self.defaults)
116 """ what's the value of a style at the current stack level"""
117 level = len(self.stack) -1
119 if style in self.stack[level]:
120 return self.stack[level][style]
125 def enforce_type(self, attr, val):
126 """converts a value to the attribute's type"""
127 if not attr in self.types:
129 elif self.types[attr] == 'int':
130 return int(float(val))
131 elif self.types[attr] == 'float':
136 def push(self, style={}):
137 """push a new level on the stack with a style dictionnary containing style:value pairs"""
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)
143 _style[attr] = self.enforce_type(attr, style[attr])
144 self.stack.append(_style)
146 def set(self, style={}):
147 """overrides style values at the current stack level"""
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)
153 self.stack[-1][attr] = self.enforce_type(attr, style[attr])
156 """ pop a style stack level """
157 if len(self.stack) > 1 :
158 self.stack = self.stack[:-1]
161 """ converts the current style to an escpos command string """
163 for style in self.cmds:
164 cmd += self.cmds[style][self.get(style)]
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.
173 def __init__(self,escpos):
175 self.stack = ['block']
178 def start_inline(self,stylestack=None):
179 """ starts an inline entity with an optional style definition """
181 self.stack.append('inline')
183 self.escpos._raw(' ')
185 self.style(stylestack)
187 def start_block(self,stylestack=None):
188 """ starts a block entity with an optional style definition """
191 print 'cleanup before block'
192 self.escpos._raw('\n')
194 self.stack.append('block')
196 self.style(stylestack)
198 def end_entity(self):
199 """ ends the entity definition. (but does not cancel the active style!) """
201 if self.stack[-1] == 'block' and self.dirty:
202 print 'cleanup after block'
203 self.escpos._raw('\n')
205 if len(self.stack) > 1:
206 self.stack = self.stack[:-1]
209 """ puts a string of text in the entity keeping the whitespace intact """
211 self.escpos.text(text)
215 """ puts text in the entity. Whitespace and newlines are stripped to single spaces. """
219 text = re.sub('\s+',' ',text)
221 print 'printing text:'+text
223 self.escpos.text(text)
226 """ inserts a linebreak in the entity """
228 self.escpos._raw('\n')
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())
235 """ puts raw text or escpos command in the entity without affecting the state of the serializer """
236 self.escpos._raw(raw)
238 class XmlLineSerializer:
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.
244 def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5):
245 self.tabwidth = tabwidth
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)
259 if self.clwidth < self.lwidth:
260 txt = txt[:max(0, self.lwidth - self.clwidth)]
262 self.clwidth += len(txt)
264 if self.crwidth < self.rwidth:
265 txt = txt[:max(0, self.rwidth - self.crwidth)]
267 self.crwidth += len(txt)
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):
274 def start_block(self,stylestack=None):
275 self.start_inline(stylestack)
277 def end_entity(self):
287 text = re.sub('\s+',' ',text)
289 print 'LINE:printing text:'+text
294 def style(self,stylestack):
299 def start_right(self):
303 print 'LBUFFER: '+self.lbuffer
305 print 'RBUFFER: '+self.rbuffer
308 return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer
312 """ ESC/POS Printer object """
317 def _check_image_size(self, size):
318 """ Check and fix the size of the image to 32 bits """
322 image_border = 32 - (size % 32)
323 if (image_border % 2) == 0:
324 return (image_border / 2, image_border / 2)
326 return (image_border / 2, (image_border / 2) + 1)
328 def _print_image(self, line, size):
329 """ Print formatted image """
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'))
341 hex_string = int(line[i:i+8],2)
342 buffer += "%02X" % hex_string
346 self._raw(buffer.decode("hex"))
350 def _raw_print_image(self, line, size, output=None ):
351 """ Print formatted image """
364 buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
365 raw += buffer.decode('hex')
369 hex_string = int(line[i:i+8],2)
370 buffer += "%02X" % hex_string
374 raw += buffer.decode("hex")
380 def _convert_image(self, im):
381 """ Parse image and prepare it to a printable format """
391 print "WARNING: Image is wider than 512 and could be truncated at print time "
393 raise ImageSizeError()
395 im_border = self._check_image_size(im.size[0])
396 for i in range(im_border[0]):
398 for i in range(im_border[1]):
401 for y in range(im.size[1]):
404 img_size[0] += im_border[0]
405 for x in range(im.size[0]):
407 RGB = im.getpixel((x, y))
408 im_color = (RGB[0] + RGB[1] + RGB[2])
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
417 pix_line += im_pattern[x]
419 elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3):
420 pix_line += im_pattern[-1]
423 img_size[0] += im_border[1]
425 return (pix_line, img_size)
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)
435 def print_base64_image(self,img):
437 print 'print_b64_img'
439 id = md5.new(img).digest()
441 if id not in self.img_cache:
444 img = img[img.find(',')+1:]
445 f = io.BytesIO('img')
446 f.write(base64.decodestring(img))
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])
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}"
556 ret = formatstr.format(value)
558 ret = ret.replace(',','COMMA')
559 ret = ret.replace('.','DOT')
560 ret = ret.replace('COMMA',thousands_separator)
561 ret = ret.replace('DOT',decimals_separator)
565 if position == 'after':
571 def print_elem(stylestack, serializer, elem, indent=0):
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'},
584 if elem.tag in elem_styles:
585 stylestack.set(elem_styles[elem.tag])
586 stylestack.set(elem.attrib)
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)
592 print_elem(stylestack,serializer,child)
593 serializer.start_inline(stylestack)
594 serializer.text(child.tail)
595 serializer.end_entity()
596 serializer.end_entity()
598 elif elem.tag in ('span','em','b','left','right'):
599 serializer.start_inline(stylestack)
600 serializer.text(elem.text)
602 print_elem(stylestack,serializer,child)
603 serializer.start_inline(stylestack)
604 serializer.text(child.tail)
605 serializer.end_entity()
606 serializer.end_entity()
608 elif elem.tag == 'value':
609 serializer.start_inline(stylestack)
610 serializer.pre(format_value(
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')
620 serializer.end_entity()
622 elif elem.tag == 'line':
623 width = stylestack.get('width')
624 if stylestack.get('size') in ('double', 'double-width'):
627 lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio'))
628 serializer.start_block(stylestack)
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()
638 elif elem.tag == 'ul':
639 serializer.start_block(stylestack)
640 bullet = stylestack.get('bullet')
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()
648 elif elem.tag == 'ol':
649 cwidth = len(str(len(elem))) + 2
651 serializer.start_block(stylestack)
653 if child.tag == 'li':
654 serializer.style(stylestack)
655 serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth))
657 print_elem(stylestack,serializer,child,indent=indent+1)
658 serializer.end_entity()
660 elif elem.tag == 'pre':
661 serializer.start_block(stylestack)
662 serializer.pre(elem.text)
663 serializer.end_entity()
665 elif elem.tag == 'hr':
666 width = stylestack.get('width')
667 if stylestack.get('size') in ('double', 'double-width'):
669 serializer.start_block(stylestack)
670 serializer.text('-'*width)
671 serializer.end_entity()
673 elif elem.tag == 'br':
674 serializer.linebreak()
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'])
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()
685 elif elem.tag == 'cut':
687 elif elem.tag == 'partialcut':
688 self.cut(mode='part')
689 elif elem.tag == 'cashdraw':
696 stylestack = StyleStack()
697 serializer = XmlSerializer(self)
698 root = ET.fromstring(xml)
700 self._raw(stylestack.to_escpos())
702 print_elem(stylestack,serializer,root)
704 if 'open-cashdrawer' in root.attrib and root.attrib['open-cashdrawer'] == 'true':
707 if not 'cut' in root.attrib or root.attrib['cut'] == 'true' :
710 except Exception as e:
711 errmsg = str(e)+'\n'+'-'*48+'\n'+traceback.format_exc() + '-'*48+'\n'
718 """ Print Utf8 encoded alpha-numeric text """
722 txt = txt.decode('utf-8')
725 txt = txt.decode('utf-16')
731 def encode_char(char):
733 Encodes a single utf-8 character into a sequence of
734 esc-pos code page change instructions and character declarations
736 char_utf8 = char.encode('utf-8')
738 encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character
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,
763 remaining = copy.copy(encodings)
768 while True: # Trying all encoding until one succeeds
770 if encoding == 'katakana': # Japanese characters
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'))
781 if kata in TXT_ENC_KATAKANA_MAP:
782 encoded = TXT_ENC_KATAKANA_MAP[kata]
787 encoded = char.encode(encoding)
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]
797 encoded = '\xb1' # could not encode, output error character
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
811 buffer += encode_char(c)
814 txt = encode_str(txt)
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(' ')
822 txt = txt[:dspace] + txt[dspace+1:]
823 self.extra_chars -= 1
829 def set(self, align='left', font='a', type='normal', width=1, height=1):
830 """ Set text properties """
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)
839 if font.upper() == "B":
840 self._raw(TXT_FONT_B)
841 else: # DEFAULT FONT: A
842 self._raw(TXT_FONT_A)
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)
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)
876 def cut(self, mode=''):
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)
887 def cashdraw(self, pin):
888 """ Send pulse to kick the cash drawer """
894 raise CashDrawerError()
898 """ Hardware operations """
899 if hw.upper() == "INIT":
901 elif hw.upper() == "SELECT":
903 elif hw.upper() == "RESET":
905 else: # DEFAULT: DOES NOTHING
909 def control(self, ctl):
910 """ Feed control sequences """
911 if ctl.upper() == "LF":
913 elif ctl.upper() == "FF":
915 elif ctl.upper() == "CR":
917 elif ctl.upper() == "HT":
919 elif ctl.upper() == "VT":