3 @author: Manuel F Martinez <manpaz@bashlinux.com>
4 @organization: Bashlinux
5 @copyright: Copyright (c) 2012 Bashlinux
21 import xml.etree.ElementTree as ET
29 print 'ESC/POS: please install jcconv for improved Japanese receipt printing:'
30 print ' # pip install jcconv'
32 from constants import *
33 from exceptions import *
36 # def __init__(self,width=40,margin=10,tabwidth=2,indent=0):
38 # self.margin = margin
39 # self.tabwidth = tabwidth
40 # self.indent = indent
44 """ ESC/POS Printer object """
49 def _check_image_size(self, size):
50 """ Check and fix the size of the image to 32 bits """
54 image_border = 32 - (size % 32)
55 if (image_border % 2) == 0:
56 return (image_border / 2, image_border / 2)
58 return (image_border / 2, (image_border / 2) + 1)
60 def _print_image(self, line, size):
61 """ Print formatted image """
68 buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
69 self._raw(buffer.decode('hex'))
73 hex_string = int(line[i:i+8],2)
74 buffer += "%02X" % hex_string
78 self._raw(buffer.decode("hex"))
82 def _raw_print_image(self, line, size, output=None ):
83 """ Print formatted image """
96 buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
97 raw += buffer.decode('hex')
101 hex_string = int(line[i:i+8],2)
102 buffer += "%02X" % hex_string
106 raw += buffer.decode("hex")
112 def _convert_image(self, im):
113 """ Parse image and prepare it to a printable format """
123 print "WARNING: Image is wider than 512 and could be truncated at print time "
125 raise ImageSizeError()
127 im_border = self._check_image_size(im.size[0])
128 for i in range(im_border[0]):
130 for i in range(im_border[1]):
133 for y in range(im.size[1]):
136 img_size[0] += im_border[0]
137 for x in range(im.size[0]):
139 RGB = im.getpixel((x, y))
140 im_color = (RGB[0] + RGB[1] + RGB[2])
142 pattern_len = len(im_pattern)
143 switch = (switch - 1 ) * (-1)
144 for x in range(pattern_len):
145 if im_color <= (255 * 3 / pattern_len * (x+1)):
146 if im_pattern[x] == "X":
147 pix_line += "%d" % switch
149 pix_line += im_pattern[x]
151 elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3):
152 pix_line += im_pattern[-1]
155 img_size[0] += im_border[1]
157 return (pix_line, img_size)
159 def image(self,path_img):
160 """ Open image file """
161 im_open = Image.open(path_img)
162 im = im_open.convert("RGB")
163 # Convert the RGB image in printable image
164 pix_line, img_size = self._convert_image(im)
165 self._print_image(pix_line, img_size)
167 def print_base64_image(self,img):
169 print 'print_b64_img'
171 id = md5.new(img).digest()
173 if id not in self.img_cache:
176 img = img[img.find(',')+1:]
177 f = io.BytesIO('img')
178 f.write(base64.decodestring(img))
180 img_rgba = Image.open(f)
181 img = Image.new('RGB', img_rgba.size, (255,255,255))
182 img.paste(img_rgba, mask=img_rgba.split()[3])
184 print 'convert image'
186 pix_line, img_size = self._convert_image(img)
190 buffer = self._raw_print_image(pix_line, img_size)
191 self.img_cache[id] = buffer
195 self._raw(self.img_cache[id])
198 """ Print QR Code for the provided string """
199 qr_code = qrcode.QRCode(version=4, box_size=4, border=1)
200 qr_code.add_data(text)
201 qr_code.make(fit=True)
202 qr_img = qr_code.make_image()
203 im = qr_img._img.convert("RGB")
204 # Convert the RGB image in printable image
205 self._convert_image(im)
207 def barcode(self, code, bc, width=255, height=2, pos='below', font='a'):
208 """ Print Barcode """
210 self._raw(TXT_ALIGN_CT)
212 if height >=2 or height <=6:
213 self._raw(BARCODE_HEIGHT)
215 raise BarcodeSizeError()
217 if width >= 1 or width <=255:
218 self._raw(BARCODE_WIDTH)
220 raise BarcodeSizeError()
222 if font.upper() == "B":
223 self._raw(BARCODE_FONT_B)
224 else: # DEFAULT FONT: A
225 self._raw(BARCODE_FONT_A)
227 if pos.upper() == "OFF":
228 self._raw(BARCODE_TXT_OFF)
229 elif pos.upper() == "BOTH":
230 self._raw(BARCODE_TXT_BTH)
231 elif pos.upper() == "ABOVE":
232 self._raw(BARCODE_TXT_ABV)
233 else: # DEFAULT POSITION: BELOW
234 self._raw(BARCODE_TXT_BLW)
236 if bc.upper() == "UPC-A":
237 self._raw(BARCODE_UPC_A)
238 elif bc.upper() == "UPC-E":
239 self._raw(BARCODE_UPC_E)
240 elif bc.upper() == "EAN13":
241 self._raw(BARCODE_EAN13)
242 elif bc.upper() == "EAN8":
243 self._raw(BARCODE_EAN8)
244 elif bc.upper() == "CODE39":
245 self._raw(BARCODE_CODE39)
246 elif bc.upper() == "ITF":
247 self._raw(BARCODE_ITF)
248 elif bc.upper() == "NW7":
249 self._raw(BARCODE_NW7)
251 raise BarcodeTypeError()
256 raise exception.BarcodeCodeError()
258 def receipt(self,xml):
259 root = ET.fromstring(xml)
260 open_cashdrawer = False
268 if root.tag != 'receipt':
269 print 'Error: expecting receipt xml and not '+str(root.tag)
271 if 'open-cashdrawer' in root.attrib:
272 open_cashdrawer = root.attrib['open-cashdrawer'] == 'true'
273 if 'cut' in root.attrib:
274 cut_receipt = root.attrib['cut'] == 'true'
276 # def justify(string,width):
277 # words = string.split()
282 # if len(w) <= width - len(line):
288 # lines.append(line.ljust(width))
292 # lines.append(line.ljust(width))
295 # def strindent(indent,string):
296 # ind = tabwidth * indent
298 # lines = justify(string,rem)
301 # txt += ' '*ind + '\n'
305 def strclean(string):
308 string = string.strip()
309 string = re.sub('\s+',' ',string)
313 def print_ul(elem, indent=0):
315 bullet = ' '+bullets[indent % 2]+' '
318 if lelem.tag == 'li':
319 self.text(' ' * indent * tabwidth + bullet + strclean(lelem.text)+'\n')
320 elif lelem.tag == 'ul':
321 print_ul(lelem,indent+1)
322 elif lelem.tag == 'ol':
323 print_old(lelem,indent+1)
325 def print_ol(elem, indent=0):
326 cwidth = len(str(len(elem))) + 2
329 if lelem.tag == 'li':
330 self.text(' ' * indent * tabwidth + ' ' + (str(i)+'.').ljust(cwidth)+strclean(lelem.text)+'\n')
332 elif lelem.tag == 'ul':
333 print_ul(lelem,indent+1)
334 elif lelem.tag == 'ol':
335 print_ol(lelem,indent+1)
339 if elem.tag == 'line':
340 left = strclean(elem.text)
343 if lelem.tag == 'left':
344 left = strclean(lelem.text)
346 elif lelem.tag == 'right':
347 right = strclean(lelem.text)
349 lwidth = int(width * ratio)
350 rwidth = width - lwidth
351 lwidth = lwidth - indent
354 if len(left) != lwidth:
355 left = left + ' ' * (lwidth - len(left))
357 right = right[-rwidth:]
358 if len(right) != rwidth:
359 right = ' ' * (rwidth - len(right)) + right
360 line = ' ' * indent + left + right + '\n'
363 elif elem.tag == 'img':
364 if src in elem.attrib and 'data:' in elem.attrib['src']:
365 self.print_base64_image(elem.attrib['src'])
366 elif elem.tag == 'p':
367 self.text(strclean(elem.text)+'\n')
368 elif elem.tag == 'h1':
369 self.set(align='left', font='a', type='b', width=2, height=2)
370 self._raw('\x1b\x21\x30')
371 self._raw('\x1b\x45\x01')
372 self.text(strclean(elem.text)+'\n')
374 elif elem.tag == 'h2':
375 self.set(align='left', font='a', type='bu', width=1, height=2)
376 self._raw('\x1b\x21\x30')
377 self.text(strclean(elem.text)+'\n')
379 elif elem.tag == 'h3':
380 self.set(align='left', font='a', type='u', width=1, height=2)
381 self._raw('\x1b\x45\x01')
382 self.text(strclean(elem.text)+'\n')
384 elif elem.tag == 'h4':
385 self.set(align='left', font='a', type='bu', width=1, height=2)
386 self.text(strclean(elem.text)+'\n')
388 elif elem.tag == 'h5':
389 self.set(align='left', font='a', type='u', width=1, height=1)
390 self._raw('\x1b\x45\x01')
391 self.text(strclean(elem.text)+'\n')
393 elif elem.tag == 'pre':
395 elif elem.tag == 'cut':
397 elif elem.tag == 'ul':
399 elif elem.tag == 'ol':
401 elif elem.tag == 'hr':
402 self.text('-'*width+'\n')
403 elif elem.tag == 'br':
405 elif elem.tag == 'barcode' and 'encoding' in elem.attrib:
406 self.barcode(strclean(elem.text),elem.attrib['encoding'])
407 elif elem.tag == 'partialcut':
408 self.cut(mode='part')
409 elif elem.tag == 'cashdraw':
421 """ Print Utf8 encoded alpha-numeric text """
425 txt = txt.decode('utf-8')
428 txt = txt.decode('utf-16')
434 def encode_char(char):
436 Encodes a single utf-8 character into a sequence of
437 esc-pos code page change instructions and character declarations
439 char_utf8 = char.encode('utf-8')
441 encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character
443 # TODO use ordering to prevent useless switches
444 # TODO Support other encodings not natively supported by python ( Thai, Khazakh, Kanjis )
445 'cp437': TXT_ENC_PC437,
446 'cp850': TXT_ENC_PC850,
447 'cp852': TXT_ENC_PC852,
448 'cp857': TXT_ENC_PC857,
449 'cp858': TXT_ENC_PC858,
450 'cp860': TXT_ENC_PC860,
451 'cp863': TXT_ENC_PC863,
452 'cp865': TXT_ENC_PC865,
453 'cp866': TXT_ENC_PC866,
454 'cp862': TXT_ENC_PC862,
455 'cp720': TXT_ENC_PC720,
456 'iso8859_2': TXT_ENC_8859_2,
457 'iso8859_7': TXT_ENC_8859_7,
458 'iso8859_9': TXT_ENC_8859_9,
459 'cp1254' : TXT_ENC_WPC1254,
460 'cp1255' : TXT_ENC_WPC1255,
461 'cp1256' : TXT_ENC_WPC1256,
462 'cp1257' : TXT_ENC_WPC1257,
463 'cp1258' : TXT_ENC_WPC1258,
464 'katakana' : TXT_ENC_KATAKANA,
466 remaining = copy.copy(encodings)
471 while True: # Trying all encoding until one succeeds
473 if encoding == 'katakana': # Japanese characters
475 # try to convert japanese text to a half-katakanas
476 kata = jcconv.kata2half(jcconv.hira2kata(char_utf8))
477 if kata != char_utf8:
478 self.extra_chars += len(kata.decode('utf-8')) - 1
479 # the conversion may result in multiple characters
480 return encode_str(kata.decode('utf-8'))
484 if kata in TXT_ENC_KATAKANA_MAP:
485 encoded = TXT_ENC_KATAKANA_MAP[kata]
490 encoded = char.encode(encoding)
493 except ValueError: #the encoding failed, select another one and retry
494 if encoding in remaining:
495 del remaining[encoding]
496 if len(remaining) >= 1:
497 encoding = remaining.items()[0][0]
500 encoded = '\xb1' # could not encode, output error character
503 if encoding != self.encoding:
504 # if the encoding changed, remember it and prefix the character with
505 # the esc-pos encoding change sequence
506 self.encoding = encoding
507 encoded = encodings[encoding] + encoded
514 buffer += encode_char(c)
517 txt = encode_str(txt)
519 # if the utf-8 -> codepage conversion inserted extra characters,
520 # remove double spaces to try to restore the original string length
521 # and prevent printing alignment issues
522 while self.extra_chars > 0:
523 dspace = txt.find(' ')
525 txt = txt[:dspace] + txt[dspace+1:]
526 self.extra_chars -= 1
532 def set(self, align='left', font='a', type='normal', width=1, height=1):
533 """ Set text properties """
535 if align.upper() == "CENTER":
536 self._raw(TXT_ALIGN_CT)
537 elif align.upper() == "RIGHT":
538 self._raw(TXT_ALIGN_RT)
539 elif align.upper() == "LEFT":
540 self._raw(TXT_ALIGN_LT)
542 if font.upper() == "B":
543 self._raw(TXT_FONT_B)
544 else: # DEFAULT FONT: A
545 self._raw(TXT_FONT_A)
547 if type.upper() == "B":
548 self._raw(TXT_BOLD_ON)
549 self._raw(TXT_UNDERL_OFF)
550 elif type.upper() == "U":
551 self._raw(TXT_BOLD_OFF)
552 self._raw(TXT_UNDERL_ON)
553 elif type.upper() == "U2":
554 self._raw(TXT_BOLD_OFF)
555 self._raw(TXT_UNDERL2_ON)
556 elif type.upper() == "BU":
557 self._raw(TXT_BOLD_ON)
558 self._raw(TXT_UNDERL_ON)
559 elif type.upper() == "BU2":
560 self._raw(TXT_BOLD_ON)
561 self._raw(TXT_UNDERL2_ON)
562 elif type.upper == "NORMAL":
563 self._raw(TXT_BOLD_OFF)
564 self._raw(TXT_UNDERL_OFF)
566 if width == 2 and height != 2:
567 self._raw(TXT_NORMAL)
568 self._raw(TXT_2WIDTH)
569 elif height == 2 and width != 2:
570 self._raw(TXT_NORMAL)
571 self._raw(TXT_2HEIGHT)
572 elif height == 2 and width == 2:
573 self._raw(TXT_2WIDTH)
574 self._raw(TXT_2HEIGHT)
575 else: # DEFAULT SIZE: NORMAL
576 self._raw(TXT_NORMAL)
579 def cut(self, mode=''):
581 # Fix the size between last line and cut
582 # TODO: handle this with a line feed
583 self._raw("\n\n\n\n\n\n")
584 if mode.upper() == "PART":
585 self._raw(PAPER_PART_CUT)
586 else: # DEFAULT MODE: FULL CUT
587 self._raw(PAPER_FULL_CUT)
590 def cashdraw(self, pin):
591 """ Send pulse to kick the cash drawer """
597 raise CashDrawerError()
601 """ Hardware operations """
602 if hw.upper() == "INIT":
604 elif hw.upper() == "SELECT":
606 elif hw.upper() == "RESET":
608 else: # DEFAULT: DOES NOTHING
612 def control(self, ctl):
613 """ Feed control sequences """
614 if ctl.upper() == "LF":
616 elif ctl.upper() == "FF":
618 elif ctl.upper() == "CR":
620 elif ctl.upper() == "HT":
622 elif ctl.upper() == "VT":