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