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
67 'value-symbol-position': 'after',
68 'value-autoint': 'off',
69 'value-decimals-separator': '.',
70 'value-thousands-separator': ',',
75 self.types = { # attribute types, default is string and can be ommitted
79 'line-ratio': 'float',
80 'value-decimals': 'int',
85 # translation from styles to escpos commands
86 # some style do not correspond to escpos command are used by
87 # the serializer instead
90 'right': TXT_ALIGN_RT,
91 'center': TXT_ALIGN_CT,
94 'off': TXT_UNDERL_OFF,
96 'double': TXT_UNDERL2_ON,
107 'normal': TXT_NORMAL,
108 'double-height': TXT_2HEIGHT,
109 'double-width': TXT_2WIDTH,
110 'double': TXT_DOUBLE,
113 'black': TXT_COLOR_BLACK,
114 'red': TXT_COLOR_RED,
118 self.push(self.defaults)
121 """ what's the value of a style at the current stack level"""
122 level = len(self.stack) -1
124 if style in self.stack[level]:
125 return self.stack[level][style]
130 def enforce_type(self, attr, val):
131 """converts a value to the attribute's type"""
132 if not attr in self.types:
134 elif self.types[attr] == 'int':
135 return int(float(val))
136 elif self.types[attr] == 'float':
141 def push(self, style={}):
142 """push a new level on the stack with a style dictionnary containing style:value pairs"""
145 if attr in self.cmds and not style[attr] in self.cmds[attr]:
146 print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr)
148 _style[attr] = self.enforce_type(attr, style[attr])
149 self.stack.append(_style)
151 def set(self, style={}):
152 """overrides style values at the current stack level"""
155 if attr in self.cmds and not style[attr] in self.cmds[attr]:
156 print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr)
158 self.stack[-1][attr] = self.enforce_type(attr, style[attr])
161 """ pop a style stack level """
162 if len(self.stack) > 1 :
163 self.stack = self.stack[:-1]
166 """ converts the current style to an escpos command string """
168 for style in self.cmds:
169 cmd += self.cmds[style][self.get(style)]
174 Converts the xml inline / block tree structure to a string,
175 keeping track of newlines and spacings.
176 The string is outputted asap to the provided escpos driver.
178 def __init__(self,escpos):
180 self.stack = ['block']
183 def start_inline(self,stylestack=None):
184 """ starts an inline entity with an optional style definition """
185 self.stack.append('inline')
187 self.escpos._raw(' ')
189 self.style(stylestack)
191 def start_block(self,stylestack=None):
192 """ starts a block entity with an optional style definition """
194 self.escpos._raw('\n')
196 self.stack.append('block')
198 self.style(stylestack)
200 def end_entity(self):
201 """ ends the entity definition. (but does not cancel the active style!) """
202 if self.stack[-1] == 'block' and self.dirty:
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)
222 self.escpos.text(text)
225 """ inserts a linebreak in the entity """
227 self.escpos._raw('\n')
229 def style(self,stylestack):
230 """ apply a style to the entity (only applies to content added after the definition) """
231 self.raw(stylestack.to_escpos())
234 """ puts raw text or escpos command in the entity without affecting the state of the serializer """
235 self.escpos._raw(raw)
237 class XmlLineSerializer:
239 This is used to convert a xml tree into a single line, with a left and a right part.
240 The content is not output to escpos directly, and is intended to be fedback to the
241 XmlSerializer as the content of a block entity.
243 def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5):
244 self.tabwidth = tabwidth
246 self.width = max(0, width - int(tabwidth*indent))
247 self.lwidth = int(self.width*ratio)
248 self.rwidth = max(0, self.width - self.lwidth)
257 if self.clwidth < self.lwidth:
258 txt = txt[:max(0, self.lwidth - self.clwidth)]
260 self.clwidth += len(txt)
262 if self.crwidth < self.rwidth:
263 txt = txt[:max(0, self.rwidth - self.crwidth)]
265 self.crwidth += len(txt)
267 def start_inline(self,stylestack=None):
268 if (self.left and self.clwidth) or (not self.left and self.crwidth):
271 def start_block(self,stylestack=None):
272 self.start_inline(stylestack)
274 def end_entity(self):
284 text = re.sub('\s+',' ',text)
290 def style(self,stylestack):
295 def start_right(self):
299 return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer
303 """ ESC/POS Printer object """
308 def _check_image_size(self, size):
309 """ Check and fix the size of the image to 32 bits """
313 image_border = 32 - (size % 32)
314 if (image_border % 2) == 0:
315 return (image_border / 2, image_border / 2)
317 return (image_border / 2, (image_border / 2) + 1)
319 def _print_image(self, line, size):
320 """ Print formatted image """
326 self._raw(S_RASTER_N)
327 buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
328 self._raw(buffer.decode('hex'))
332 hex_string = int(line[i:i+8],2)
333 buffer += "%02X" % hex_string
337 self._raw(buffer.decode("hex"))
341 def _raw_print_image(self, line, size, output=None ):
342 """ Print formatted image """
355 buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
356 raw += buffer.decode('hex')
360 hex_string = int(line[i:i+8],2)
361 buffer += "%02X" % hex_string
365 raw += buffer.decode("hex")
371 def _convert_image(self, im):
372 """ Parse image and prepare it to a printable format """
382 print "WARNING: Image is wider than 512 and could be truncated at print time "
384 raise ImageSizeError()
386 im_border = self._check_image_size(im.size[0])
387 for i in range(im_border[0]):
389 for i in range(im_border[1]):
392 for y in range(im.size[1]):
395 img_size[0] += im_border[0]
396 for x in range(im.size[0]):
398 RGB = im.getpixel((x, y))
399 im_color = (RGB[0] + RGB[1] + RGB[2])
401 pattern_len = len(im_pattern)
402 switch = (switch - 1 ) * (-1)
403 for x in range(pattern_len):
404 if im_color <= (255 * 3 / pattern_len * (x+1)):
405 if im_pattern[x] == "X":
406 pix_line += "%d" % switch
408 pix_line += im_pattern[x]
410 elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3):
411 pix_line += im_pattern[-1]
414 img_size[0] += im_border[1]
416 return (pix_line, img_size)
418 def image(self,path_img):
419 """ Open image file """
420 im_open = Image.open(path_img)
421 im = im_open.convert("RGB")
422 # Convert the RGB image in printable image
423 pix_line, img_size = self._convert_image(im)
424 self._print_image(pix_line, img_size)
426 def print_base64_image(self,img):
428 print 'print_b64_img'
430 id = md5.new(img).digest()
432 if id not in self.img_cache:
435 img = img[img.find(',')+1:]
436 f = io.BytesIO('img')
437 f.write(base64.decodestring(img))
439 img_rgba = Image.open(f)
440 img = Image.new('RGB', img_rgba.size, (255,255,255))
441 img.paste(img_rgba, mask=img_rgba.split()[3])
443 print 'convert image'
445 pix_line, img_size = self._convert_image(img)
449 buffer = self._raw_print_image(pix_line, img_size)
450 self.img_cache[id] = buffer
454 self._raw(self.img_cache[id])
457 """ Print QR Code for the provided string """
458 qr_code = qrcode.QRCode(version=4, box_size=4, border=1)
459 qr_code.add_data(text)
460 qr_code.make(fit=True)
461 qr_img = qr_code.make_image()
462 im = qr_img._img.convert("RGB")
463 # Convert the RGB image in printable image
464 self._convert_image(im)
466 def barcode(self, code, bc, width=255, height=2, pos='below', font='a'):
467 """ Print Barcode """
469 self._raw(TXT_ALIGN_CT)
471 if height >=2 or height <=6:
472 self._raw(BARCODE_HEIGHT)
474 raise BarcodeSizeError()
476 if width >= 1 or width <=255:
477 self._raw(BARCODE_WIDTH)
479 raise BarcodeSizeError()
481 if font.upper() == "B":
482 self._raw(BARCODE_FONT_B)
483 else: # DEFAULT FONT: A
484 self._raw(BARCODE_FONT_A)
486 if pos.upper() == "OFF":
487 self._raw(BARCODE_TXT_OFF)
488 elif pos.upper() == "BOTH":
489 self._raw(BARCODE_TXT_BTH)
490 elif pos.upper() == "ABOVE":
491 self._raw(BARCODE_TXT_ABV)
492 else: # DEFAULT POSITION: BELOW
493 self._raw(BARCODE_TXT_BLW)
495 if bc.upper() == "UPC-A":
496 self._raw(BARCODE_UPC_A)
497 elif bc.upper() == "UPC-E":
498 self._raw(BARCODE_UPC_E)
499 elif bc.upper() == "EAN13":
500 self._raw(BARCODE_EAN13)
501 elif bc.upper() == "EAN8":
502 self._raw(BARCODE_EAN8)
503 elif bc.upper() == "CODE39":
504 self._raw(BARCODE_CODE39)
505 elif bc.upper() == "ITF":
506 self._raw(BARCODE_ITF)
507 elif bc.upper() == "NW7":
508 self._raw(BARCODE_NW7)
510 raise BarcodeTypeError()
515 raise exception.BarcodeCodeError()
517 def receipt(self,xml):
519 Prints an xml based receipt definition
522 def strclean(string):
525 string = string.strip()
526 string = re.sub('\s+',' ',string)
529 def format_value(value, decimals=3, width=0, decimals_separator='.', thousands_separator=',', autoint=False, symbol='', position='after'):
530 decimals = max(0,int(decimals))
531 width = max(0,int(width))
534 if autoint and math.floor(value) == value:
539 if thousands_separator:
540 formatstr = "{:"+str(width)+",."+str(decimals)+"f}"
542 formatstr = "{:"+str(width)+"."+str(decimals)+"f}"
545 ret = formatstr.format(value)
546 ret = ret.replace(',','COMMA')
547 ret = ret.replace('.','DOT')
548 ret = ret.replace('COMMA',thousands_separator)
549 ret = ret.replace('DOT',decimals_separator)
552 if position == 'after':
558 def print_elem(stylestack, serializer, elem, indent=0):
561 'h1': {'bold': 'on', 'size':'double'},
562 'h2': {'size':'double'},
563 'h3': {'bold': 'on', 'size':'double-height'},
564 'h4': {'size': 'double-height'},
565 'h5': {'bold': 'on'},
571 if elem.tag in elem_styles:
572 stylestack.set(elem_styles[elem.tag])
573 stylestack.set(elem.attrib)
575 if elem.tag in ('p','div','section','article','receipt','header','footer','li','h1','h2','h3','h4','h5'):
576 serializer.start_block(stylestack)
577 serializer.text(elem.text)
579 print_elem(stylestack,serializer,child)
580 serializer.start_inline(stylestack)
581 serializer.text(child.tail)
582 serializer.end_entity()
583 serializer.end_entity()
585 elif elem.tag in ('span','em','b','left','right'):
586 serializer.start_inline(stylestack)
587 serializer.text(elem.text)
589 print_elem(stylestack,serializer,child)
590 serializer.start_inline(stylestack)
591 serializer.text(child.tail)
592 serializer.end_entity()
593 serializer.end_entity()
595 elif elem.tag == 'value':
596 serializer.start_inline(stylestack)
597 serializer.pre(format_value(
599 decimals=stylestack.get('value-decimals'),
600 width=stylestack.get('value-width'),
601 decimals_separator=stylestack.get('value-decimals-separator'),
602 thousands_separator=stylestack.get('value-thousands-separator'),
603 autoint=(stylestack.get('value-autoint') == 'on'),
604 symbol=stylestack.get('value-symbol'),
605 position=stylestack.get('value-symbol-position')
607 serializer.end_entity()
609 elif elem.tag == 'line':
610 width = stylestack.get('width')
611 if stylestack.get('size') in ('double', 'double-width'):
614 lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio'))
615 serializer.start_block(stylestack)
617 if child.tag == 'left':
618 print_elem(stylestack,lineserializer,child,indent=indent)
619 elif child.tag == 'right':
620 lineserializer.start_right()
621 print_elem(stylestack,lineserializer,child,indent=indent)
622 serializer.pre(lineserializer.get_line())
623 serializer.end_entity()
625 elif elem.tag == 'ul':
626 serializer.start_block(stylestack)
627 bullet = stylestack.get('bullet')
629 if child.tag == 'li':
630 serializer.style(stylestack)
631 serializer.raw(' ' * indent * stylestack.get('tabwidth') + bullet)
632 print_elem(stylestack,serializer,child,indent=indent+1)
633 serializer.end_entity()
635 elif elem.tag == 'ol':
636 cwidth = len(str(len(elem))) + 2
638 serializer.start_block(stylestack)
640 if child.tag == 'li':
641 serializer.style(stylestack)
642 serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth))
644 print_elem(stylestack,serializer,child,indent=indent+1)
645 serializer.end_entity()
647 elif elem.tag == 'pre':
648 serializer.start_block(stylestack)
649 serializer.pre(elem.text)
650 serializer.end_entity()
652 elif elem.tag == 'hr':
653 width = stylestack.get('width')
654 if stylestack.get('size') in ('double', 'double-width'):
656 serializer.start_block(stylestack)
657 serializer.text('-'*width)
658 serializer.end_entity()
660 elif elem.tag == 'br':
661 serializer.linebreak()
663 elif elem.tag == 'img':
664 if 'src' in elem.attrib and 'data:' in elem.attrib['src']:
665 self.print_base64_image(elem.attrib['src'])
667 elif elem.tag == 'barcode' and 'encoding' in elem.attrib:
668 serializer.start_block(stylestack)
669 self.barcode(strclean(elem.text),elem.attrib['encoding'])
670 serializer.end_entity()
672 elif elem.tag == 'cut':
674 elif elem.tag == 'partialcut':
675 self.cut(mode='part')
676 elif elem.tag == 'cashdraw':
683 stylestack = StyleStack()
684 serializer = XmlSerializer(self)
685 root = ET.fromstring(xml)
687 self._raw(stylestack.to_escpos())
689 print_elem(stylestack,serializer,root)
691 if 'open-cashdrawer' in root.attrib and root.attrib['open-cashdrawer'] == 'true':
694 if not 'cut' in root.attrib or root.attrib['cut'] == 'true' :
697 except Exception as e:
698 errmsg = str(e)+'\n'+'-'*48+'\n'+traceback.format_exc() + '-'*48+'\n'
705 """ Print Utf8 encoded alpha-numeric text """
709 txt = txt.decode('utf-8')
712 txt = txt.decode('utf-16')
718 def encode_char(char):
720 Encodes a single utf-8 character into a sequence of
721 esc-pos code page change instructions and character declarations
723 char_utf8 = char.encode('utf-8')
725 encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character
727 # TODO use ordering to prevent useless switches
728 # TODO Support other encodings not natively supported by python ( Thai, Khazakh, Kanjis )
729 'cp437': TXT_ENC_PC437,
730 'cp850': TXT_ENC_PC850,
731 'cp852': TXT_ENC_PC852,
732 'cp857': TXT_ENC_PC857,
733 'cp858': TXT_ENC_PC858,
734 'cp860': TXT_ENC_PC860,
735 'cp863': TXT_ENC_PC863,
736 'cp865': TXT_ENC_PC865,
737 'cp866': TXT_ENC_PC866,
738 'cp862': TXT_ENC_PC862,
739 'cp720': TXT_ENC_PC720,
740 'iso8859_2': TXT_ENC_8859_2,
741 'iso8859_7': TXT_ENC_8859_7,
742 'iso8859_9': TXT_ENC_8859_9,
743 'cp1254' : TXT_ENC_WPC1254,
744 'cp1255' : TXT_ENC_WPC1255,
745 'cp1256' : TXT_ENC_WPC1256,
746 'cp1257' : TXT_ENC_WPC1257,
747 'cp1258' : TXT_ENC_WPC1258,
748 'katakana' : TXT_ENC_KATAKANA,
750 remaining = copy.copy(encodings)
755 while True: # Trying all encoding until one succeeds
757 if encoding == 'katakana': # Japanese characters
759 # try to convert japanese text to a half-katakanas
760 kata = jcconv.kata2half(jcconv.hira2kata(char_utf8))
761 if kata != char_utf8:
762 self.extra_chars += len(kata.decode('utf-8')) - 1
763 # the conversion may result in multiple characters
764 return encode_str(kata.decode('utf-8'))
768 if kata in TXT_ENC_KATAKANA_MAP:
769 encoded = TXT_ENC_KATAKANA_MAP[kata]
774 encoded = char.encode(encoding)
777 except ValueError: #the encoding failed, select another one and retry
778 if encoding in remaining:
779 del remaining[encoding]
780 if len(remaining) >= 1:
781 encoding = remaining.items()[0][0]
784 encoded = '\xb1' # could not encode, output error character
787 if encoding != self.encoding:
788 # if the encoding changed, remember it and prefix the character with
789 # the esc-pos encoding change sequence
790 self.encoding = encoding
791 encoded = encodings[encoding] + encoded
798 buffer += encode_char(c)
801 txt = encode_str(txt)
803 # if the utf-8 -> codepage conversion inserted extra characters,
804 # remove double spaces to try to restore the original string length
805 # and prevent printing alignment issues
806 while self.extra_chars > 0:
807 dspace = txt.find(' ')
809 txt = txt[:dspace] + txt[dspace+1:]
810 self.extra_chars -= 1
816 def set(self, align='left', font='a', type='normal', width=1, height=1):
817 """ Set text properties """
819 if align.upper() == "CENTER":
820 self._raw(TXT_ALIGN_CT)
821 elif align.upper() == "RIGHT":
822 self._raw(TXT_ALIGN_RT)
823 elif align.upper() == "LEFT":
824 self._raw(TXT_ALIGN_LT)
826 if font.upper() == "B":
827 self._raw(TXT_FONT_B)
828 else: # DEFAULT FONT: A
829 self._raw(TXT_FONT_A)
831 if type.upper() == "B":
832 self._raw(TXT_BOLD_ON)
833 self._raw(TXT_UNDERL_OFF)
834 elif type.upper() == "U":
835 self._raw(TXT_BOLD_OFF)
836 self._raw(TXT_UNDERL_ON)
837 elif type.upper() == "U2":
838 self._raw(TXT_BOLD_OFF)
839 self._raw(TXT_UNDERL2_ON)
840 elif type.upper() == "BU":
841 self._raw(TXT_BOLD_ON)
842 self._raw(TXT_UNDERL_ON)
843 elif type.upper() == "BU2":
844 self._raw(TXT_BOLD_ON)
845 self._raw(TXT_UNDERL2_ON)
846 elif type.upper == "NORMAL":
847 self._raw(TXT_BOLD_OFF)
848 self._raw(TXT_UNDERL_OFF)
850 if width == 2 and height != 2:
851 self._raw(TXT_NORMAL)
852 self._raw(TXT_2WIDTH)
853 elif height == 2 and width != 2:
854 self._raw(TXT_NORMAL)
855 self._raw(TXT_2HEIGHT)
856 elif height == 2 and width == 2:
857 self._raw(TXT_2WIDTH)
858 self._raw(TXT_2HEIGHT)
859 else: # DEFAULT SIZE: NORMAL
860 self._raw(TXT_NORMAL)
863 def cut(self, mode=''):
865 # Fix the size between last line and cut
866 # TODO: handle this with a line feed
867 self._raw("\n\n\n\n\n\n")
868 if mode.upper() == "PART":
869 self._raw(PAPER_PART_CUT)
870 else: # DEFAULT MODE: FULL CUT
871 self._raw(PAPER_FULL_CUT)
874 def cashdraw(self, pin):
875 """ Send pulse to kick the cash drawer """
881 raise CashDrawerError()
885 """ Hardware operations """
886 if hw.upper() == "INIT":
888 elif hw.upper() == "SELECT":
890 elif hw.upper() == "RESET":
892 else: # DEFAULT: DOES NOTHING
896 def control(self, ctl):
897 """ Feed control sequences """
898 if ctl.upper() == "LF":
900 elif ctl.upper() == "FF":
902 elif ctl.upper() == "CR":
904 elif ctl.upper() == "HT":
906 elif ctl.upper() == "VT":