From ae1bd8dba55a1ceabc4a2448be497b3b427cc5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 24 Aug 2016 15:02:11 +0200 Subject: [PATCH 01/16] Rely on python-escpos for low-level commands. This rips out everything except the layout engine, and makes it run on python-escpos as the base. --- DESCRIPTION.rst | 13 - README.md | 22 +- setup.py | 2 +- test_printer.py | 59 --- xmlescpos/__init__.py | 2 +- xmlescpos/constants.py | 189 ------- xmlescpos/escpos.py | 927 --------------------------------- xmlescpos/exceptions.py | 116 ----- xmlescpos/layout.py | 547 +++++++++++++++++++ xmlescpos/printer.py | 202 ------- xmlescpos/supported_devices.py | 11 - 11 files changed, 558 insertions(+), 1532 deletions(-) delete mode 100644 test_printer.py delete mode 100644 xmlescpos/constants.py delete mode 100644 xmlescpos/escpos.py delete mode 100644 xmlescpos/exceptions.py create mode 100644 xmlescpos/layout.py delete mode 100644 xmlescpos/printer.py delete mode 100644 xmlescpos/supported_devices.py diff --git a/DESCRIPTION.rst b/DESCRIPTION.rst index 5fc0fe6..97ed119 100644 --- a/DESCRIPTION.rst +++ b/DESCRIPTION.rst @@ -1,4 +1,3 @@ - XML-ESC/POS =========== @@ -31,15 +30,3 @@ example is self-explanatory: from xmlescpos.printer import Usb printer = Usb(0x04b8,0x0e03) printer.receipt("
Hello World!
") - -Limitations ------------ -The utf8 support is incomplete, mostly asian languages -are not working since they are badly documented and -only supported by region-specific hardware. - -This is also the very first release, which is a simple -extraction from the Odoo code base. While it works well, -it needs some cleanup for public use. - -Also, the doc is non-existent. diff --git a/README.md b/README.md index 3f69d74..3afd6a1 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ XML-ESC/POS is a simple python library that allows you to print receipts on ESC/POS Compatible receipt printers with a simple utf8 -encoded XML format similar to HTML. Barcode, pictures, -text encoding are automatically handled. No more dicking +encoded XML format similar to HTML. No more dicking around with esc-pos commands ! The following example is self-explanatory: @@ -28,23 +27,20 @@ The following example is self-explanatory: -And printing from python is quite easy, you just -need the USB product / vendor id of your printer. -Some common ids are found in `supported_devices.py` - from xmlescpos.printer import Usb - printer = Usb(0x04b8,0x0e03) - printer.receipt("
Hello World!
") +It uses python-escpos internally. So to print it, you'd do: + + from escpos import printer + from xmlescpos import Layout + epson = printer.Dummy() # Or directly to USB, Serial, Network + + Layout(xml).format(epson) + ## Install sudo pip install pyxmlescpos -## Limitations - -The utf8 support is incomplete, mostly asian languages -are not working. Documentation is hard to find, support relies on region-specific hardware, etc. There is some very basic -support for Japanese. # Documentation ## XML Structure diff --git a/setup.py b/setup.py index bd9dcb4..9166b09 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ # project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/technical.html#install-requires-vs-requirements-files - install_requires=['pyusb'], + install_requires=['python-escpos'], # List additional groups of dependencies here (e.g. development dependencies). # You can install these using the following syntax, for example: diff --git a/test_printer.py b/test_printer.py deleted file mode 100644 index 3bb7507..0000000 --- a/test_printer.py +++ /dev/null @@ -1,59 +0,0 @@ -test_temp = """ - -

Receipt!

-

div,span,p,ul,ol are also supported

:w - - Product - 0.15 - -
- - TOTAL - 0.15 - - - 5449000000996 - - - -
-""" -from xmlescpos.exceptions import * -from xmlescpos.printer import Usb -import usb -import pprint -import sys - -pp = pprint.PrettyPrinter(indent=4) - -try: - printer = Usb(0x04b8,0x0202) - - printer._raw('\x1D\x28\x47\x02\x00\x30\x04'); - printer._raw('AAAA'); - printer._raw('\x0c'); - - printer._raw('\x1c\x61\x31'); - printer._raw('BBBB'); - printer._raw('\x0c'); - - printer._raw('\x1d\x28\x47\x02\x00\x50\x04'); - printer._raw('\x1D\x28\x47\x02\x00\x30\x04'); - printer._raw('\x1D\x28\x47\x02\x00\x54\x00'); - printer._raw('CCCC'); - printer._raw('\x1D\x28\x47\x02\x00\x54\x01'); - - #printer.receipt(test_temp) - pp.pprint(printer.get_printer_status()) - -except NoDeviceError as e: - print "No device found %s" %str(e) -except HandleDeviceError as e: - print "Impossible to handle the device due to previous error %s" % str(e) -except TicketNotPrinted as e: - print "The ticket does not seems to have been fully printed %s" % str(e) -except NoStatusError as e: - print "Impossible to get the status of the printer %s" % str(e) -finally: - printer.close() - diff --git a/xmlescpos/__init__.py b/xmlescpos/__init__.py index 3fdedde..481ce9b 100644 --- a/xmlescpos/__init__.py +++ b/xmlescpos/__init__.py @@ -1 +1 @@ -__all__ = ["constants","escpos","exceptions","printer","supported_devices"] +from .layout import Layout \ No newline at end of file diff --git a/xmlescpos/constants.py b/xmlescpos/constants.py deleted file mode 100644 index 93dd9a3..0000000 --- a/xmlescpos/constants.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- - -""" ESC/POS Commands (Constants) """ - -# Feed control sequences -CTL_LF = '\x0a' # Print and line feed -CTL_FF = '\x0c' # Form feed -CTL_CR = '\x0d' # Carriage return -CTL_HT = '\x09' # Horizontal tab -CTL_VT = '\x0b' # Vertical tab - -# RT Status commands -DLE_EOT_PRINTER = '\x10\x04\x01' # Transmit printer status -DLE_EOT_OFFLINE = '\x10\x04\x02' -DLE_EOT_ERROR = '\x10\x04\x03' -DLE_EOT_PAPER = '\x10\x04\x04' - -# Printer hardware -HW_INIT = '\x1b\x40' # Clear data in buffer and reset modes -HW_SELECT = '\x1b\x3d\x01' # Printer select -HW_RESET = '\x1b\x3f\x0a\x00' # Reset printer hardware -# Cash Drawer -CD_KICK_2 = '\x1b\x70\x00' # Sends a pulse to pin 2 [] -CD_KICK_5 = '\x1b\x70\x01' # Sends a pulse to pin 5 [] -# Paper -PAPER_FULL_CUT = '\x1d\x56\x00' # Full cut paper -PAPER_PART_CUT = '\x1d\x56\x01' # Partial cut paper -SHEET_SLIP_MODE = '\x1B\x63\x30\x04' # Print ticket on injet slip paper -SHEET_ROLL_MODE = '\x1B\x63\x30\x01' # Print ticket on paper roll - -# Text format -TXT_NORMAL = '\x1b\x21\x00' # Normal text -TXT_2HEIGHT = '\x1b\x21\x10' # Double height text -TXT_2WIDTH = '\x1b\x21\x20' # Double width text -TXT_DOUBLE = '\x1b\x21\x30' # Double height & Width -TXT_UNDERL_OFF = '\x1b\x2d\x00' # Underline font OFF -TXT_UNDERL_ON = '\x1b\x2d\x01' # Underline font 1-dot ON -TXT_UNDERL2_ON = '\x1b\x2d\x02' # Underline font 2-dot ON -TXT_BOLD_OFF = '\x1b\x45\x00' # Bold font OFF -TXT_BOLD_ON = '\x1b\x45\x01' # Bold font ON -TXT_FONT_A = '\x1b\x4d\x00' # Font type A -TXT_FONT_B = '\x1b\x4d\x01' # Font type B -TXT_ALIGN_LT = '\x1b\x61\x00' # Left justification -TXT_ALIGN_CT = '\x1b\x61\x01' # Centering -TXT_ALIGN_RT = '\x1b\x61\x02' # Right justification -TXT_COLOR_BLACK = '\x1b\x72\x00' # Default Color -TXT_COLOR_RED = '\x1b\x72\x01' # Alternative Color ( Usually Red ) - -# Text Encoding - -TXT_ENC_PC437 = '\x1b\x74\x00' # PC437 USA -TXT_ENC_KATAKANA= '\x1b\x74\x01' # KATAKANA (JAPAN) -TXT_ENC_PC850 = '\x1b\x74\x02' # PC850 Multilingual -TXT_ENC_PC860 = '\x1b\x74\x03' # PC860 Portuguese -TXT_ENC_PC863 = '\x1b\x74\x04' # PC863 Canadian-French -TXT_ENC_PC865 = '\x1b\x74\x05' # PC865 Nordic -TXT_ENC_KANJI6 = '\x1b\x74\x06' # One-pass Kanji, Hiragana -TXT_ENC_KANJI7 = '\x1b\x74\x07' # One-pass Kanji -TXT_ENC_KANJI8 = '\x1b\x74\x08' # One-pass Kanji -TXT_ENC_PC851 = '\x1b\x74\x0b' # PC851 Greek -TXT_ENC_PC853 = '\x1b\x74\x0c' # PC853 Turkish -TXT_ENC_PC857 = '\x1b\x74\x0d' # PC857 Turkish -TXT_ENC_PC737 = '\x1b\x74\x0e' # PC737 Greek -TXT_ENC_8859_7 = '\x1b\x74\x0f' # ISO8859-7 Greek -TXT_ENC_WPC1252 = '\x1b\x74\x10' # WPC1252 -TXT_ENC_PC866 = '\x1b\x74\x11' # PC866 Cyrillic #2 -TXT_ENC_PC852 = '\x1b\x74\x12' # PC852 Latin2 -TXT_ENC_PC858 = '\x1b\x74\x13' # PC858 Euro -TXT_ENC_KU42 = '\x1b\x74\x14' # KU42 Thai -TXT_ENC_TIS11 = '\x1b\x74\x15' # TIS11 Thai -TXT_ENC_TIS18 = '\x1b\x74\x1a' # TIS18 Thai -TXT_ENC_TCVN3 = '\x1b\x74\x1e' # TCVN3 Vietnamese -TXT_ENC_TCVN3B = '\x1b\x74\x1f' # TCVN3 Vietnamese -TXT_ENC_PC720 = '\x1b\x74\x20' # PC720 Arabic -TXT_ENC_WPC775 = '\x1b\x74\x21' # WPC775 Baltic Rim -TXT_ENC_PC855 = '\x1b\x74\x22' # PC855 Cyrillic -TXT_ENC_PC861 = '\x1b\x74\x23' # PC861 Icelandic -TXT_ENC_PC862 = '\x1b\x74\x24' # PC862 Hebrew -TXT_ENC_PC864 = '\x1b\x74\x25' # PC864 Arabic -TXT_ENC_PC869 = '\x1b\x74\x26' # PC869 Greek -TXT_ENC_PC936 = '\x1C\x21\x00' # PC936 GBK(Guobiao Kuozhan) -TXT_ENC_8859_2 = '\x1b\x74\x27' # ISO8859-2 Latin2 -TXT_ENC_8859_9 = '\x1b\x74\x28' # ISO8859-2 Latin9 -TXT_ENC_PC1098 = '\x1b\x74\x29' # PC1098 Farsi -TXT_ENC_PC1118 = '\x1b\x74\x2a' # PC1118 Lithuanian -TXT_ENC_PC1119 = '\x1b\x74\x2b' # PC1119 Lithuanian -TXT_ENC_PC1125 = '\x1b\x74\x2c' # PC1125 Ukrainian -TXT_ENC_WPC1250 = '\x1b\x74\x2d' # WPC1250 Latin2 -TXT_ENC_WPC1251 = '\x1b\x74\x2e' # WPC1251 Cyrillic -TXT_ENC_WPC1253 = '\x1b\x74\x2f' # WPC1253 Greek -TXT_ENC_WPC1254 = '\x1b\x74\x30' # WPC1254 Turkish -TXT_ENC_WPC1255 = '\x1b\x74\x31' # WPC1255 Hebrew -TXT_ENC_WPC1256 = '\x1b\x74\x32' # WPC1256 Arabic -TXT_ENC_WPC1257 = '\x1b\x74\x33' # WPC1257 Baltic Rim -TXT_ENC_WPC1258 = '\x1b\x74\x34' # WPC1258 Vietnamese -TXT_ENC_KZ1048 = '\x1b\x74\x35' # KZ-1048 Kazakhstan - -TXT_ENC_KATAKANA_MAP = { - # Maps UTF-8 Katakana symbols to KATAKANA Page Codes - - # Half-Width Katakanas - '\xef\xbd\xa1':'\xa1', # 。 - '\xef\xbd\xa2':'\xa2', # 「 - '\xef\xbd\xa3':'\xa3', # 」 - '\xef\xbd\xa4':'\xa4', # 、 - '\xef\xbd\xa5':'\xa5', # ・ - - '\xef\xbd\xa6':'\xa6', # ヲ - '\xef\xbd\xa7':'\xa7', # ァ - '\xef\xbd\xa8':'\xa8', # ィ - '\xef\xbd\xa9':'\xa9', # ゥ - '\xef\xbd\xaa':'\xaa', # ェ - '\xef\xbd\xab':'\xab', # ォ - '\xef\xbd\xac':'\xac', # ャ - '\xef\xbd\xad':'\xad', # ュ - '\xef\xbd\xae':'\xae', # ョ - '\xef\xbd\xaf':'\xaf', # ッ - '\xef\xbd\xb0':'\xb0', # ー - '\xef\xbd\xb1':'\xb1', # ア - '\xef\xbd\xb2':'\xb2', # イ - '\xef\xbd\xb3':'\xb3', # ウ - '\xef\xbd\xb4':'\xb4', # エ - '\xef\xbd\xb5':'\xb5', # オ - '\xef\xbd\xb6':'\xb6', # カ - '\xef\xbd\xb7':'\xb7', # キ - '\xef\xbd\xb8':'\xb8', # ク - '\xef\xbd\xb9':'\xb9', # ケ - '\xef\xbd\xba':'\xba', # コ - '\xef\xbd\xbb':'\xbb', # サ - '\xef\xbd\xbc':'\xbc', # シ - '\xef\xbd\xbd':'\xbd', # ス - '\xef\xbd\xbe':'\xbe', # セ - '\xef\xbd\xbf':'\xbf', # ソ - '\xef\xbe\x80':'\xc0', # タ - '\xef\xbe\x81':'\xc1', # チ - '\xef\xbe\x82':'\xc2', # ツ - '\xef\xbe\x83':'\xc3', # テ - '\xef\xbe\x84':'\xc4', # ト - '\xef\xbe\x85':'\xc5', # ナ - '\xef\xbe\x86':'\xc6', # ニ - '\xef\xbe\x87':'\xc7', # ヌ - '\xef\xbe\x88':'\xc8', # ネ - '\xef\xbe\x89':'\xc9', # ノ - '\xef\xbe\x8a':'\xca', # ハ - '\xef\xbe\x8b':'\xcb', # ヒ - '\xef\xbe\x8c':'\xcc', # フ - '\xef\xbe\x8d':'\xcd', # ヘ - '\xef\xbe\x8e':'\xce', # ホ - '\xef\xbe\x8f':'\xcf', # マ - '\xef\xbe\x90':'\xd0', # ミ - '\xef\xbe\x91':'\xd1', # ム - '\xef\xbe\x92':'\xd2', # メ - '\xef\xbe\x93':'\xd3', # モ - '\xef\xbe\x94':'\xd4', # ヤ - '\xef\xbe\x95':'\xd5', # ユ - '\xef\xbe\x96':'\xd6', # ヨ - '\xef\xbe\x97':'\xd7', # ラ - '\xef\xbe\x98':'\xd8', # リ - '\xef\xbe\x99':'\xd9', # ル - '\xef\xbe\x9a':'\xda', # レ - '\xef\xbe\x9b':'\xdb', # ロ - '\xef\xbe\x9c':'\xdc', # ワ - '\xef\xbe\x9d':'\xdd', # ン - - '\xef\xbe\x9e':'\xde', # ゙ - '\xef\xbe\x9f':'\xdf', # ゚ -} - -# Barcod format -BARCODE_TXT_OFF = '\x1d\x48\x00' # HRI barcode chars OFF -BARCODE_TXT_ABV = '\x1d\x48\x01' # HRI barcode chars above -BARCODE_TXT_BLW = '\x1d\x48\x02' # HRI barcode chars below -BARCODE_TXT_BTH = '\x1d\x48\x03' # HRI barcode chars both above and below -BARCODE_FONT_A = '\x1d\x66\x00' # Font type A for HRI barcode chars -BARCODE_FONT_B = '\x1d\x66\x01' # Font type B for HRI barcode chars -BARCODE_HEIGHT = '\x1d\x68\x64' # Barcode Height [1-255] -BARCODE_WIDTH = '\x1d\x77\x03' # Barcode Width [2-6] -BARCODE_UPC_A = '\x1d\x6b\x00' # Barcode type UPC-A -BARCODE_UPC_E = '\x1d\x6b\x01' # Barcode type UPC-E -BARCODE_EAN13 = '\x1d\x6b\x02' # Barcode type EAN13 -BARCODE_EAN8 = '\x1d\x6b\x03' # Barcode type EAN8 -BARCODE_CODE39 = '\x1d\x6b\x04' # Barcode type CODE39 -BARCODE_ITF = '\x1d\x6b\x05' # Barcode type ITF -BARCODE_NW7 = '\x1d\x6b\x06' # Barcode type NW7 -# Image format -S_RASTER_N = '\x1d\x76\x30\x00' # Set raster image normal size -S_RASTER_2W = '\x1d\x76\x30\x01' # Set raster image double width -S_RASTER_2H = '\x1d\x76\x30\x02' # Set raster image double height -S_RASTER_Q = '\x1d\x76\x30\x03' # Set raster image quadruple diff --git a/xmlescpos/escpos.py b/xmlescpos/escpos.py deleted file mode 100644 index ed9e57c..0000000 --- a/xmlescpos/escpos.py +++ /dev/null @@ -1,927 +0,0 @@ -# -*- coding: utf-8 -*- - -import time -import copy -import io -import base64 -import math -import md5 -import re -import traceback -import xml.etree.ElementTree as ET -import xml.dom.minidom as minidom - -from PIL import Image - -try: - import jcconv -except ImportError: - jcconv = None - -try: - import qrcode -except ImportError: - qrcode = None - -from constants import * -from exceptions import * - -def utfstr(stuff): - """ converts stuff to string and does without failing if stuff is a utf8 string """ - if isinstance(stuff,basestring): - return stuff - else: - return str(stuff) - -class StyleStack: - """ - The stylestack is used by the xml receipt serializer to compute the active styles along the xml - document. Styles are just xml attributes, there is no css mechanism. But the style applied by - the attributes are inherited by deeper nodes. - """ - def __init__(self): - self.stack = [] - self.defaults = { # default style values - 'align': 'left', - 'underline': 'off', - 'bold': 'off', - 'size': 'normal', - 'font' : 'a', - 'width': 48, - 'indent': 0, - 'tabwidth': 2, - 'bullet': ' - ', - 'line-ratio':0.5, - 'color': 'black', - - 'value-decimals': 2, - 'value-symbol': '', - 'value-symbol-position': 'after', - 'value-autoint': 'off', - 'value-decimals-separator': '.', - 'value-thousands-separator': ',', - 'value-width': 0, - - } - - self.types = { # attribute types, default is string and can be ommitted - 'width': 'int', - 'indent': 'int', - 'tabwidth': 'int', - 'line-ratio': 'float', - 'value-decimals': 'int', - 'value-width': 'int', - } - - self.cmds = { - # translation from styles to escpos commands - # some style do not correspond to escpos command are used by - # the serializer instead - 'align': { - 'left': TXT_ALIGN_LT, - 'right': TXT_ALIGN_RT, - 'center': TXT_ALIGN_CT, - '_order': 1, - }, - 'underline': { - 'off': TXT_UNDERL_OFF, - 'on': TXT_UNDERL_ON, - 'double': TXT_UNDERL2_ON, - # must be issued after 'size' command - # because ESC ! resets ESC - - '_order': 10, - }, - 'bold': { - 'off': TXT_BOLD_OFF, - 'on': TXT_BOLD_ON, - # must be issued after 'size' command - # because ESC ! resets ESC - - '_order': 10, - }, - 'font': { - 'a': TXT_FONT_A, - 'b': TXT_FONT_B, - # must be issued after 'size' command - # because ESC ! resets ESC - - '_order': 10, - }, - 'size': { - 'normal': TXT_NORMAL, - 'double-height': TXT_2HEIGHT, - 'double-width': TXT_2WIDTH, - 'double': TXT_DOUBLE, - '_order': 1, - }, - 'color': { - 'black': TXT_COLOR_BLACK, - 'red': TXT_COLOR_RED, - '_order': 1, - }, - } - - self.push(self.defaults) - - def get(self,style): - """ what's the value of a style at the current stack level""" - level = len(self.stack) -1 - while level >= 0: - if style in self.stack[level]: - return self.stack[level][style] - else: - level = level - 1 - return None - - def enforce_type(self, attr, val): - """converts a value to the attribute's type""" - if not attr in self.types: - return utfstr(val) - elif self.types[attr] == 'int': - return int(float(val)) - elif self.types[attr] == 'float': - return float(val) - else: - return utfstr(val) - - def push(self, style={}): - """push a new level on the stack with a style dictionnary containing style:value pairs""" - _style = {} - for attr in style: - if attr in self.cmds and not style[attr] in self.cmds[attr]: - print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) - else: - _style[attr] = self.enforce_type(attr, style[attr]) - self.stack.append(_style) - - def set(self, style={}): - """overrides style values at the current stack level""" - _style = {} - for attr in style: - if attr in self.cmds and not style[attr] in self.cmds[attr]: - print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) - else: - self.stack[-1][attr] = self.enforce_type(attr, style[attr]) - - def pop(self): - """ pop a style stack level """ - if len(self.stack) > 1 : - self.stack = self.stack[:-1] - - def to_escpos(self): - """ converts the current style to an escpos command string """ - cmd = '' - ordered_cmds = self.cmds.keys() - ordered_cmds.sort(lambda x,y: cmp(self.cmds[x]['_order'], self.cmds[y]['_order'])) - for style in ordered_cmds: - cmd += self.cmds[style][self.get(style)] - return cmd - -class XmlSerializer: - """ - Converts the xml inline / block tree structure to a string, - keeping track of newlines and spacings. - The string is outputted asap to the provided escpos driver. - """ - def __init__(self,escpos): - self.escpos = escpos - self.stack = ['block'] - self.dirty = False - - def start_inline(self,stylestack=None): - """ starts an inline entity with an optional style definition """ - self.stack.append('inline') - if self.dirty: - self.escpos._raw(' ') - if stylestack: - self.style(stylestack) - - def start_block(self,stylestack=None): - """ starts a block entity with an optional style definition """ - if self.dirty: - self.escpos._raw('\n') - self.dirty = False - self.stack.append('block') - if stylestack: - self.style(stylestack) - - def end_entity(self): - """ ends the entity definition. (but does not cancel the active style!) """ - if self.stack[-1] == 'block' and self.dirty: - self.escpos._raw('\n') - self.dirty = False - if len(self.stack) > 1: - self.stack = self.stack[:-1] - - def pre(self,text): - """ puts a string of text in the entity keeping the whitespace intact """ - if text: - self.escpos.text(text) - self.dirty = True - - def text(self,text): - """ puts text in the entity. Whitespace and newlines are stripped to single spaces. """ - if text: - text = utfstr(text) - text = text.strip() - text = re.sub('\s+',' ',text) - if text: - self.dirty = True - self.escpos.text(text) - - def linebreak(self): - """ inserts a linebreak in the entity """ - self.dirty = False - self.escpos._raw('\n') - - def style(self,stylestack): - """ apply a style to the entity (only applies to content added after the definition) """ - self.raw(stylestack.to_escpos()) - - def raw(self,raw): - """ puts raw text or escpos command in the entity without affecting the state of the serializer """ - self.escpos._raw(raw) - -class XmlLineSerializer: - """ - This is used to convert a xml tree into a single line, with a left and a right part. - The content is not output to escpos directly, and is intended to be fedback to the - XmlSerializer as the content of a block entity. - """ - def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5): - self.tabwidth = tabwidth - self.indent = indent - self.width = max(0, width - int(tabwidth*indent)) - self.lwidth = int(self.width*ratio) - self.rwidth = max(0, self.width - self.lwidth) - self.clwidth = 0 - self.crwidth = 0 - self.lbuffer = '' - self.rbuffer = '' - self.left = True - - def _txt(self,txt): - if self.left: - if self.clwidth < self.lwidth: - txt = txt[:max(0, self.lwidth - self.clwidth)] - self.lbuffer += txt - self.clwidth += len(txt) - else: - if self.crwidth < self.rwidth: - txt = txt[:max(0, self.rwidth - self.crwidth)] - self.rbuffer += txt - self.crwidth += len(txt) - - def start_inline(self,stylestack=None): - if (self.left and self.clwidth) or (not self.left and self.crwidth): - self._txt(' ') - - def start_block(self,stylestack=None): - self.start_inline(stylestack) - - def end_entity(self): - pass - - def pre(self,text): - if text: - self._txt(text) - def text(self,text): - if text: - text = utfstr(text) - text = text.strip() - text = re.sub('\s+',' ',text) - if text: - self._txt(text) - - def linebreak(self): - pass - def style(self,stylestack): - pass - def raw(self,raw): - pass - - def start_right(self): - self.left = False - - def get_line(self): - return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer - - -class Escpos: - """ ESC/POS Printer object """ - device = None - encoding = None - img_cache = {} - - def _check_image_size(self, size): - """ Check and fix the size of the image to 32 bits """ - if size % 32 == 0: - return (0, 0) - else: - image_border = 32 - (size % 32) - if (image_border % 2) == 0: - return (image_border / 2, image_border / 2) - else: - return (image_border / 2, (image_border / 2) + 1) - - def _print_image(self, line, size): - """ Print formatted image """ - i = 0 - cont = 0 - buffer = "" - - - self._raw(S_RASTER_N) - buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0) - self._raw(buffer.decode('hex')) - buffer = "" - - while i < len(line): - hex_string = int(line[i:i+8],2) - buffer += "%02X" % hex_string - i += 8 - cont += 1 - if cont % 4 == 0: - self._raw(buffer.decode("hex")) - buffer = "" - cont = 0 - - def _raw_print_image(self, line, size, output=None ): - """ Print formatted image """ - i = 0 - cont = 0 - buffer = "" - raw = "" - - def __raw(string): - if output: - output(string) - else: - self._raw(string) - - raw += S_RASTER_N - buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0) - raw += buffer.decode('hex') - buffer = "" - - while i < len(line): - hex_string = int(line[i:i+8],2) - buffer += "%02X" % hex_string - i += 8 - cont += 1 - if cont % 4 == 0: - raw += buffer.decode("hex") - buffer = "" - cont = 0 - - return raw - - def _convert_image(self, im): - """ Parse image and prepare it to a printable format """ - pixels = [] - pix_line = "" - im_left = "" - im_right = "" - switch = 0 - img_size = [ 0, 0 ] - - - if im.size[0] > 512: - print "WARNING: Image is wider than 512 and could be truncated at print time " - if im.size[1] > 255: - raise ImageSizeError() - - im_border = self._check_image_size(im.size[0]) - for i in range(im_border[0]): - im_left += "0" - for i in range(im_border[1]): - im_right += "0" - - for y in range(im.size[1]): - img_size[1] += 1 - pix_line += im_left - img_size[0] += im_border[0] - for x in range(im.size[0]): - img_size[0] += 1 - RGB = im.getpixel((x, y)) - im_color = (RGB[0] + RGB[1] + RGB[2]) - im_pattern = "1X0" - pattern_len = len(im_pattern) - switch = (switch - 1 ) * (-1) - for x in range(pattern_len): - if im_color <= (255 * 3 / pattern_len * (x+1)): - if im_pattern[x] == "X": - pix_line += "%d" % switch - else: - pix_line += im_pattern[x] - break - elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3): - pix_line += im_pattern[-1] - break - pix_line += im_right - img_size[0] += im_border[1] - - return (pix_line, img_size) - - def image(self,path_img): - """ Open image file """ - im_open = Image.open(path_img) - im = im_open.convert("RGB") - # Convert the RGB image in printable image - pix_line, img_size = self._convert_image(im) - self._print_image(pix_line, img_size) - - def print_base64_image(self,img): - - print 'print_b64_img' - - id = md5.new(img).digest() - - if id not in self.img_cache: - print 'not in cache' - - img = img[img.find(',')+1:] - f = io.BytesIO('img') - f.write(base64.decodestring(img)) - f.seek(0) - img_rgba = Image.open(f) - img = Image.new('RGB', img_rgba.size, (255,255,255)) - channels = img_rgba.split() - if len(channels) > 1: - # use alpha channel as mask - img.paste(img_rgba, mask=channels[3]) - else: - img.paste(img_rgba) - - print 'convert image' - - pix_line, img_size = self._convert_image(img) - - print 'print image' - - buffer = self._raw_print_image(pix_line, img_size) - self.img_cache[id] = buffer - - print 'raw image' - - self._raw(self.img_cache[id]) - - def qr(self,text): - """ Print QR Code for the provided string """ - qr_code = qrcode.QRCode(version=4, box_size=4, border=1) - qr_code.add_data(text) - qr_code.make(fit=True) - qr_img = qr_code.make_image() - im = qr_img._img.convert("RGB") - # Convert the RGB image in printable image - self._convert_image(im) - - def barcode(self, code, bc, width=255, height=2, pos='below', font='a'): - """ Print Barcode """ - # Align Bar Code() - self._raw(TXT_ALIGN_CT) - # Height - if height >=2 or height <=6: - self._raw(BARCODE_HEIGHT) - else: - raise BarcodeSizeError() - # Width - if width >= 1 or width <=255: - self._raw(BARCODE_WIDTH) - else: - raise BarcodeSizeError() - # Font - if font.upper() == "B": - self._raw(BARCODE_FONT_B) - else: # DEFAULT FONT: A - self._raw(BARCODE_FONT_A) - # Position - if pos.upper() == "OFF": - self._raw(BARCODE_TXT_OFF) - elif pos.upper() == "BOTH": - self._raw(BARCODE_TXT_BTH) - elif pos.upper() == "ABOVE": - self._raw(BARCODE_TXT_ABV) - else: # DEFAULT POSITION: BELOW - self._raw(BARCODE_TXT_BLW) - # Type - if bc.upper() == "UPC-A": - self._raw(BARCODE_UPC_A) - elif bc.upper() == "UPC-E": - self._raw(BARCODE_UPC_E) - elif bc.upper() == "EAN13": - self._raw(BARCODE_EAN13) - elif bc.upper() == "EAN8": - self._raw(BARCODE_EAN8) - elif bc.upper() == "CODE39": - self._raw(BARCODE_CODE39) - elif bc.upper() == "ITF": - self._raw(BARCODE_ITF) - elif bc.upper() == "NW7": - self._raw(BARCODE_NW7) - else: - raise BarcodeTypeError() - # Print Code - if code: - self._raw(code) - else: - raise exception.BarcodeCodeError() - - def receipt(self,xml): - """ - Prints an xml based receipt definition - """ - - def strclean(string): - if not string: - string = '' - string = string.strip() - string = re.sub('\s+',' ',string) - return string - - def format_value(value, decimals=3, width=0, decimals_separator='.', thousands_separator=',', autoint=False, symbol='', position='after'): - decimals = max(0,int(decimals)) - width = max(0,int(width)) - value = float(value) - - if autoint and math.floor(value) == value: - decimals = 0 - if width == 0: - width = '' - - if thousands_separator: - formatstr = "{:"+str(width)+",."+str(decimals)+"f}" - else: - formatstr = "{:"+str(width)+"."+str(decimals)+"f}" - - - ret = formatstr.format(value) - ret = ret.replace(',','COMMA') - ret = ret.replace('.','DOT') - ret = ret.replace('COMMA',thousands_separator) - ret = ret.replace('DOT',decimals_separator) - - if symbol: - if position == 'after': - ret = ret + symbol - else: - ret = symbol + ret - return ret - - def print_elem(stylestack, serializer, elem, indent=0): - - elem_styles = { - 'h1': {'bold': 'on', 'size':'double'}, - 'h2': {'size':'double'}, - 'h3': {'bold': 'on', 'size':'double-height'}, - 'h4': {'size': 'double-height'}, - 'h5': {'bold': 'on'}, - 'em': {'font': 'b'}, - 'b': {'bold': 'on'}, - } - - stylestack.push() - if elem.tag in elem_styles: - stylestack.set(elem_styles[elem.tag]) - stylestack.set(elem.attrib) - - if elem.tag in ('p','div','section','article','receipt','header','footer','li','h1','h2','h3','h4','h5'): - serializer.start_block(stylestack) - serializer.text(elem.text) - for child in elem: - print_elem(stylestack,serializer,child) - serializer.start_inline(stylestack) - serializer.text(child.tail) - serializer.end_entity() - serializer.end_entity() - - elif elem.tag in ('span','em','b','left','right'): - serializer.start_inline(stylestack) - serializer.text(elem.text) - for child in elem: - print_elem(stylestack,serializer,child) - serializer.start_inline(stylestack) - serializer.text(child.tail) - serializer.end_entity() - serializer.end_entity() - - elif elem.tag == 'value': - serializer.start_inline(stylestack) - serializer.pre(format_value( - elem.text, - decimals=stylestack.get('value-decimals'), - width=stylestack.get('value-width'), - decimals_separator=stylestack.get('value-decimals-separator'), - thousands_separator=stylestack.get('value-thousands-separator'), - autoint=(stylestack.get('value-autoint') == 'on'), - symbol=stylestack.get('value-symbol'), - position=stylestack.get('value-symbol-position') - )) - serializer.end_entity() - - elif elem.tag == 'line': - width = stylestack.get('width') - if stylestack.get('size') in ('double', 'double-width'): - width = width / 2 - - lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio')) - serializer.start_block(stylestack) - for child in elem: - if child.tag == 'left': - print_elem(stylestack,lineserializer,child,indent=indent) - elif child.tag == 'right': - lineserializer.start_right() - print_elem(stylestack,lineserializer,child,indent=indent) - serializer.pre(lineserializer.get_line()) - serializer.end_entity() - - elif elem.tag == 'ul': - serializer.start_block(stylestack) - bullet = stylestack.get('bullet') - for child in elem: - if child.tag == 'li': - serializer.style(stylestack) - serializer.raw(' ' * indent * stylestack.get('tabwidth') + bullet) - print_elem(stylestack,serializer,child,indent=indent+1) - serializer.end_entity() - - elif elem.tag == 'ol': - cwidth = len(str(len(elem))) + 2 - i = 1 - serializer.start_block(stylestack) - for child in elem: - if child.tag == 'li': - serializer.style(stylestack) - serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth)) - i = i + 1 - print_elem(stylestack,serializer,child,indent=indent+1) - serializer.end_entity() - - elif elem.tag == 'pre': - serializer.start_block(stylestack) - serializer.pre(elem.text) - serializer.end_entity() - - elif elem.tag == 'hr': - width = stylestack.get('width') - if stylestack.get('size') in ('double', 'double-width'): - width = width / 2 - serializer.start_block(stylestack) - serializer.text('-'*width) - serializer.end_entity() - - elif elem.tag == 'br': - serializer.linebreak() - - elif elem.tag == 'img': - if 'src' in elem.attrib and 'data:' in elem.attrib['src']: - self.print_base64_image(elem.attrib['src']) - - elif elem.tag == 'barcode' and 'encoding' in elem.attrib: - serializer.start_block(stylestack) - self.barcode(strclean(elem.text),elem.attrib['encoding']) - serializer.end_entity() - - elif elem.tag == 'cut': - self.cut() - elif elem.tag == 'partialcut': - self.cut(mode='part') - elif elem.tag == 'cashdraw': - self.cashdraw(2) - self.cashdraw(5) - - stylestack.pop() - - try: - stylestack = StyleStack() - serializer = XmlSerializer(self) - root = ET.fromstring(xml.encode('utf-8')) - if 'sheet' in root.attrib and root.attrib['sheet'] == 'slip': - self._raw(SHEET_SLIP_MODE) - self.slip_sheet_mode = True - else: - self._raw(SHEET_ROLL_MODE) - - self._raw(stylestack.to_escpos()) - - print_elem(stylestack,serializer,root) - - if 'open-cashdrawer' in root.attrib and root.attrib['open-cashdrawer'] == 'true': - self.cashdraw(2) - self.cashdraw(5) - if not 'cut' in root.attrib or root.attrib['cut'] == 'true' : - if self.slip_sheet_mode: - self._raw(CTL_FF) - else: - self.cut() - - except Exception as e: - errmsg = str(e)+'\n'+'-'*48+'\n'+traceback.format_exc() + '-'*48+'\n' - self.text(errmsg) - self.cut() - - raise e - - def text(self,txt): - """ Print Utf8 encoded alpha-numeric text """ - if not txt: - return - try: - txt = txt.decode('utf-8') - except: - try: - txt = txt.decode('utf-16') - except: - pass - - self.extra_chars = 0 - - def encode_char(char): - """ - Encodes a single utf-8 character into a sequence of - esc-pos code page change instructions and character declarations - """ - char_utf8 = char.encode('utf-8') - encoded = '' - encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character - encodings = { - # TODO use ordering to prevent useless switches - # TODO Support other encodings not natively supported by python ( Thai, Khazakh, Kanjis ) - 'cp437': TXT_ENC_PC437, - 'cp850': TXT_ENC_PC850, - 'cp852': TXT_ENC_PC852, - 'cp857': TXT_ENC_PC857, - 'cp858': TXT_ENC_PC858, - 'cp860': TXT_ENC_PC860, - 'cp863': TXT_ENC_PC863, - 'cp865': TXT_ENC_PC865, - 'cp866': TXT_ENC_PC866, - 'cp862': TXT_ENC_PC862, - 'cp720': TXT_ENC_PC720, - 'cp936': TXT_ENC_PC936, - 'iso8859_2': TXT_ENC_8859_2, - 'iso8859_7': TXT_ENC_8859_7, - 'iso8859_9': TXT_ENC_8859_9, - 'cp1254' : TXT_ENC_WPC1254, - 'cp1255' : TXT_ENC_WPC1255, - 'cp1256' : TXT_ENC_WPC1256, - 'cp1257' : TXT_ENC_WPC1257, - 'cp1258' : TXT_ENC_WPC1258, - 'katakana' : TXT_ENC_KATAKANA, - } - remaining = copy.copy(encodings) - - if not encoding : - encoding = 'cp437' - - while True: # Trying all encoding until one succeeds - try: - if encoding == 'katakana': # Japanese characters - if jcconv: - # try to convert japanese text to a half-katakanas - kata = jcconv.kata2half(jcconv.hira2kata(char_utf8)) - if kata != char_utf8: - self.extra_chars += len(kata.decode('utf-8')) - 1 - # the conversion may result in multiple characters - return encode_str(kata.decode('utf-8')) - else: - kata = char_utf8 - - if kata in TXT_ENC_KATAKANA_MAP: - encoded = TXT_ENC_KATAKANA_MAP[kata] - break - else: - raise ValueError() - else: - encoded = char.encode(encoding) - break - - except ValueError: #the encoding failed, select another one and retry - if encoding in remaining: - del remaining[encoding] - if len(remaining) >= 1: - encoding = remaining.items()[0][0] - else: - encoding = 'cp437' - encoded = '\xb1' # could not encode, output error character - break; - - if encoding != self.encoding: - # if the encoding changed, remember it and prefix the character with - # the esc-pos encoding change sequence - self.encoding = encoding - encoded = encodings[encoding] + encoded - - return encoded - - def encode_str(txt): - buffer = '' - for c in txt: - buffer += encode_char(c) - return buffer - - txt = encode_str(txt) - - # if the utf-8 -> codepage conversion inserted extra characters, - # remove double spaces to try to restore the original string length - # and prevent printing alignment issues - while self.extra_chars > 0: - dspace = txt.find(' ') - if dspace > 0: - txt = txt[:dspace] + txt[dspace+1:] - self.extra_chars -= 1 - else: - break - - self._raw(txt) - - def set(self, align='left', font='a', type='normal', width=1, height=1): - """ Set text properties """ - # Align - if align.upper() == "CENTER": - self._raw(TXT_ALIGN_CT) - elif align.upper() == "RIGHT": - self._raw(TXT_ALIGN_RT) - elif align.upper() == "LEFT": - self._raw(TXT_ALIGN_LT) - # Font - if font.upper() == "B": - self._raw(TXT_FONT_B) - else: # DEFAULT FONT: A - self._raw(TXT_FONT_A) - # Type - if type.upper() == "B": - self._raw(TXT_BOLD_ON) - self._raw(TXT_UNDERL_OFF) - elif type.upper() == "U": - self._raw(TXT_BOLD_OFF) - self._raw(TXT_UNDERL_ON) - elif type.upper() == "U2": - self._raw(TXT_BOLD_OFF) - self._raw(TXT_UNDERL2_ON) - elif type.upper() == "BU": - self._raw(TXT_BOLD_ON) - self._raw(TXT_UNDERL_ON) - elif type.upper() == "BU2": - self._raw(TXT_BOLD_ON) - self._raw(TXT_UNDERL2_ON) - elif type.upper == "NORMAL": - self._raw(TXT_BOLD_OFF) - self._raw(TXT_UNDERL_OFF) - # Width - if width == 2 and height != 2: - self._raw(TXT_NORMAL) - self._raw(TXT_2WIDTH) - elif height == 2 and width != 2: - self._raw(TXT_NORMAL) - self._raw(TXT_2HEIGHT) - elif height == 2 and width == 2: - self._raw(TXT_2WIDTH) - self._raw(TXT_2HEIGHT) - else: # DEFAULT SIZE: NORMAL - self._raw(TXT_NORMAL) - - - def cut(self, mode=''): - """ Cut paper """ - # Fix the size between last line and cut - # TODO: handle this with a line feed - self._raw("\n\n\n\n\n\n") - if mode.upper() == "PART": - self._raw(PAPER_PART_CUT) - else: # DEFAULT MODE: FULL CUT - self._raw(PAPER_FULL_CUT) - - - def cashdraw(self, pin): - """ Send pulse to kick the cash drawer """ - if pin == 2: - self._raw(CD_KICK_2) - elif pin == 5: - self._raw(CD_KICK_5) - else: - raise CashDrawerError() - - - def hw(self, hw): - """ Hardware operations """ - if hw.upper() == "INIT": - self._raw(HW_INIT) - elif hw.upper() == "SELECT": - self._raw(HW_SELECT) - elif hw.upper() == "RESET": - self._raw(HW_RESET) - else: # DEFAULT: DOES NOTHING - pass - - - def control(self, ctl): - """ Feed control sequences """ - if ctl.upper() == "LF": - self._raw(CTL_LF) - elif ctl.upper() == "FF": - self._raw(CTL_FF) - elif ctl.upper() == "CR": - self._raw(CTL_CR) - elif ctl.upper() == "HT": - self._raw(CTL_HT) - elif ctl.upper() == "VT": - self._raw(CTL_VT) diff --git a/xmlescpos/exceptions.py b/xmlescpos/exceptions.py deleted file mode 100644 index 51d860f..0000000 --- a/xmlescpos/exceptions.py +++ /dev/null @@ -1,116 +0,0 @@ -""" ESC/POS Exceptions classes """ - -import os - -class Error(Exception): - """ Base class for ESC/POS errors """ - def __init__(self, msg, status=None): - Exception.__init__(self) - self.msg = msg - self.resultcode = 1 - if status is not None: - self.resultcode = status - - def __str__(self): - return self.msg - -# Result/Exit codes -# 0 = success -# 10 = No Barcode type defined -# 20 = Barcode size values are out of range -# 30 = Barcode text not supplied -# 40 = Image height is too large -# 50 = No string supplied to be printed -# 60 = Invalid pin to send Cash Drawer pulse - - -class BarcodeTypeError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 10 - - def __str__(self): - return "No Barcode type is defined" - -class BarcodeSizeError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 20 - - def __str__(self): - return "Barcode size is out of range" - -class BarcodeCodeError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 30 - - def __str__(self): - return "Code was not supplied" - -class ImageSizeError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 40 - - def __str__(self): - return "Image height is longer than 255px and can't be printed" - -class TextError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 50 - - def __str__(self): - return "Text string must be supplied to the text() method" - - -class CashDrawerError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 60 - - def __str__(self): - return "Valid pin must be set to send pulse" - -class NoStatusError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 70 - - def __str__(self): - return "Impossible to get status from the printer" - -class TicketNotPrinted(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 80 - - def __str__(self): - return "A part of the ticket was not been printed" - -class NoDeviceError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 90 - - def __str__(self): - return "Impossible to find the printer Device" - -class HandleDeviceError(Error): - def __init__(self, msg=""): - Error.__init__(self, msg) - self.msg = msg - self.resultcode = 100 - - def __str__(self): - return "Impossible to handle device" diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py new file mode 100644 index 0000000..aa00fda --- /dev/null +++ b/xmlescpos/layout.py @@ -0,0 +1,547 @@ + # -*- coding: utf-8 -*- + +import time +import copy +import io +import base64 +import math +import md5 +import re +import traceback +import xml.etree.ElementTree as ET +import xml.dom.minidom as minidom + +from escpos.constants import * + + +# python-escpos is currently missing these - add them there +TXT_COLOR_BLACK = ESC + '\x72\x00' # Default Color +TXT_COLOR_RED = ESC + '\x72\x01' # Alternative Color ( Usually Red ) +SHEET_SLIP_MODE = ESC + '\x63\x30\x04' # Print ticket on injet slip paper +SHEET_ROLL_MODE = ESC + '\x63\x30\x01' # Print ticket on paper roll + + +def utfstr(stuff): + """ converts stuff to string and does without failing if stuff is a utf8 string """ + if isinstance(stuff,basestring): + return stuff + else: + return str(stuff) + + +class StyleStack: + """As we move through the the layout document, this keeps track of + the changing styles. We then can push the current desired styles + to the printer. + """ + def __init__(self): + self.stack = [] + self.defaults = { # default style values + 'align': 'left', + 'underline': 'off', + 'bold': 'off', + 'size': 'normal', + 'font' : 'a', + 'width': 48, + 'indent': 0, + 'tabwidth': 2, + 'bullet': ' - ', + 'line-ratio':0.5, + 'color': 'black', + + 'value-decimals': 2, + 'value-symbol': '', + 'value-symbol-position': 'after', + 'value-autoint': 'off', + 'value-decimals-separator': '.', + 'value-thousands-separator': ',', + 'value-width': 0, + } + + self.types = { # attribute types, default is string and can be ommitted + 'width': 'int', + 'indent': 'int', + 'tabwidth': 'int', + 'line-ratio': 'float', + 'value-decimals': 'int', + 'value-width': 'int', + } + + self.cmds = { + # translation from styles to escpos commands + # some style do not correspond to escpos command are used by + # the serializer instead + 'align': { + 'left': TXT_ALIGN_LT, + 'right': TXT_ALIGN_RT, + 'center': TXT_ALIGN_CT, + '_order': 1, + }, + 'underline': { + 'off': TXT_UNDERL_OFF, + 'on': TXT_UNDERL_ON, + 'double': TXT_UNDERL2_ON, + # must be issued after 'size' command + # because ESC ! resets ESC - + '_order': 10, + }, + 'bold': { + 'off': TXT_BOLD_OFF, + 'on': TXT_BOLD_ON, + # must be issued after 'size' command + # because ESC ! resets ESC - + '_order': 10, + }, + 'font': { + 'a': TXT_FONT_A, + 'b': TXT_FONT_B, + # must be issued after 'size' command + # because ESC ! resets ESC - + '_order': 10, + }, + 'size': { + 'normal': TXT_NORMAL, + 'double-height': TXT_2HEIGHT, + 'double-width': TXT_2WIDTH, + 'double': TXT_4SQUARE, + '_order': 1, + }, + 'color': { + 'black': TXT_COLOR_BLACK, + 'red': TXT_COLOR_RED, + '_order': 1, + } + } + + self.push(self.defaults) + + def get(self,style): + """ what's the value of a style at the current stack level""" + level = len(self.stack) -1 + while level >= 0: + if style in self.stack[level]: + return self.stack[level][style] + else: + level = level - 1 + return None + + def enforce_type(self, attr, val): + """converts a value to the attribute's type""" + if not attr in self.types: + return utfstr(val) + elif self.types[attr] == 'int': + return int(float(val)) + elif self.types[attr] == 'float': + return float(val) + else: + return utfstr(val) + + def push(self, style={}): + """push a new level on the stack with a style dictionnary containing style:value pairs""" + _style = {} + for attr in style: + if attr in self.cmds and not style[attr] in self.cmds[attr]: + print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) + else: + _style[attr] = self.enforce_type(attr, style[attr]) + self.stack.append(_style) + + def set(self, style={}): + """overrides style values at the current stack level""" + _style = {} + for attr in style: + if attr in self.cmds and not style[attr] in self.cmds[attr]: + print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) + else: + self.stack[-1][attr] = self.enforce_type(attr, style[attr]) + + def pop(self): + """ pop a style stack level """ + if len(self.stack) > 1 : + self.stack = self.stack[:-1] + + def to_escpos(self): + """ converts the current style to an escpos command string """ + cmd = '' + ordered_cmds = self.cmds.keys() + ordered_cmds.sort(lambda x,y: cmp(self.cmds[x]['_order'], self.cmds[y]['_order'])) + for style in ordered_cmds: + cmd += self.cmds[style][self.get(style)] + return cmd + + +class XmlSerializer: + """ + Converts the xml inline / block tree structure to a string, + keeping track of newlines and spacings. + The string is outputted asap to the provided escpos driver. + """ + + def __init__(self, printer): + self.printer = printer + self.stack = ['block'] + self.dirty = False + + def start_inline(self,stylestack=None): + """ starts an inline entity with an optional style definition """ + self.stack.append('inline') + if self.dirty: + self.printer._raw(' ') + if stylestack: + self.style(stylestack) + + def start_block(self,stylestack=None): + """ starts a block entity with an optional style definition """ + if self.dirty: + self.printer._raw('\n') + self.dirty = False + self.stack.append('block') + if stylestack: + self.style(stylestack) + + def end_entity(self): + """ ends the entity definition. (but does not cancel the active style!) """ + if self.stack[-1] == 'block' and self.dirty: + self.printer._raw('\n') + self.dirty = False + if len(self.stack) > 1: + self.stack = self.stack[:-1] + + def pre(self,text): + """ puts a string of text in the entity keeping the whitespace intact """ + if text: + self.printer.text(text) + self.dirty = True + + def text(self,text): + """ puts text in the entity. Whitespace and newlines are stripped to single spaces. """ + if text: + text = utfstr(text) + text = text.strip() + text = re.sub('\s+',' ',text) + if text: + self.dirty = True + self.printer.text(text) + + def linebreak(self): + """ inserts a linebreak in the entity """ + self.dirty = False + self.printer._raw('\n') + + def style(self,stylestack): + """ apply a style to the entity (only applies to content added after the definition) """ + self.printer._raw(stylestack.to_escpos()) + + def raw(self, raw): + self.printer._raw(raw) + + +class XmlLineSerializer: + """ + This is used to convert a xml tree into a single line, with a left and a right part. + The content is not output to escpos directly, and is intended to be fedback to the + XmlSerializer as the content of a block entity. + """ + def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5): + self.tabwidth = tabwidth + self.indent = indent + self.width = max(0, width - int(tabwidth*indent)) + self.lwidth = int(self.width*ratio) + self.rwidth = max(0, self.width - self.lwidth) + self.clwidth = 0 + self.crwidth = 0 + self.lbuffer = '' + self.rbuffer = '' + self.left = True + + def _txt(self,txt): + if self.left: + if self.clwidth < self.lwidth: + txt = txt[:max(0, self.lwidth - self.clwidth)] + self.lbuffer += txt + self.clwidth += len(txt) + else: + if self.crwidth < self.rwidth: + txt = txt[:max(0, self.rwidth - self.crwidth)] + self.rbuffer += txt + self.crwidth += len(txt) + + def start_inline(self,stylestack=None): + if (self.left and self.clwidth) or (not self.left and self.crwidth): + self._txt(' ') + + def start_block(self,stylestack=None): + self.start_inline(stylestack) + + def end_entity(self): + pass + + def pre(self,text): + if text: + self._txt(text) + def text(self,text): + if text: + text = utfstr(text) + text = text.strip() + text = re.sub('\s+',' ',text) + if text: + self._txt(text) + + def linebreak(self): + pass + def style(self,stylestack): + pass + def raw(self,raw): + pass + + def start_right(self): + self.left = False + + def get_line(self): + return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer + + +class Layout(object): + """Main class. Parses an XML layout. + + Convert to ESC/POS. Send to a pyton-escpos printer object. + + Usage:: + + from escpos import printer + epson = printer.Dummy() + Layout(xml).format(epson) + """ + + device = None + encoding = None + img_cache = {} + + def __init__(self, xml): + self._root = root = ET.fromstring(xml.encode('utf-8')) + + self.slip_sheet_mode = False + if 'sheet' in root.attrib and root.attrib['sheet'] == 'slip': + self.slip_sheet_mode = True + + self.open_crashdrawer = 'open-cashdrawer' in root.attrib and \ + root.attrib['open-cashdrawer'] == 'true' + + def print_base64_image(self,img): + + id = md5.new(img).digest() + + if id not in self.img_cache: + img = img[img.find(',')+1:] + f = io.BytesIO('img') + f.write(base64.decodestring(img)) + f.seek(0) + img_rgba = Image.open(f) + img = Image.new('RGB', img_rgba.size, (255,255,255)) + channels = img_rgba.split() + if len(channels) > 1: + # use alpha channel as mask + img.paste(img_rgba, mask=channels[3]) + else: + img.paste(img_rgba) + + pix_line, img_size = self._convert_image(img) + + buffer = self._raw_print_image(pix_line, img_size) + self.img_cache[id] = buffer + + self._raw(self.img_cache[id]) + + def print_elem(self, stylestack, serializer, elem, printer, indent=0): + """Recursively print an element in the document. + """ + + elem_styles = { + 'h1': {'bold': 'on', 'size':'double'}, + 'h2': {'size':'double'}, + 'h3': {'bold': 'on', 'size':'double-height'}, + 'h4': {'size': 'double-height'}, + 'h5': {'bold': 'on'}, + 'em': {'font': 'b'}, + 'b': {'bold': 'on'}, + } + + stylestack.push() + if elem.tag in elem_styles: + stylestack.set(elem_styles[elem.tag]) + stylestack.set(elem.attrib) + + if elem.tag in ('p','div','section','article','receipt','header','footer','li','h1','h2','h3','h4','h5'): + serializer.start_block(stylestack) + serializer.text(elem.text) + for child in elem: + self.print_elem(stylestack,serializer,child,printer) + serializer.start_inline(stylestack) + serializer.text(child.tail) + serializer.end_entity() + serializer.end_entity() + + elif elem.tag in ('span','em','b','left','right'): + serializer.start_inline(stylestack) + serializer.text(elem.text) + for child in elem: + self.print_elem(stylestack,serializer,child, printer) + serializer.start_inline(stylestack) + serializer.text(child.tail) + serializer.end_entity() + serializer.end_entity() + + elif elem.tag == 'value': + serializer.start_inline(stylestack) + serializer.pre(format_value( + elem.text, + decimals=stylestack.get('value-decimals'), + width=stylestack.get('value-width'), + decimals_separator=stylestack.get('value-decimals-separator'), + thousands_separator=stylestack.get('value-thousands-separator'), + autoint=(stylestack.get('value-autoint') == 'on'), + symbol=stylestack.get('value-symbol'), + position=stylestack.get('value-symbol-position') + )) + serializer.end_entity() + + elif elem.tag == 'line': + width = stylestack.get('width') + if stylestack.get('size') in ('double', 'double-width'): + width = width / 2 + + lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio')) + serializer.start_block(stylestack) + for child in elem: + if child.tag == 'left': + self.print_elem(stylestack,lineserializer,child,printer, indent=indent) + elif child.tag == 'right': + lineserializer.start_right() + self.print_elem(stylestack,lineserializer,child,printer, indent=indent) + serializer.pre(lineserializer.get_line()) + serializer.end_entity() + + elif elem.tag == 'ul': + serializer.start_block(stylestack) + bullet = stylestack.get('bullet') + for child in elem: + if child.tag == 'li': + serializer.style(stylestack) + serializer.raw(' ' * indent * stylestack.get('tabwidth') + bullet) + self.print_elem(stylestack,serializer,child,printer,indent=indent+1) + serializer.end_entity() + + elif elem.tag == 'ol': + cwidth = len(str(len(elem))) + 2 + i = 1 + serializer.start_block(stylestack) + for child in elem: + if child.tag == 'li': + serializer.style(stylestack) + serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth)) + i = i + 1 + self.print_elem(stylestack,serializer,child,printer,indent=indent+1) + serializer.end_entity() + + elif elem.tag == 'pre': + serializer.start_block(stylestack) + serializer.pre(elem.text) + serializer.end_entity() + + elif elem.tag == 'hr': + width = stylestack.get('width') + if stylestack.get('size') in ('double', 'double-width'): + width = width / 2 + serializer.start_block(stylestack) + serializer.text('-'*width) + serializer.end_entity() + + elif elem.tag == 'br': + serializer.linebreak() + + elif elem.tag == 'img': + if 'src' in elem.attrib and 'data:' in elem.attrib['src']: + self.print_base64_image(elem.attrib['src']) + + elif elem.tag == 'barcode' and 'encoding' in elem.attrib: + serializer.start_block(stylestack) + printer.barcode(strclean(elem.text),elem.attrib['encoding']) + serializer.end_entity() + + elif elem.tag == 'cut': + printer.cut() + elif elem.tag == 'partialcut': + printer.cut(mode='part') + elif elem.tag == 'cashdraw': + printer.cashdraw(2) + printer.cashdraw(5) + + stylestack.pop() + + def format(self, printer): + """Format the layout to print on the given printer driver. + """ + + stylestack = StyleStack() + serializer = XmlSerializer(printer) + root = self._root + + # Init the mode + if self.slip_sheet_mode: + printer._raw(SHEET_SLIP_MODE) + else: + printer._raw(SHEET_ROLL_MODE) + + # init tye styles + printer._raw(stylestack.to_escpos()) + + # Print the root element + self.print_elem(stylestack, serializer, self._root, printer) + + # Finalize print actions: cut paper, open cashdrawer + if self.open_crashdrawer: + self.cashdraw(2) + self.cashdraw(5) + + if not 'cut' in root.attrib or root.attrib['cut'] == 'true' : + if self.slip_sheet_mode: + printer._raw(CTL_FF) + else: + printer.cut() + + +def strclean(string): + if not string: + string = '' + string = string.strip() + string = re.sub('\s+',' ',string) + return string + + +def format_value(value, decimals=3, width=0, decimals_separator='.', thousands_separator=',', autoint=False, symbol='', position='after'): + decimals = max(0,int(decimals)) + width = max(0,int(width)) + value = float(value) + + if autoint and math.floor(value) == value: + decimals = 0 + if width == 0: + width = '' + + if thousands_separator: + formatstr = "{:"+str(width)+",."+str(decimals)+"f}" + else: + formatstr = "{:"+str(width)+"."+str(decimals)+"f}" + + ret = formatstr.format(value) + ret = ret.replace(',','COMMA') + ret = ret.replace('.','DOT') + ret = ret.replace('COMMA',thousands_separator) + ret = ret.replace('DOT',decimals_separator) + + if symbol: + if position == 'after': + ret = ret + symbol + else: + ret = symbol + ret + return ret \ No newline at end of file diff --git a/xmlescpos/printer.py b/xmlescpos/printer.py deleted file mode 100644 index f57d438..0000000 --- a/xmlescpos/printer.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/python - -import usb.core -import usb.util -import serial -import socket - -from escpos import * -from constants import * -from exceptions import * -from time import sleep - -class Usb(Escpos): - """ Define USB printer """ - - def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01): - """ - @param idVendor : Vendor ID - @param idProduct : Product ID - @param interface : USB device interface - @param in_ep : Input end point - @param out_ep : Output end point - """ - - self.errorText = "ERROR PRINTER\n\n\n\n\n\n"+PAPER_FULL_CUT - - self.idVendor = idVendor - self.idProduct = idProduct - self.interface = interface - self.in_ep = in_ep - self.out_ep = out_ep - self.open() - - def open(self): - """ Search device on USB tree and set is as escpos device """ - - self.device = usb.core.find(idVendor=self.idVendor, idProduct=self.idProduct) - if self.device is None: - raise NoDeviceError() - try: - if self.device.is_kernel_driver_active(self.interface): - self.device.detach_kernel_driver(self.interface) - self.device.set_configuration() - usb.util.claim_interface(self.device, self.interface) - except usb.core.USBError as e: - raise HandleDeviceError(e) - - def close(self): - i = 0 - while True: - try: - if not self.device.is_kernel_driver_active(self.interface): - usb.util.release_interface(self.device, self.interface) - self.device.attach_kernel_driver(self.interface) - usb.util.dispose_resources(self.device) - else: - self.device = None - return True - except usb.core.USBError as e: - i += 1 - if i > 100: - return False - - sleep(0.1) - - def _raw(self, msg): - """ Print any command sent in raw format """ - if len(msg) != self.device.write(self.out_ep, msg, self.interface): - self.device.write(self.out_ep, self.errorText, self.interface) - raise TicketNotPrinted() - - def __extract_status(self): - maxiterate = 0 - rep = None - while rep == None: - maxiterate += 1 - if maxiterate > 10000: - raise NoStatusError() - r = self.device.read(self.in_ep, 20, self.interface).tolist() - while len(r): - rep = r.pop() - return rep - - def get_printer_status(self): - status = { - 'printer': {}, - 'offline': {}, - 'error' : {}, - 'paper' : {}, - } - - self.device.write(self.out_ep, DLE_EOT_PRINTER, self.interface) - printer = self.__extract_status() - self.device.write(self.out_ep, DLE_EOT_OFFLINE, self.interface) - offline = self.__extract_status() - self.device.write(self.out_ep, DLE_EOT_ERROR, self.interface) - error = self.__extract_status() - self.device.write(self.out_ep, DLE_EOT_PAPER, self.interface) - paper = self.__extract_status() - - status['printer']['status_code'] = printer - status['printer']['status_error'] = not ((printer & 147) == 18) - status['printer']['online'] = not bool(printer & 8) - status['printer']['recovery'] = bool(printer & 32) - status['printer']['paper_feed_on'] = bool(printer & 64) - status['printer']['drawer_pin_high'] = bool(printer & 4) - status['offline']['status_code'] = offline - status['offline']['status_error'] = not ((offline & 147) == 18) - status['offline']['cover_open'] = bool(offline & 4) - status['offline']['paper_feed_on'] = bool(offline & 8) - status['offline']['paper'] = not bool(offline & 32) - status['offline']['error'] = bool(offline & 64) - status['error']['status_code'] = error - status['error']['status_error'] = not ((error & 147) == 18) - status['error']['recoverable'] = bool(error & 4) - status['error']['autocutter'] = bool(error & 8) - status['error']['unrecoverable'] = bool(error & 32) - status['error']['auto_recoverable'] = not bool(error & 64) - status['paper']['status_code'] = paper - status['paper']['status_error'] = not ((paper & 147) == 18) - status['paper']['near_end'] = bool(paper & 12) - status['paper']['present'] = not bool(paper & 96) - - return status - - def __del__(self): - """ Release USB interface """ - if self.device: - self.close() - self.device = None - - - -class Serial(Escpos): - """ Define Serial printer """ - - def __init__(self, devfile="/dev/ttyS0", baudrate=9600, bytesize=8, timeout=1): - """ - @param devfile : Device file under dev filesystem - @param baudrate : Baud rate for serial transmission - @param bytesize : Serial buffer size - @param timeout : Read/Write timeout - """ - self.devfile = devfile - self.baudrate = baudrate - self.bytesize = bytesize - self.timeout = timeout - self.open() - - - def open(self): - """ Setup serial port and set is as escpos device """ - self.device = serial.Serial(port=self.devfile, baudrate=self.baudrate, bytesize=self.bytesize, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=self.timeout, dsrdtr=True) - - if self.device is not None: - print "Serial printer enabled" - else: - print "Unable to open serial printer on: %s" % self.devfile - - - def _raw(self, msg): - """ Print any command sent in raw format """ - self.device.write(msg) - - - def __del__(self): - """ Close Serial interface """ - if self.device is not None: - self.device.close() - - - -class Network(Escpos): - """ Define Network printer """ - - def __init__(self,host,port=9100): - """ - @param host : Printer's hostname or IP address - @param port : Port to write to - """ - self.host = host - self.port = port - self.open() - - - def open(self): - """ Open TCP socket and set it as escpos device """ - self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.device.connect((self.host, self.port)) - - if self.device is None: - print "Could not open socket for %s" % self.host - - - def _raw(self, msg): - self.device.send(msg) - - - def __del__(self): - """ Close TCP connection """ - self.device.close() - diff --git a/xmlescpos/supported_devices.py b/xmlescpos/supported_devices.py deleted file mode 100644 index bd08433..0000000 --- a/xmlescpos/supported_devices.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/python - -# This is a list of esc/pos compatible usb printers. The vendor and product ids can be found by -# typing lsusb in a linux terminal, this will give you the ids in the form ID VENDOR:PRODUCT - -device_list = [ - { 'vendor' : 0x04b8, 'product' : 0x0e03, 'name' : 'Epson TM-T20' }, - { 'vendor' : 0x04b8, 'product' : 0x0202, 'name' : 'Epson TM-T70' }, - { 'vendor' : 0x04b8, 'product' : 0x0e15, 'name' : 'Epson TM-T20II' }, -] - From 2c4ee969ecfc4cf89437de037aa8b465d11e42ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 24 Aug 2016 15:08:54 +0200 Subject: [PATCH 02/16] Run the code through autopep8. --- xmlescpos/layout.py | 310 ++++++++++++++++++++++++++------------------ 1 file changed, 186 insertions(+), 124 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index aa00fda..9bca5f1 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -1,4 +1,4 @@ - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- import time import copy @@ -15,15 +15,15 @@ # python-escpos is currently missing these - add them there -TXT_COLOR_BLACK = ESC + '\x72\x00' # Default Color -TXT_COLOR_RED = ESC + '\x72\x01' # Alternative Color ( Usually Red ) -SHEET_SLIP_MODE = ESC + '\x63\x30\x04' # Print ticket on injet slip paper -SHEET_ROLL_MODE = ESC + '\x63\x30\x01' # Print ticket on paper roll +TXT_COLOR_BLACK = ESC + '\x72\x00' # Default Color +TXT_COLOR_RED = ESC + '\x72\x01' # Alternative Color ( Usually Red ) +SHEET_SLIP_MODE = ESC + '\x63\x30\x04' # Print ticket on injet slip paper +SHEET_ROLL_MODE = ESC + '\x63\x30\x01' # Print ticket on paper roll def utfstr(stuff): """ converts stuff to string and does without failing if stuff is a utf8 string """ - if isinstance(stuff,basestring): + if isinstance(stuff, basestring): return stuff else: return str(stuff) @@ -34,37 +34,38 @@ class StyleStack: the changing styles. We then can push the current desired styles to the printer. """ + def __init__(self): self.stack = [] self.defaults = { # default style values - 'align': 'left', + 'align': 'left', 'underline': 'off', - 'bold': 'off', - 'size': 'normal', - 'font' : 'a', - 'width': 48, - 'indent': 0, - 'tabwidth': 2, - 'bullet': ' - ', - 'line-ratio':0.5, - 'color': 'black', - - 'value-decimals': 2, - 'value-symbol': '', - 'value-symbol-position': 'after', - 'value-autoint': 'off', - 'value-decimals-separator': '.', + 'bold': 'off', + 'size': 'normal', + 'font': 'a', + 'width': 48, + 'indent': 0, + 'tabwidth': 2, + 'bullet': ' - ', + 'line-ratio': 0.5, + 'color': 'black', + + 'value-decimals': 2, + 'value-symbol': '', + 'value-symbol-position': 'after', + 'value-autoint': 'off', + 'value-decimals-separator': '.', 'value-thousands-separator': ',', - 'value-width': 0, + 'value-width': 0, } - self.types = { # attribute types, default is string and can be ommitted - 'width': 'int', - 'indent': 'int', + self.types = { # attribute types, default is string and can be ommitted + 'width': 'int', + 'indent': 'int', 'tabwidth': 'int', - 'line-ratio': 'float', - 'value-decimals': 'int', - 'value-width': 'int', + 'line-ratio': 'float', + 'value-decimals': 'int', + 'value-width': 'int', } self.cmds = { @@ -72,52 +73,52 @@ def __init__(self): # some style do not correspond to escpos command are used by # the serializer instead 'align': { - 'left': TXT_ALIGN_LT, - 'right': TXT_ALIGN_RT, - 'center': TXT_ALIGN_CT, - '_order': 1, + 'left': TXT_ALIGN_LT, + 'right': TXT_ALIGN_RT, + 'center': TXT_ALIGN_CT, + '_order': 1, }, 'underline': { - 'off': TXT_UNDERL_OFF, - 'on': TXT_UNDERL_ON, - 'double': TXT_UNDERL2_ON, + 'off': TXT_UNDERL_OFF, + 'on': TXT_UNDERL_ON, + 'double': TXT_UNDERL2_ON, # must be issued after 'size' command # because ESC ! resets ESC - - '_order': 10, + '_order': 10, }, 'bold': { - 'off': TXT_BOLD_OFF, - 'on': TXT_BOLD_ON, + 'off': TXT_BOLD_OFF, + 'on': TXT_BOLD_ON, # must be issued after 'size' command # because ESC ! resets ESC - - '_order': 10, + '_order': 10, }, 'font': { - 'a': TXT_FONT_A, - 'b': TXT_FONT_B, + 'a': TXT_FONT_A, + 'b': TXT_FONT_B, # must be issued after 'size' command # because ESC ! resets ESC - - '_order': 10, + '_order': 10, }, 'size': { - 'normal': TXT_NORMAL, - 'double-height': TXT_2HEIGHT, - 'double-width': TXT_2WIDTH, - 'double': TXT_4SQUARE, - '_order': 1, + 'normal': TXT_NORMAL, + 'double-height': TXT_2HEIGHT, + 'double-width': TXT_2WIDTH, + 'double': TXT_4SQUARE, + '_order': 1, }, 'color': { - 'black': TXT_COLOR_BLACK, - 'red': TXT_COLOR_RED, - '_order': 1, + 'black': TXT_COLOR_BLACK, + 'red': TXT_COLOR_RED, + '_order': 1, } } self.push(self.defaults) - def get(self,style): + def get(self, style): """ what's the value of a style at the current stack level""" - level = len(self.stack) -1 + level = len(self.stack) - 1 while level >= 0: if style in self.stack[level]: return self.stack[level][style] @@ -127,7 +128,7 @@ def get(self,style): def enforce_type(self, attr, val): """converts a value to the attribute's type""" - if not attr in self.types: + if attr not in self.types: return utfstr(val) elif self.types[attr] == 'int': return int(float(val)) @@ -141,7 +142,7 @@ def push(self, style={}): _style = {} for attr in style: if attr in self.cmds and not style[attr] in self.cmds[attr]: - print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) + print 'WARNING: ESC/POS PRINTING: ignoring invalid value: ' + utfstr(style[attr]) + ' for style: ' + utfstr(attr) else: _style[attr] = self.enforce_type(attr, style[attr]) self.stack.append(_style) @@ -151,20 +152,24 @@ def set(self, style={}): _style = {} for attr in style: if attr in self.cmds and not style[attr] in self.cmds[attr]: - print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) + print 'WARNING: ESC/POS PRINTING: ignoring invalid value: ' + utfstr(style[attr]) + ' for style: ' + utfstr(attr) else: self.stack[-1][attr] = self.enforce_type(attr, style[attr]) def pop(self): """ pop a style stack level """ - if len(self.stack) > 1 : + if len(self.stack) > 1: self.stack = self.stack[:-1] def to_escpos(self): """ converts the current style to an escpos command string """ cmd = '' ordered_cmds = self.cmds.keys() - ordered_cmds.sort(lambda x,y: cmp(self.cmds[x]['_order'], self.cmds[y]['_order'])) + ordered_cmds.sort( + lambda x, + y: cmp( + self.cmds[x]['_order'], + self.cmds[y]['_order'])) for style in ordered_cmds: cmd += self.cmds[style][self.get(style)] return cmd @@ -182,7 +187,7 @@ def __init__(self, printer): self.stack = ['block'] self.dirty = False - def start_inline(self,stylestack=None): + def start_inline(self, stylestack=None): """ starts an inline entity with an optional style definition """ self.stack.append('inline') if self.dirty: @@ -190,7 +195,7 @@ def start_inline(self,stylestack=None): if stylestack: self.style(stylestack) - def start_block(self,stylestack=None): + def start_block(self, stylestack=None): """ starts a block entity with an optional style definition """ if self.dirty: self.printer._raw('\n') @@ -207,18 +212,18 @@ def end_entity(self): if len(self.stack) > 1: self.stack = self.stack[:-1] - def pre(self,text): + def pre(self, text): """ puts a string of text in the entity keeping the whitespace intact """ if text: self.printer.text(text) self.dirty = True - def text(self,text): + def text(self, text): """ puts text in the entity. Whitespace and newlines are stripped to single spaces. """ if text: text = utfstr(text) text = text.strip() - text = re.sub('\s+',' ',text) + text = re.sub('\s+', ' ', text) if text: self.dirty = True self.printer.text(text) @@ -228,7 +233,7 @@ def linebreak(self): self.dirty = False self.printer._raw('\n') - def style(self,stylestack): + def style(self, stylestack): """ apply a style to the entity (only applies to content added after the definition) """ self.printer._raw(stylestack.to_escpos()) @@ -242,19 +247,20 @@ class XmlLineSerializer: The content is not output to escpos directly, and is intended to be fedback to the XmlSerializer as the content of a block entity. """ + def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5): self.tabwidth = tabwidth self.indent = indent - self.width = max(0, width - int(tabwidth*indent)) - self.lwidth = int(self.width*ratio) + self.width = max(0, width - int(tabwidth * indent)) + self.lwidth = int(self.width * ratio) self.rwidth = max(0, self.width - self.lwidth) self.clwidth = 0 self.crwidth = 0 - self.lbuffer = '' - self.rbuffer = '' - self.left = True + self.lbuffer = '' + self.rbuffer = '' + self.left = True - def _txt(self,txt): + def _txt(self, txt): if self.left: if self.clwidth < self.lwidth: txt = txt[:max(0, self.lwidth - self.clwidth)] @@ -264,41 +270,45 @@ def _txt(self,txt): if self.crwidth < self.rwidth: txt = txt[:max(0, self.rwidth - self.crwidth)] self.rbuffer += txt - self.crwidth += len(txt) + self.crwidth += len(txt) - def start_inline(self,stylestack=None): + def start_inline(self, stylestack=None): if (self.left and self.clwidth) or (not self.left and self.crwidth): self._txt(' ') - def start_block(self,stylestack=None): + def start_block(self, stylestack=None): self.start_inline(stylestack) def end_entity(self): pass - def pre(self,text): + def pre(self, text): if text: self._txt(text) - def text(self,text): + + def text(self, text): if text: text = utfstr(text) text = text.strip() - text = re.sub('\s+',' ',text) + text = re.sub('\s+', ' ', text) if text: self._txt(text) def linebreak(self): pass - def style(self,stylestack): + + def style(self, stylestack): pass - def raw(self,raw): + + def raw(self, raw): pass def start_right(self): self.left = False def get_line(self): - return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer + return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * \ + (self.width - self.clwidth - self.crwidth) + self.rbuffer class Layout(object): @@ -313,8 +323,8 @@ class Layout(object): Layout(xml).format(epson) """ - device = None - encoding = None + device = None + encoding = None img_cache = {} def __init__(self, xml): @@ -327,17 +337,17 @@ def __init__(self, xml): self.open_crashdrawer = 'open-cashdrawer' in root.attrib and \ root.attrib['open-cashdrawer'] == 'true' - def print_base64_image(self,img): + def print_base64_image(self, img): id = md5.new(img).digest() if id not in self.img_cache: - img = img[img.find(',')+1:] + img = img[img.find(',') + 1:] f = io.BytesIO('img') f.write(base64.decodestring(img)) f.seek(0) img_rgba = Image.open(f) - img = Image.new('RGB', img_rgba.size, (255,255,255)) + img = Image.new('RGB', img_rgba.size, (255, 255, 255)) channels = img_rgba.split() if len(channels) > 1: # use alpha channel as mask @@ -357,13 +367,13 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): """ elem_styles = { - 'h1': {'bold': 'on', 'size':'double'}, - 'h2': {'size':'double'}, - 'h3': {'bold': 'on', 'size':'double-height'}, + 'h1': {'bold': 'on', 'size': 'double'}, + 'h2': {'size': 'double'}, + 'h3': {'bold': 'on', 'size': 'double-height'}, 'h4': {'size': 'double-height'}, 'h5': {'bold': 'on'}, 'em': {'font': 'b'}, - 'b': {'bold': 'on'}, + 'b': {'bold': 'on'}, } stylestack.push() @@ -371,21 +381,34 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): stylestack.set(elem_styles[elem.tag]) stylestack.set(elem.attrib) - if elem.tag in ('p','div','section','article','receipt','header','footer','li','h1','h2','h3','h4','h5'): + if elem.tag in ( + 'p', + 'div', + 'section', + 'article', + 'receipt', + 'header', + 'footer', + 'li', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5'): serializer.start_block(stylestack) serializer.text(elem.text) for child in elem: - self.print_elem(stylestack,serializer,child,printer) + self.print_elem(stylestack, serializer, child, printer) serializer.start_inline(stylestack) serializer.text(child.tail) serializer.end_entity() serializer.end_entity() - elif elem.tag in ('span','em','b','left','right'): + elif elem.tag in ('span', 'em', 'b', 'left', 'right'): serializer.start_inline(stylestack) serializer.text(elem.text) for child in elem: - self.print_elem(stylestack,serializer,child, printer) + self.print_elem(stylestack, serializer, child, printer) serializer.start_inline(stylestack) serializer.text(child.tail) serializer.end_entity() @@ -393,16 +416,17 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): elif elem.tag == 'value': serializer.start_inline(stylestack) - serializer.pre(format_value( - elem.text, - decimals=stylestack.get('value-decimals'), - width=stylestack.get('value-width'), - decimals_separator=stylestack.get('value-decimals-separator'), - thousands_separator=stylestack.get('value-thousands-separator'), - autoint=(stylestack.get('value-autoint') == 'on'), - symbol=stylestack.get('value-symbol'), - position=stylestack.get('value-symbol-position') - )) + serializer.pre( + format_value( + elem.text, + decimals=stylestack.get('value-decimals'), + width=stylestack.get('value-width'), + decimals_separator=stylestack.get('value-decimals-separator'), + thousands_separator=stylestack.get('value-thousands-separator'), + autoint=( + stylestack.get('value-autoint') == 'on'), + symbol=stylestack.get('value-symbol'), + position=stylestack.get('value-symbol-position'))) serializer.end_entity() elif elem.tag == 'line': @@ -410,14 +434,28 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): if stylestack.get('size') in ('double', 'double-width'): width = width / 2 - lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio')) + lineserializer = XmlLineSerializer( + stylestack.get('indent') + indent, + stylestack.get('tabwidth'), + width, + stylestack.get('line-ratio')) serializer.start_block(stylestack) for child in elem: if child.tag == 'left': - self.print_elem(stylestack,lineserializer,child,printer, indent=indent) + self.print_elem( + stylestack, + lineserializer, + child, + printer, + indent=indent) elif child.tag == 'right': lineserializer.start_right() - self.print_elem(stylestack,lineserializer,child,printer, indent=indent) + self.print_elem( + stylestack, + lineserializer, + child, + printer, + indent=indent) serializer.pre(lineserializer.get_line()) serializer.end_entity() @@ -427,8 +465,14 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): for child in elem: if child.tag == 'li': serializer.style(stylestack) - serializer.raw(' ' * indent * stylestack.get('tabwidth') + bullet) - self.print_elem(stylestack,serializer,child,printer,indent=indent+1) + serializer.raw( + ' ' * indent * stylestack.get('tabwidth') + bullet) + self.print_elem( + stylestack, + serializer, + child, + printer, + indent=indent + 1) serializer.end_entity() elif elem.tag == 'ol': @@ -438,9 +482,19 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): for child in elem: if child.tag == 'li': serializer.style(stylestack) - serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth)) + serializer.raw(' ' * + indent * + stylestack.get('tabwidth') + + ' ' + + (str(i) + + ')').ljust(cwidth)) i = i + 1 - self.print_elem(stylestack,serializer,child,printer,indent=indent+1) + self.print_elem( + stylestack, + serializer, + child, + printer, + indent=indent + 1) serializer.end_entity() elif elem.tag == 'pre': @@ -453,7 +507,7 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): if stylestack.get('size') in ('double', 'double-width'): width = width / 2 serializer.start_block(stylestack) - serializer.text('-'*width) + serializer.text('-' * width) serializer.end_entity() elif elem.tag == 'br': @@ -465,7 +519,7 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): elif elem.tag == 'barcode' and 'encoding' in elem.attrib: serializer.start_block(stylestack) - printer.barcode(strclean(elem.text),elem.attrib['encoding']) + printer.barcode(strclean(elem.text), elem.attrib['encoding']) serializer.end_entity() elif elem.tag == 'cut': @@ -503,7 +557,7 @@ def format(self, printer): self.cashdraw(2) self.cashdraw(5) - if not 'cut' in root.attrib or root.attrib['cut'] == 'true' : + if not 'cut' in root.attrib or root.attrib['cut'] == 'true': if self.slip_sheet_mode: printer._raw(CTL_FF) else: @@ -514,14 +568,22 @@ def strclean(string): if not string: string = '' string = string.strip() - string = re.sub('\s+',' ',string) + string = re.sub('\s+', ' ', string) return string -def format_value(value, decimals=3, width=0, decimals_separator='.', thousands_separator=',', autoint=False, symbol='', position='after'): - decimals = max(0,int(decimals)) - width = max(0,int(width)) - value = float(value) +def format_value( + value, + decimals=3, + width=0, + decimals_separator='.', + thousands_separator=',', + autoint=False, + symbol='', + position='after'): + decimals = max(0, int(decimals)) + width = max(0, int(width)) + value = float(value) if autoint and math.floor(value) == value: decimals = 0 @@ -529,19 +591,19 @@ def format_value(value, decimals=3, width=0, decimals_separator='.', thousands_s width = '' if thousands_separator: - formatstr = "{:"+str(width)+",."+str(decimals)+"f}" + formatstr = "{:" + str(width) + ",." + str(decimals) + "f}" else: - formatstr = "{:"+str(width)+"."+str(decimals)+"f}" + formatstr = "{:" + str(width) + "." + str(decimals) + "f}" ret = formatstr.format(value) - ret = ret.replace(',','COMMA') - ret = ret.replace('.','DOT') - ret = ret.replace('COMMA',thousands_separator) - ret = ret.replace('DOT',decimals_separator) + ret = ret.replace(',', 'COMMA') + ret = ret.replace('.', 'DOT') + ret = ret.replace('COMMA', thousands_separator) + ret = ret.replace('DOT', decimals_separator) if symbol: if position == 'after': ret = ret + symbol else: ret = symbol + ret - return ret \ No newline at end of file + return ret From d392ed6ba9ebf0c844198ae5cc1f8a6667c6a566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 24 Aug 2016 15:11:05 +0200 Subject: [PATCH 03/16] Disable automatic cutting. Calling printer.cut() is simple enough. At the core, this should focus on formatting, not control. --- xmlescpos/layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index 9bca5f1..755a216 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -557,7 +557,7 @@ def format(self, printer): self.cashdraw(2) self.cashdraw(5) - if not 'cut' in root.attrib or root.attrib['cut'] == 'true': + if 'cut' in root.attrib and root.attrib['cut'] == 'true': if self.slip_sheet_mode: printer._raw(CTL_FF) else: From a75edbfb0bcef4fec92b300c5bad1eadfc28e634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 24 Aug 2016 15:19:46 +0200 Subject: [PATCH 04/16] Removed some unused imports, minor cleanup. --- xmlescpos/layout.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index 755a216..7d6c683 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -1,15 +1,11 @@ # -*- coding: utf-8 -*- -import time -import copy import io import base64 import math import md5 import re -import traceback import xml.etree.ElementTree as ET -import xml.dom.minidom as minidom from escpos.constants import * @@ -323,8 +319,6 @@ class Layout(object): Layout(xml).format(epson) """ - device = None - encoding = None img_cache = {} def __init__(self, xml): @@ -524,8 +518,10 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): elif elem.tag == 'cut': printer.cut() + elif elem.tag == 'partialcut': printer.cut(mode='part') + elif elem.tag == 'cashdraw': printer.cashdraw(2) printer.cashdraw(5) From 707d5412d5809de2ba70f2d81216853a85a69af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 24 Aug 2016 15:20:00 +0200 Subject: [PATCH 05/16] Fix image rendering. --- xmlescpos/layout.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index 7d6c683..d75a563 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -6,6 +6,7 @@ import md5 import re import xml.etree.ElementTree as ET +from PIL import Image from escpos.constants import * @@ -331,8 +332,7 @@ def __init__(self, xml): self.open_crashdrawer = 'open-cashdrawer' in root.attrib and \ root.attrib['open-cashdrawer'] == 'true' - def print_base64_image(self, img): - + def get_base64_image(self, img): id = md5.new(img).digest() if id not in self.img_cache: @@ -343,18 +343,16 @@ def print_base64_image(self, img): img_rgba = Image.open(f) img = Image.new('RGB', img_rgba.size, (255, 255, 255)) channels = img_rgba.split() + if len(channels) > 1: # use alpha channel as mask img.paste(img_rgba, mask=channels[3]) else: img.paste(img_rgba) - pix_line, img_size = self._convert_image(img) - - buffer = self._raw_print_image(pix_line, img_size) - self.img_cache[id] = buffer + self.img_cache[id] = img - self._raw(self.img_cache[id]) + return self.img_cache[id] def print_elem(self, stylestack, serializer, elem, printer, indent=0): """Recursively print an element in the document. @@ -509,7 +507,7 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): elif elem.tag == 'img': if 'src' in elem.attrib and 'data:' in elem.attrib['src']: - self.print_base64_image(elem.attrib['src']) + printer.image(self.get_base64_image(elem.attrib['src'])) elif elem.tag == 'barcode' and 'encoding' in elem.attrib: serializer.start_block(stylestack) From 9417cc9027036fd9bef6ef0bd5d2f0cfbb12556e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Fri, 26 Aug 2016 10:03:29 +0200 Subject: [PATCH 06/16] Add some TODOs. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 3afd6a1..c91ab6a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,16 @@ It uses python-escpos internally. So to print it, you'd do: Layout(xml).format(epson) +## Ideas for future support: + +- Make the tag much more powerful; support text wrapping in + the columns; different styles on each side. + +- Support borders. + +- Be closer to real HTML, i.e. have a
tag. could + have an auto-ratio mode, and become flexbox. + ## Install sudo pip install pyxmlescpos From ab52884c3276ebcb2287c26e7da0b117a5d56111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Fri, 26 Aug 2016 10:04:01 +0200 Subject: [PATCH 07/16] Move ESC constants to python-escpos project. --- xmlescpos/layout.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index d75a563..d7f5072 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -11,13 +11,6 @@ from escpos.constants import * -# python-escpos is currently missing these - add them there -TXT_COLOR_BLACK = ESC + '\x72\x00' # Default Color -TXT_COLOR_RED = ESC + '\x72\x01' # Alternative Color ( Usually Red ) -SHEET_SLIP_MODE = ESC + '\x63\x30\x04' # Print ticket on injet slip paper -SHEET_ROLL_MODE = ESC + '\x63\x30\x01' # Print ticket on paper roll - - def utfstr(stuff): """ converts stuff to string and does without failing if stuff is a utf8 string """ if isinstance(stuff, basestring): From 9f3aa84249ba908534c3ba49d59ca6729728f613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Fri, 26 Aug 2016 10:04:27 +0200 Subject: [PATCH 08/16] Support width=auto using the printer profile. --- xmlescpos/layout.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index d7f5072..1e92297 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -23,9 +23,13 @@ class StyleStack: """As we move through the the layout document, this keeps track of the changing styles. We then can push the current desired styles to the printer. + + The "width" has a special "auto" value, which will read the + column width for the current font from the printer profile. """ - def __init__(self): + def __init__(self, profile): + self.profile = profile self.stack = [] self.defaults = { # default style values 'align': 'left', @@ -33,7 +37,7 @@ def __init__(self): 'bold': 'off', 'size': 'normal', 'font': 'a', - 'width': 48, + 'width': 'auto', 'indent': 0, 'tabwidth': 2, 'bullet': ' - ', @@ -50,7 +54,7 @@ def __init__(self): } self.types = { # attribute types, default is string and can be ommitted - 'width': 'int', + 'width': lambda v: v if v == 'auto' else int(v), 'indent': 'int', 'tabwidth': 'int', 'line-ratio': 'float', @@ -106,7 +110,7 @@ def __init__(self): self.push(self.defaults) - def get(self, style): + def _get(self, style): """ what's the value of a style at the current stack level""" level = len(self.stack) - 1 while level >= 0: @@ -116,6 +120,16 @@ def get(self, style): level = level - 1 return None + def get(self, style): + value = self._get(style) + + if style == 'width' and value == 'auto': + font = self._get('font') + return self.profile.get_columns(font) + + return value + + def enforce_type(self, attr, val): """converts a value to the attribute's type""" if attr not in self.types: @@ -124,6 +138,8 @@ def enforce_type(self, attr, val): return int(float(val)) elif self.types[attr] == 'float': return float(val) + elif callable(self.types[attr]): + return self.types[attr](val) else: return utfstr(val) @@ -523,7 +539,7 @@ def format(self, printer): """Format the layout to print on the given printer driver. """ - stylestack = StyleStack() + stylestack = StyleStack(printer.profile) serializer = XmlSerializer(printer) root = self._root From fe3ebe46d83ce8979192059bae6c1d8aa10fe776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 31 Aug 2016 10:08:03 +0200 Subject: [PATCH 09/16] Use special dash to format
. --- xmlescpos/layout.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index 1e92297..c8a4fb2 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -101,11 +101,11 @@ def __init__(self, profile): 'double': TXT_4SQUARE, '_order': 1, }, - 'color': { - 'black': TXT_COLOR_BLACK, - 'red': TXT_COLOR_RED, - '_order': 1, - } + # 'color': { + # 'black': 'TXT_COLOR_BLACK', + # 'red': 'TXT_COLOR_RED', + # '_order': 1, + # } } self.push(self.defaults) @@ -508,7 +508,7 @@ def print_elem(self, stylestack, serializer, elem, printer, indent=0): if stylestack.get('size') in ('double', 'double-width'): width = width / 2 serializer.start_block(stylestack) - serializer.text('-' * width) + serializer.text(u'─' * width) serializer.end_entity() elif elem.tag == 'br': @@ -544,10 +544,10 @@ def format(self, printer): root = self._root # Init the mode - if self.slip_sheet_mode: - printer._raw(SHEET_SLIP_MODE) - else: - printer._raw(SHEET_ROLL_MODE) + # if self.slip_sheet_mode: + # printer._raw(SHEET_SLIP_MODE) + # else: + # printer._raw(SHEET_ROLL_MODE) # init tye styles printer._raw(stylestack.to_escpos()) From 04f5746908ca3e411f7408fa702bd1601925795a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 31 Aug 2016 10:11:33 +0200 Subject: [PATCH 10/16] Uncomment temporarily disabled code. --- xmlescpos/layout.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index c8a4fb2..bb55432 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -101,11 +101,11 @@ def __init__(self, profile): 'double': TXT_4SQUARE, '_order': 1, }, - # 'color': { - # 'black': 'TXT_COLOR_BLACK', - # 'red': 'TXT_COLOR_RED', - # '_order': 1, - # } + 'color': { + 'black': 'TXT_COLOR_BLACK', + 'red': 'TXT_COLOR_RED', + '_order': 1, + } } self.push(self.defaults) @@ -544,10 +544,10 @@ def format(self, printer): root = self._root # Init the mode - # if self.slip_sheet_mode: - # printer._raw(SHEET_SLIP_MODE) - # else: - # printer._raw(SHEET_ROLL_MODE) + if self.slip_sheet_mode: + printer._raw(SHEET_SLIP_MODE) + else: + printer._raw(SHEET_ROLL_MODE) # init tye styles printer._raw(stylestack.to_escpos()) From 82d316950b888fdb02dd2a480f1f47622634a35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Sun, 4 Sep 2016 13:07:52 +0200 Subject: [PATCH 11/16] TXT_COLOR constants were strings. --- xmlescpos/layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index bb55432..7fd1d24 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -102,8 +102,8 @@ def __init__(self, profile): '_order': 1, }, 'color': { - 'black': 'TXT_COLOR_BLACK', - 'red': 'TXT_COLOR_RED', + 'black': TXT_COLOR_BLACK, + 'red': TXT_COLOR_RED, '_order': 1, } } From 656954b0a54362ccc78aec96038751111092aa9d Mon Sep 17 00:00:00 2001 From: Dmytro Katyukha Date: Fri, 3 Feb 2017 16:40:29 +0000 Subject: [PATCH 12/16] Bugfixed format meth. Change print statements to logger calls --- .gitignore | 8 ++++++++ xmlescpos/layout.py | 27 +++++++++++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28c0c42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.pyc +*.pyo + +*.swp +*.swn +*.swo + +pyxmlescpos.egg-info/ diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index 7fd1d24..c1d6772 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -10,6 +10,9 @@ from escpos.constants import * +import logging +_logger = logging.getLogger(__name__) + def utfstr(stuff): """ converts stuff to string and does without failing if stuff is a utf8 string """ @@ -148,7 +151,7 @@ def push(self, style={}): _style = {} for attr in style: if attr in self.cmds and not style[attr] in self.cmds[attr]: - print 'WARNING: ESC/POS PRINTING: ignoring invalid value: ' + utfstr(style[attr]) + ' for style: ' + utfstr(attr) + _logger.warn('WARNING: ESC/POS PRINTING: ignoring invalid value: ' + utfstr(style[attr]) + ' for style: ' + utfstr(attr)) else: _style[attr] = self.enforce_type(attr, style[attr]) self.stack.append(_style) @@ -158,7 +161,7 @@ def set(self, style={}): _style = {} for attr in style: if attr in self.cmds and not style[attr] in self.cmds[attr]: - print 'WARNING: ESC/POS PRINTING: ignoring invalid value: ' + utfstr(style[attr]) + ' for style: ' + utfstr(attr) + _logger.warn('WARNING: ESC/POS PRINTING: ignoring invalid value: ' + utfstr(style[attr]) + ' for style: ' + utfstr(attr)) else: self.stack[-1][attr] = self.enforce_type(attr, style[attr]) @@ -350,16 +353,16 @@ def get_base64_image(self, img): f.write(base64.decodestring(img)) f.seek(0) img_rgba = Image.open(f) - img = Image.new('RGB', img_rgba.size, (255, 255, 255)) - channels = img_rgba.split() + #img = Image.new('RGB', img_rgba.size, (255, 255, 255)) + #channels = img_rgba.split() - if len(channels) > 1: - # use alpha channel as mask - img.paste(img_rgba, mask=channels[3]) - else: - img.paste(img_rgba) + #if len(channels) > 1: + ## use alpha channel as mask + #img.paste(img_rgba, mask=channels[3]) + #else: + #img.paste(img_rgba) - self.img_cache[id] = img + self.img_cache[id] = img_rgba return self.img_cache[id] @@ -557,8 +560,8 @@ def format(self, printer): # Finalize print actions: cut paper, open cashdrawer if self.open_crashdrawer: - self.cashdraw(2) - self.cashdraw(5) + self.printer.cashdraw(2) + self.printer.cashdraw(5) if 'cut' in root.attrib and root.attrib['cut'] == 'true': if self.slip_sheet_mode: From 5c0e0e9f609a3720dc8cea39ae55b6bfb8182a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Sat, 14 Oct 2017 17:29:02 +0100 Subject: [PATCH 13/16] modernize for Python 3. --- setup.py | 2 +- xmlescpos/layout.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 9166b09..9795b55 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ # project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/technical.html#install-requires-vs-requirements-files - install_requires=['python-escpos'], + install_requires=['python-escpos', 'six'], # List additional groups of dependencies here (e.g. development dependencies). # You can install these using the following syntax, for example: diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index c1d6772..afc39f2 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import io import base64 import math -import md5 +import hashlib import re import xml.etree.ElementTree as ET from PIL import Image @@ -11,12 +12,13 @@ from escpos.constants import * import logging +import six _logger = logging.getLogger(__name__) def utfstr(stuff): """ converts stuff to string and does without failing if stuff is a utf8 string """ - if isinstance(stuff, basestring): + if isinstance(stuff, six.string_types): return stuff else: return str(stuff) @@ -173,7 +175,7 @@ def pop(self): def to_escpos(self): """ converts the current style to an escpos command string """ cmd = '' - ordered_cmds = self.cmds.keys() + ordered_cmds = list(self.cmds.keys()) ordered_cmds.sort( lambda x, y: cmp( @@ -345,7 +347,7 @@ def __init__(self, xml): root.attrib['open-cashdrawer'] == 'true' def get_base64_image(self, img): - id = md5.new(img).digest() + id = hashlib.md5(img).hexdigest() if id not in self.img_cache: img = img[img.find(',') + 1:] From 6d80c036a3e87c6d179b417d69dad089382461eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Sat, 14 Oct 2017 18:06:55 +0100 Subject: [PATCH 14/16] Inc version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9795b55..bbf38ac 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/development.html#single-sourcing-the-version - version='0.1.0', + version='0.2.0', description='Print XML-defined Receipts on ESC/POS Receipt Printers', long_description=long_description, From fe640785e6e103ed1de957f72037dc99a6c28fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Fri, 20 Oct 2017 16:08:09 +0100 Subject: [PATCH 15/16] Some Python 3 fixes. --- xmlescpos/layout.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/xmlescpos/layout.py b/xmlescpos/layout.py index afc39f2..5f7be77 100644 --- a/xmlescpos/layout.py +++ b/xmlescpos/layout.py @@ -174,13 +174,10 @@ def pop(self): def to_escpos(self): """ converts the current style to an escpos command string """ - cmd = '' + cmd = b'' ordered_cmds = list(self.cmds.keys()) ordered_cmds.sort( - lambda x, - y: cmp( - self.cmds[x]['_order'], - self.cmds[y]['_order'])) + key=lambda x: self.cmds[x]['_order']) for style in ordered_cmds: cmd += self.cmds[style][self.get(style)] return cmd @@ -202,14 +199,14 @@ def start_inline(self, stylestack=None): """ starts an inline entity with an optional style definition """ self.stack.append('inline') if self.dirty: - self.printer._raw(' ') + self.printer._raw(b' ') if stylestack: self.style(stylestack) def start_block(self, stylestack=None): """ starts a block entity with an optional style definition """ if self.dirty: - self.printer._raw('\n') + self.printer._raw(b'\n') self.dirty = False self.stack.append('block') if stylestack: @@ -218,7 +215,7 @@ def start_block(self, stylestack=None): def end_entity(self): """ ends the entity definition. (but does not cancel the active style!) """ if self.stack[-1] == 'block' and self.dirty: - self.printer._raw('\n') + self.printer._raw(b'\n') self.dirty = False if len(self.stack) > 1: self.stack = self.stack[:-1] @@ -242,7 +239,7 @@ def text(self, text): def linebreak(self): """ inserts a linebreak in the entity """ self.dirty = False - self.printer._raw('\n') + self.printer._raw(b'\n') def style(self, stylestack): """ apply a style to the entity (only applies to content added after the definition) """ From 382d3f1016dfe9bde4b5e083663c7e8a6a436d79 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Fri, 13 Jul 2018 11:20:17 -0500 Subject: [PATCH 16/16] Update install instruction --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c91ab6a..9f90ce8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ It uses python-escpos internally. So to print it, you'd do: ## Install - sudo pip install pyxmlescpos + sudo pip install git+https://github.com/miracle2k/py-xml-escpos.git@0.2.0 # Documentation