├── DESCRIPTION.rst ├── LICENSE.txt ├── README.md ├── setup.cfg ├── setup.py ├── test_printer.py └── xmlescpos ├── __init__.py ├── constants.py ├── escpos.py ├── exceptions.py ├── printer.py └── supported_devices.py /DESCRIPTION.rst: -------------------------------------------------------------------------------- 1 | 2 | XML-ESC/POS 3 | =========== 4 | 5 | XML-ESC/POS is a simple library that allows you to 6 | interact with ESC/POS devices with a simple utf8 7 | encoded xml format similar to HTML. The following 8 | example is self-explanatory: 9 | 10 | .. code:: xml 11 | 12 |

Receipt!

13 |

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

14 | 15 | Product 16 | 0.15€ 17 | 18 |
19 | 20 | TOTAL 21 | 0.15€ 22 | 23 | 24 | 5449000000996 25 | 26 | 27 | 28 |
29 | 30 | .. code:: python 31 | from xmlescpos.printer import Usb 32 | printer = Usb(0x04b8,0x0e03) 33 | printer.receipt("
Hello World!
") 34 | 35 | Limitations 36 | ----------- 37 | The utf8 support is incomplete, mostly asian languages 38 | are not working since they are badly documented and 39 | only supported by region-specific hardware. 40 | 41 | This is also the very first release, which is a simple 42 | extraction from the Odoo code base. While it works well, 43 | it needs some cleanup for public use. 44 | 45 | Also, the doc is non-existent. 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Frederic van der Essen & Manuel F. Martinez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # XML-ESC/POS 3 | 4 | XML-ESC/POS is a simple python library that allows you to 5 | print receipts on ESC/POS Compatible receipt printers with a simple utf8 6 | encoded XML format similar to HTML. Barcode, pictures, 7 | text encoding are automatically handled. No more dicking 8 | around with esc-pos commands ! 9 | 10 | The following example is self-explanatory: 11 | 12 | 13 |

Receipt!

14 |

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

:w 15 | 16 | Product 17 | 0.15€ 18 | 19 |
20 | 21 | TOTAL 22 | 0.15€ 23 | 24 | 25 | 5449000000996 26 | 27 | 28 | 29 |
30 | 31 | And printing from python is quite easy, you just 32 | need the USB product / vendor id of your printer. 33 | Some common ids are found in `supported_devices.py` 34 | 35 | from xmlescpos.printer import Usb 36 | printer = Usb(0x04b8,0x0e03) 37 | printer.receipt("
Hello World!
") 38 | 39 | ## Install 40 | 41 | sudo pip install pyxmlescpos 42 | 43 | ## Limitations 44 | 45 | The utf8 support is incomplete, mostly asian languages 46 | are not working. Documentation is hard to find, support relies on region-specific hardware, etc. There is some very basic 47 | support for Japanese. 48 | 49 | # Documentation 50 | ## XML Structure 51 | The library prints receipts defined by utf-8 encoded XML 52 | documents. The tags and structure of the document are in 53 | many ways similar to HTML. The two main differences between 54 | XML-ESC/POS and HTML, is the presence of ESC/POS specific 55 | tags, and the lack of CSS. Oh, and it is XML based, so you *must* provide 56 | valid XML, or you'll get a traceback on your receipt. 57 | 58 | The styling is done with custom attributes on the elements 59 | themselves. The styling is inherited by child elements. 60 | 61 | ## Supported HTML Tags 62 | ### Inline Tags 63 | - `span`,`em`,`b` 64 | 65 | ### Block level Tags 66 | - `p`,`div`,`section`,`article`,`header`,`footer`,`li`,`h1-5`,`hr` 67 | 68 | ### List tags 69 | - `ul`,`ol` 70 | 71 | The indentation width is determined by the `tabwidth` 72 | attribute, which specifies the indentation in number of white 73 | space characters. `tabwidth` is inherited by child elements, 74 | like all styling attributes. 75 | 76 | `ul` elements also support the `bullet` attribute which specifies 77 | the character used to represent bullets. 78 | 79 | 83 | 84 | ### Image Tags 85 | The `img` tag prints the picture specified by the `src` attribute. 86 | The `src` attribute must contain the picture encoded in png, gif 87 | or jpeg in a base64 data-url. 88 | 89 | 90 | 91 | ## Esc/Pos Specific tags 92 | - `cut`, cuts the recipt 93 | - `partialcut`, partially cuts the receipt 94 | - `cashdraw`, activates the cash-drawer connected to the printer 95 | 96 | ### Barcode Tags 97 | It is possible to include barcodes in your receipt with the `barcode 98 | tag`. The `encoding` attribute lets you specify the barcode encoding 99 | used, and its presence is mandatory. The following encodings are 100 | supported: `UPC-A`,`UPC-E`,`EAN13`,`EAN8`,`CODE39`,`ITF`,`NW7`. 101 | 102 | 103 | 5400113509509 104 | 105 | 106 | ### Line Tag 107 | The `line` tag is used to quickly layout receipt lines. Its child elements 108 | must have either the `left` or `right` tag, and will be aligned left or right 109 | on the same line. There is a hard limit between the left and right part of the 110 | line, and content overflow is hidden. The placement of the limit is given 111 | by the `line-ratio` attribute, which is the ratio of the left part's width with 112 | the total width of the line. A ratio of 0.5 ( the default ) thus divides the line in two 113 | equal parts. 114 | 115 | 116 | Product Name 117 | $0.15 118 | 119 | 120 | ### Value Tag 121 | The `value` tag is used to format numerical values. It allows to specify 122 | the number of digits, the decimal and thousands separator independently of 123 | the formatting of the provided number. The following attributes are supported: 124 | 125 | - `value-decimals` : The number of decimals 126 | - `value-width` : The number will be left-padded with zeroes until its 127 | formatting has that many characters. 128 | - `value-decimals-separator` : The character used to seprate the decimal and 129 | integer part of the number. 130 | - `value-thousands-separator` : The character used to separate thousands. 131 | - `value-autoint` : The number will not print decimals if it is an integer 132 | - `value-symbol` : The unit symbol will be placed before or after the number 133 | - `value-symbol-position` : `before` or `after` 134 | 135 | 136 | 137 | 138 | 3.1415 139 | 140 | 141 | Those attributes are inherited, and can thus be specified once and for all 142 | on the receipt's root element. 143 | 144 | ## Styling Attributes 145 | 146 | The following attributes are used to style the elements. They are inherited and can be applied to 147 | any element. 148 | 149 | - `align`: `left`,`right` or `center`. Specifies the text alignment. 150 | - `underline`: `off` or `on` or `double` 151 | - `bold`: `off` or `on` 152 | - `size`: `normal`,`double`,`double-height`,`double-width`, the font size. 153 | - `font`: `a` or `b`, the font used (two fonts ought to be enough ... ) 154 | - `width`: the width of block level elements, in characters. (default 48) 155 | - `indent`: The indentation (in tabs) before a block level element. 156 | - `tabwidth`: The number of spaces in a single indentation level (default 2) 157 | - `color` : `black` or `red` 158 | 159 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages # Always prefer setuptools over distutils 5 | from codecs import open # To use a consistent encoding 6 | from os import path 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | 10 | # Get the long description from the relevant file 11 | with open(path.join(here, 'DESCRIPTION.rst'), encoding='utf-8') as f: 12 | long_description = f.read() 13 | 14 | setup( 15 | name='pyxmlescpos', 16 | 17 | # Versions should comply with PEP440. For a discussion on single-sourcing 18 | # the version across setup.py and the project code, see 19 | # https://packaging.python.org/en/latest/development.html#single-sourcing-the-version 20 | version='0.1.0', 21 | 22 | description='Print XML-defined Receipts on ESC/POS Receipt Printers', 23 | long_description=long_description, 24 | 25 | # The project's main homepage. 26 | url='https://github.com/fvdsn/py-xml-escpos', 27 | download_url = 'https://github.com/fvdsn/py-xml-escpos/tarball/0.1.0', 28 | 29 | # Author details 30 | author='Frédéric van der Essen & Manuel F Martinez', 31 | author_email='fvdessen+x@gmail.com', 32 | 33 | # Choose your license 34 | license='MIT', 35 | 36 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 37 | classifiers=[ 38 | # How mature is this project? Common values are 39 | # 3 - Alpha 40 | # 4 - Beta 41 | # 5 - Production/Stable 42 | 'Development Status :: 3 - Alpha', 43 | 44 | # Indicate who your project is intended for 45 | 'Intended Audience :: Developers', 46 | 'Topic :: Printing', 47 | 48 | # Pick your license as you wish (should match "license" above) 49 | 'License :: OSI Approved :: MIT License', 50 | 51 | # Specify the Python versions you support here. In particular, ensure 52 | # that you indicate whether you support Python 2, Python 3 or both. 53 | 'Programming Language :: Python :: 2', 54 | 'Programming Language :: Python :: 2.6', 55 | 'Programming Language :: Python :: 2.7', 56 | ], 57 | 58 | # What does your project relate to? 59 | keywords='printing receipt xml escpos', 60 | 61 | # You can just specify the packages manually here if your project is 62 | # simple. Or you can use find_packages(). 63 | packages=find_packages(exclude=['contrib', 'docs', 'tests*']), 64 | 65 | # List run-time dependencies here. These will be installed by pip when your 66 | # project is installed. For an analysis of "install_requires" vs pip's 67 | # requirements files see: 68 | # https://packaging.python.org/en/latest/technical.html#install-requires-vs-requirements-files 69 | install_requires=['pyusb'], 70 | 71 | # List additional groups of dependencies here (e.g. development dependencies). 72 | # You can install these using the following syntax, for example: 73 | # $ pip install -e .[dev,test] 74 | # extras_require = { 75 | # 'dev': ['check-manifest'], 76 | # 'test': ['coverage'], 77 | # }, 78 | 79 | # If there are data files included in your packages that need to be 80 | # installed, specify them here. If using Python 2.6 or less, then these 81 | # have to be included in MANIFEST.in as well. 82 | # package_data={ 83 | # 'sample': ['package_data.dat'], 84 | # }, 85 | 86 | # Although 'package_data' is the preferred approach, in some case you may 87 | # need to place data files outside of your packages. 88 | # see http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files 89 | # In this case, 'data_file' will be installed into '/my_data' 90 | # data_files=[('my_data', ['data/data_file'])], 91 | 92 | # To provide executable scripts, use entry points in preference to the 93 | # "scripts" keyword. Entry points provide cross-platform support and allow 94 | # pip to create the appropriate form of executable for the target platform. 95 | # entry_points={ 96 | # 'console_scripts': [ 97 | # 'sample=sample:main', 98 | # ], 99 | # }, 100 | ) 101 | -------------------------------------------------------------------------------- /test_printer.py: -------------------------------------------------------------------------------- 1 | test_temp = """ 2 | 3 |

Receipt!

4 |

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

:w 5 | 6 | Product 7 | 0.15 8 | 9 |
10 | 11 | TOTAL 12 | 0.15 13 | 14 | 15 | 5449000000996 16 | 17 | 18 | 19 |
20 | """ 21 | from xmlescpos.exceptions import * 22 | from xmlescpos.printer import Usb 23 | import usb 24 | import pprint 25 | import sys 26 | 27 | pp = pprint.PrettyPrinter(indent=4) 28 | 29 | try: 30 | printer = Usb(0x04b8,0x0202) 31 | 32 | printer._raw('\x1D\x28\x47\x02\x00\x30\x04'); 33 | printer._raw('AAAA'); 34 | printer._raw('\x0c'); 35 | 36 | printer._raw('\x1c\x61\x31'); 37 | printer._raw('BBBB'); 38 | printer._raw('\x0c'); 39 | 40 | printer._raw('\x1d\x28\x47\x02\x00\x50\x04'); 41 | printer._raw('\x1D\x28\x47\x02\x00\x30\x04'); 42 | printer._raw('\x1D\x28\x47\x02\x00\x54\x00'); 43 | printer._raw('CCCC'); 44 | printer._raw('\x1D\x28\x47\x02\x00\x54\x01'); 45 | 46 | #printer.receipt(test_temp) 47 | pp.pprint(printer.get_printer_status()) 48 | 49 | except NoDeviceError as e: 50 | print "No device found %s" %str(e) 51 | except HandleDeviceError as e: 52 | print "Impossible to handle the device due to previous error %s" % str(e) 53 | except TicketNotPrinted as e: 54 | print "The ticket does not seems to have been fully printed %s" % str(e) 55 | except NoStatusError as e: 56 | print "Impossible to get the status of the printer %s" % str(e) 57 | finally: 58 | printer.close() 59 | 60 | -------------------------------------------------------------------------------- /xmlescpos/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["constants","escpos","exceptions","printer","supported_devices"] 2 | -------------------------------------------------------------------------------- /xmlescpos/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ ESC/POS Commands (Constants) """ 4 | 5 | # Feed control sequences 6 | CTL_LF = '\x0a' # Print and line feed 7 | CTL_FF = '\x0c' # Form feed 8 | CTL_CR = '\x0d' # Carriage return 9 | CTL_HT = '\x09' # Horizontal tab 10 | CTL_VT = '\x0b' # Vertical tab 11 | 12 | # RT Status commands 13 | DLE_EOT_PRINTER = '\x10\x04\x01' # Transmit printer status 14 | DLE_EOT_OFFLINE = '\x10\x04\x02' 15 | DLE_EOT_ERROR = '\x10\x04\x03' 16 | DLE_EOT_PAPER = '\x10\x04\x04' 17 | 18 | # Printer hardware 19 | HW_INIT = '\x1b\x40' # Clear data in buffer and reset modes 20 | HW_SELECT = '\x1b\x3d\x01' # Printer select 21 | HW_RESET = '\x1b\x3f\x0a\x00' # Reset printer hardware 22 | # Cash Drawer 23 | CD_KICK_2 = '\x1b\x70\x00' # Sends a pulse to pin 2 [] 24 | CD_KICK_5 = '\x1b\x70\x01' # Sends a pulse to pin 5 [] 25 | # Paper 26 | PAPER_FULL_CUT = '\x1d\x56\x00' # Full cut paper 27 | PAPER_PART_CUT = '\x1d\x56\x01' # Partial cut paper 28 | SHEET_SLIP_MODE = '\x1B\x63\x30\x04' # Print ticket on injet slip paper 29 | SHEET_ROLL_MODE = '\x1B\x63\x30\x01' # Print ticket on paper roll 30 | 31 | # Text format 32 | TXT_NORMAL = '\x1b\x21\x00' # Normal text 33 | TXT_2HEIGHT = '\x1b\x21\x10' # Double height text 34 | TXT_2WIDTH = '\x1b\x21\x20' # Double width text 35 | TXT_DOUBLE = '\x1b\x21\x30' # Double height & Width 36 | TXT_UNDERL_OFF = '\x1b\x2d\x00' # Underline font OFF 37 | TXT_UNDERL_ON = '\x1b\x2d\x01' # Underline font 1-dot ON 38 | TXT_UNDERL2_ON = '\x1b\x2d\x02' # Underline font 2-dot ON 39 | TXT_BOLD_OFF = '\x1b\x45\x00' # Bold font OFF 40 | TXT_BOLD_ON = '\x1b\x45\x01' # Bold font ON 41 | TXT_FONT_A = '\x1b\x4d\x00' # Font type A 42 | TXT_FONT_B = '\x1b\x4d\x01' # Font type B 43 | TXT_ALIGN_LT = '\x1b\x61\x00' # Left justification 44 | TXT_ALIGN_CT = '\x1b\x61\x01' # Centering 45 | TXT_ALIGN_RT = '\x1b\x61\x02' # Right justification 46 | TXT_COLOR_BLACK = '\x1b\x72\x00' # Default Color 47 | TXT_COLOR_RED = '\x1b\x72\x01' # Alternative Color ( Usually Red ) 48 | 49 | # Text Encoding 50 | 51 | TXT_ENC_PC437 = '\x1b\x74\x00' # PC437 USA 52 | TXT_ENC_KATAKANA= '\x1b\x74\x01' # KATAKANA (JAPAN) 53 | TXT_ENC_PC850 = '\x1b\x74\x02' # PC850 Multilingual 54 | TXT_ENC_PC860 = '\x1b\x74\x03' # PC860 Portuguese 55 | TXT_ENC_PC863 = '\x1b\x74\x04' # PC863 Canadian-French 56 | TXT_ENC_PC865 = '\x1b\x74\x05' # PC865 Nordic 57 | TXT_ENC_KANJI6 = '\x1b\x74\x06' # One-pass Kanji, Hiragana 58 | TXT_ENC_KANJI7 = '\x1b\x74\x07' # One-pass Kanji 59 | TXT_ENC_KANJI8 = '\x1b\x74\x08' # One-pass Kanji 60 | TXT_ENC_PC851 = '\x1b\x74\x0b' # PC851 Greek 61 | TXT_ENC_PC853 = '\x1b\x74\x0c' # PC853 Turkish 62 | TXT_ENC_PC857 = '\x1b\x74\x0d' # PC857 Turkish 63 | TXT_ENC_PC737 = '\x1b\x74\x0e' # PC737 Greek 64 | TXT_ENC_8859_7 = '\x1b\x74\x0f' # ISO8859-7 Greek 65 | TXT_ENC_WPC1252 = '\x1b\x74\x10' # WPC1252 66 | TXT_ENC_PC866 = '\x1b\x74\x11' # PC866 Cyrillic #2 67 | TXT_ENC_PC852 = '\x1b\x74\x12' # PC852 Latin2 68 | TXT_ENC_PC858 = '\x1b\x74\x13' # PC858 Euro 69 | TXT_ENC_KU42 = '\x1b\x74\x14' # KU42 Thai 70 | TXT_ENC_TIS11 = '\x1b\x74\x15' # TIS11 Thai 71 | TXT_ENC_TIS18 = '\x1b\x74\x1a' # TIS18 Thai 72 | TXT_ENC_TCVN3 = '\x1b\x74\x1e' # TCVN3 Vietnamese 73 | TXT_ENC_TCVN3B = '\x1b\x74\x1f' # TCVN3 Vietnamese 74 | TXT_ENC_PC720 = '\x1b\x74\x20' # PC720 Arabic 75 | TXT_ENC_WPC775 = '\x1b\x74\x21' # WPC775 Baltic Rim 76 | TXT_ENC_PC855 = '\x1b\x74\x22' # PC855 Cyrillic 77 | TXT_ENC_PC861 = '\x1b\x74\x23' # PC861 Icelandic 78 | TXT_ENC_PC862 = '\x1b\x74\x24' # PC862 Hebrew 79 | TXT_ENC_PC864 = '\x1b\x74\x25' # PC864 Arabic 80 | TXT_ENC_PC869 = '\x1b\x74\x26' # PC869 Greek 81 | TXT_ENC_PC936 = '\x1C\x21\x00' # PC936 GBK(Guobiao Kuozhan) 82 | TXT_ENC_8859_2 = '\x1b\x74\x27' # ISO8859-2 Latin2 83 | TXT_ENC_8859_9 = '\x1b\x74\x28' # ISO8859-2 Latin9 84 | TXT_ENC_PC1098 = '\x1b\x74\x29' # PC1098 Farsi 85 | TXT_ENC_PC1118 = '\x1b\x74\x2a' # PC1118 Lithuanian 86 | TXT_ENC_PC1119 = '\x1b\x74\x2b' # PC1119 Lithuanian 87 | TXT_ENC_PC1125 = '\x1b\x74\x2c' # PC1125 Ukrainian 88 | TXT_ENC_WPC1250 = '\x1b\x74\x2d' # WPC1250 Latin2 89 | TXT_ENC_WPC1251 = '\x1b\x74\x2e' # WPC1251 Cyrillic 90 | TXT_ENC_WPC1253 = '\x1b\x74\x2f' # WPC1253 Greek 91 | TXT_ENC_WPC1254 = '\x1b\x74\x30' # WPC1254 Turkish 92 | TXT_ENC_WPC1255 = '\x1b\x74\x31' # WPC1255 Hebrew 93 | TXT_ENC_WPC1256 = '\x1b\x74\x32' # WPC1256 Arabic 94 | TXT_ENC_WPC1257 = '\x1b\x74\x33' # WPC1257 Baltic Rim 95 | TXT_ENC_WPC1258 = '\x1b\x74\x34' # WPC1258 Vietnamese 96 | TXT_ENC_KZ1048 = '\x1b\x74\x35' # KZ-1048 Kazakhstan 97 | 98 | TXT_ENC_KATAKANA_MAP = { 99 | # Maps UTF-8 Katakana symbols to KATAKANA Page Codes 100 | 101 | # Half-Width Katakanas 102 | '\xef\xbd\xa1':'\xa1', # 。 103 | '\xef\xbd\xa2':'\xa2', # 「 104 | '\xef\xbd\xa3':'\xa3', # 」 105 | '\xef\xbd\xa4':'\xa4', # 、 106 | '\xef\xbd\xa5':'\xa5', # ・ 107 | 108 | '\xef\xbd\xa6':'\xa6', # ヲ 109 | '\xef\xbd\xa7':'\xa7', # ァ 110 | '\xef\xbd\xa8':'\xa8', # ィ 111 | '\xef\xbd\xa9':'\xa9', # ゥ 112 | '\xef\xbd\xaa':'\xaa', # ェ 113 | '\xef\xbd\xab':'\xab', # ォ 114 | '\xef\xbd\xac':'\xac', # ャ 115 | '\xef\xbd\xad':'\xad', # ュ 116 | '\xef\xbd\xae':'\xae', # ョ 117 | '\xef\xbd\xaf':'\xaf', # ッ 118 | '\xef\xbd\xb0':'\xb0', # ー 119 | '\xef\xbd\xb1':'\xb1', # ア 120 | '\xef\xbd\xb2':'\xb2', # イ 121 | '\xef\xbd\xb3':'\xb3', # ウ 122 | '\xef\xbd\xb4':'\xb4', # エ 123 | '\xef\xbd\xb5':'\xb5', # オ 124 | '\xef\xbd\xb6':'\xb6', # カ 125 | '\xef\xbd\xb7':'\xb7', # キ 126 | '\xef\xbd\xb8':'\xb8', # ク 127 | '\xef\xbd\xb9':'\xb9', # ケ 128 | '\xef\xbd\xba':'\xba', # コ 129 | '\xef\xbd\xbb':'\xbb', # サ 130 | '\xef\xbd\xbc':'\xbc', # シ 131 | '\xef\xbd\xbd':'\xbd', # ス 132 | '\xef\xbd\xbe':'\xbe', # セ 133 | '\xef\xbd\xbf':'\xbf', # ソ 134 | '\xef\xbe\x80':'\xc0', # タ 135 | '\xef\xbe\x81':'\xc1', # チ 136 | '\xef\xbe\x82':'\xc2', # ツ 137 | '\xef\xbe\x83':'\xc3', # テ 138 | '\xef\xbe\x84':'\xc4', # ト 139 | '\xef\xbe\x85':'\xc5', # ナ 140 | '\xef\xbe\x86':'\xc6', # ニ 141 | '\xef\xbe\x87':'\xc7', # ヌ 142 | '\xef\xbe\x88':'\xc8', # ネ 143 | '\xef\xbe\x89':'\xc9', # ノ 144 | '\xef\xbe\x8a':'\xca', # ハ 145 | '\xef\xbe\x8b':'\xcb', # ヒ 146 | '\xef\xbe\x8c':'\xcc', # フ 147 | '\xef\xbe\x8d':'\xcd', # ヘ 148 | '\xef\xbe\x8e':'\xce', # ホ 149 | '\xef\xbe\x8f':'\xcf', # マ 150 | '\xef\xbe\x90':'\xd0', # ミ 151 | '\xef\xbe\x91':'\xd1', # ム 152 | '\xef\xbe\x92':'\xd2', # メ 153 | '\xef\xbe\x93':'\xd3', # モ 154 | '\xef\xbe\x94':'\xd4', # ヤ 155 | '\xef\xbe\x95':'\xd5', # ユ 156 | '\xef\xbe\x96':'\xd6', # ヨ 157 | '\xef\xbe\x97':'\xd7', # ラ 158 | '\xef\xbe\x98':'\xd8', # リ 159 | '\xef\xbe\x99':'\xd9', # ル 160 | '\xef\xbe\x9a':'\xda', # レ 161 | '\xef\xbe\x9b':'\xdb', # ロ 162 | '\xef\xbe\x9c':'\xdc', # ワ 163 | '\xef\xbe\x9d':'\xdd', # ン 164 | 165 | '\xef\xbe\x9e':'\xde', # ゙ 166 | '\xef\xbe\x9f':'\xdf', # ゚ 167 | } 168 | 169 | # Barcod format 170 | BARCODE_TXT_OFF = '\x1d\x48\x00' # HRI barcode chars OFF 171 | BARCODE_TXT_ABV = '\x1d\x48\x01' # HRI barcode chars above 172 | BARCODE_TXT_BLW = '\x1d\x48\x02' # HRI barcode chars below 173 | BARCODE_TXT_BTH = '\x1d\x48\x03' # HRI barcode chars both above and below 174 | BARCODE_FONT_A = '\x1d\x66\x00' # Font type A for HRI barcode chars 175 | BARCODE_FONT_B = '\x1d\x66\x01' # Font type B for HRI barcode chars 176 | BARCODE_HEIGHT = '\x1d\x68\x64' # Barcode Height [1-255] 177 | BARCODE_WIDTH = '\x1d\x77\x03' # Barcode Width [2-6] 178 | BARCODE_UPC_A = '\x1d\x6b\x00' # Barcode type UPC-A 179 | BARCODE_UPC_E = '\x1d\x6b\x01' # Barcode type UPC-E 180 | BARCODE_EAN13 = '\x1d\x6b\x02' # Barcode type EAN13 181 | BARCODE_EAN8 = '\x1d\x6b\x03' # Barcode type EAN8 182 | BARCODE_CODE39 = '\x1d\x6b\x04' # Barcode type CODE39 183 | BARCODE_ITF = '\x1d\x6b\x05' # Barcode type ITF 184 | BARCODE_NW7 = '\x1d\x6b\x06' # Barcode type NW7 185 | # Image format 186 | S_RASTER_N = '\x1d\x76\x30\x00' # Set raster image normal size 187 | S_RASTER_2W = '\x1d\x76\x30\x01' # Set raster image double width 188 | S_RASTER_2H = '\x1d\x76\x30\x02' # Set raster image double height 189 | S_RASTER_Q = '\x1d\x76\x30\x03' # Set raster image quadruple 190 | -------------------------------------------------------------------------------- /xmlescpos/escpos.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import copy 5 | import io 6 | import base64 7 | import math 8 | import md5 9 | import re 10 | import traceback 11 | import xml.etree.ElementTree as ET 12 | import xml.dom.minidom as minidom 13 | 14 | from PIL import Image 15 | 16 | try: 17 | import jcconv 18 | except ImportError: 19 | jcconv = None 20 | 21 | try: 22 | import qrcode 23 | except ImportError: 24 | qrcode = None 25 | 26 | from constants import * 27 | from exceptions import * 28 | 29 | def utfstr(stuff): 30 | """ converts stuff to string and does without failing if stuff is a utf8 string """ 31 | if isinstance(stuff,basestring): 32 | return stuff 33 | else: 34 | return str(stuff) 35 | 36 | class StyleStack: 37 | """ 38 | The stylestack is used by the xml receipt serializer to compute the active styles along the xml 39 | document. Styles are just xml attributes, there is no css mechanism. But the style applied by 40 | the attributes are inherited by deeper nodes. 41 | """ 42 | def __init__(self): 43 | self.stack = [] 44 | self.defaults = { # default style values 45 | 'align': 'left', 46 | 'underline': 'off', 47 | 'bold': 'off', 48 | 'size': 'normal', 49 | 'font' : 'a', 50 | 'width': 48, 51 | 'indent': 0, 52 | 'tabwidth': 2, 53 | 'bullet': ' - ', 54 | 'line-ratio':0.5, 55 | 'color': 'black', 56 | 57 | 'value-decimals': 2, 58 | 'value-symbol': '', 59 | 'value-symbol-position': 'after', 60 | 'value-autoint': 'off', 61 | 'value-decimals-separator': '.', 62 | 'value-thousands-separator': ',', 63 | 'value-width': 0, 64 | 65 | } 66 | 67 | self.types = { # attribute types, default is string and can be ommitted 68 | 'width': 'int', 69 | 'indent': 'int', 70 | 'tabwidth': 'int', 71 | 'line-ratio': 'float', 72 | 'value-decimals': 'int', 73 | 'value-width': 'int', 74 | } 75 | 76 | self.cmds = { 77 | # translation from styles to escpos commands 78 | # some style do not correspond to escpos command are used by 79 | # the serializer instead 80 | 'align': { 81 | 'left': TXT_ALIGN_LT, 82 | 'right': TXT_ALIGN_RT, 83 | 'center': TXT_ALIGN_CT, 84 | '_order': 1, 85 | }, 86 | 'underline': { 87 | 'off': TXT_UNDERL_OFF, 88 | 'on': TXT_UNDERL_ON, 89 | 'double': TXT_UNDERL2_ON, 90 | # must be issued after 'size' command 91 | # because ESC ! resets ESC - 92 | '_order': 10, 93 | }, 94 | 'bold': { 95 | 'off': TXT_BOLD_OFF, 96 | 'on': TXT_BOLD_ON, 97 | # must be issued after 'size' command 98 | # because ESC ! resets ESC - 99 | '_order': 10, 100 | }, 101 | 'font': { 102 | 'a': TXT_FONT_A, 103 | 'b': TXT_FONT_B, 104 | # must be issued after 'size' command 105 | # because ESC ! resets ESC - 106 | '_order': 10, 107 | }, 108 | 'size': { 109 | 'normal': TXT_NORMAL, 110 | 'double-height': TXT_2HEIGHT, 111 | 'double-width': TXT_2WIDTH, 112 | 'double': TXT_DOUBLE, 113 | '_order': 1, 114 | }, 115 | 'color': { 116 | 'black': TXT_COLOR_BLACK, 117 | 'red': TXT_COLOR_RED, 118 | '_order': 1, 119 | }, 120 | } 121 | 122 | self.push(self.defaults) 123 | 124 | def get(self,style): 125 | """ what's the value of a style at the current stack level""" 126 | level = len(self.stack) -1 127 | while level >= 0: 128 | if style in self.stack[level]: 129 | return self.stack[level][style] 130 | else: 131 | level = level - 1 132 | return None 133 | 134 | def enforce_type(self, attr, val): 135 | """converts a value to the attribute's type""" 136 | if not attr in self.types: 137 | return utfstr(val) 138 | elif self.types[attr] == 'int': 139 | return int(float(val)) 140 | elif self.types[attr] == 'float': 141 | return float(val) 142 | else: 143 | return utfstr(val) 144 | 145 | def push(self, style={}): 146 | """push a new level on the stack with a style dictionnary containing style:value pairs""" 147 | _style = {} 148 | for attr in style: 149 | if attr in self.cmds and not style[attr] in self.cmds[attr]: 150 | print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) 151 | else: 152 | _style[attr] = self.enforce_type(attr, style[attr]) 153 | self.stack.append(_style) 154 | 155 | def set(self, style={}): 156 | """overrides style values at the current stack level""" 157 | _style = {} 158 | for attr in style: 159 | if attr in self.cmds and not style[attr] in self.cmds[attr]: 160 | print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) 161 | else: 162 | self.stack[-1][attr] = self.enforce_type(attr, style[attr]) 163 | 164 | def pop(self): 165 | """ pop a style stack level """ 166 | if len(self.stack) > 1 : 167 | self.stack = self.stack[:-1] 168 | 169 | def to_escpos(self): 170 | """ converts the current style to an escpos command string """ 171 | cmd = '' 172 | ordered_cmds = self.cmds.keys() 173 | ordered_cmds.sort(lambda x,y: cmp(self.cmds[x]['_order'], self.cmds[y]['_order'])) 174 | for style in ordered_cmds: 175 | cmd += self.cmds[style][self.get(style)] 176 | return cmd 177 | 178 | class XmlSerializer: 179 | """ 180 | Converts the xml inline / block tree structure to a string, 181 | keeping track of newlines and spacings. 182 | The string is outputted asap to the provided escpos driver. 183 | """ 184 | def __init__(self,escpos): 185 | self.escpos = escpos 186 | self.stack = ['block'] 187 | self.dirty = False 188 | 189 | def start_inline(self,stylestack=None): 190 | """ starts an inline entity with an optional style definition """ 191 | self.stack.append('inline') 192 | if self.dirty: 193 | self.escpos._raw(' ') 194 | if stylestack: 195 | self.style(stylestack) 196 | 197 | def start_block(self,stylestack=None): 198 | """ starts a block entity with an optional style definition """ 199 | if self.dirty: 200 | self.escpos._raw('\n') 201 | self.dirty = False 202 | self.stack.append('block') 203 | if stylestack: 204 | self.style(stylestack) 205 | 206 | def end_entity(self): 207 | """ ends the entity definition. (but does not cancel the active style!) """ 208 | if self.stack[-1] == 'block' and self.dirty: 209 | self.escpos._raw('\n') 210 | self.dirty = False 211 | if len(self.stack) > 1: 212 | self.stack = self.stack[:-1] 213 | 214 | def pre(self,text): 215 | """ puts a string of text in the entity keeping the whitespace intact """ 216 | if text: 217 | self.escpos.text(text) 218 | self.dirty = True 219 | 220 | def text(self,text): 221 | """ puts text in the entity. Whitespace and newlines are stripped to single spaces. """ 222 | if text: 223 | text = utfstr(text) 224 | text = text.strip() 225 | text = re.sub('\s+',' ',text) 226 | if text: 227 | self.dirty = True 228 | self.escpos.text(text) 229 | 230 | def linebreak(self): 231 | """ inserts a linebreak in the entity """ 232 | self.dirty = False 233 | self.escpos._raw('\n') 234 | 235 | def style(self,stylestack): 236 | """ apply a style to the entity (only applies to content added after the definition) """ 237 | self.raw(stylestack.to_escpos()) 238 | 239 | def raw(self,raw): 240 | """ puts raw text or escpos command in the entity without affecting the state of the serializer """ 241 | self.escpos._raw(raw) 242 | 243 | class XmlLineSerializer: 244 | """ 245 | This is used to convert a xml tree into a single line, with a left and a right part. 246 | The content is not output to escpos directly, and is intended to be fedback to the 247 | XmlSerializer as the content of a block entity. 248 | """ 249 | def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5): 250 | self.tabwidth = tabwidth 251 | self.indent = indent 252 | self.width = max(0, width - int(tabwidth*indent)) 253 | self.lwidth = int(self.width*ratio) 254 | self.rwidth = max(0, self.width - self.lwidth) 255 | self.clwidth = 0 256 | self.crwidth = 0 257 | self.lbuffer = '' 258 | self.rbuffer = '' 259 | self.left = True 260 | 261 | def _txt(self,txt): 262 | if self.left: 263 | if self.clwidth < self.lwidth: 264 | txt = txt[:max(0, self.lwidth - self.clwidth)] 265 | self.lbuffer += txt 266 | self.clwidth += len(txt) 267 | else: 268 | if self.crwidth < self.rwidth: 269 | txt = txt[:max(0, self.rwidth - self.crwidth)] 270 | self.rbuffer += txt 271 | self.crwidth += len(txt) 272 | 273 | def start_inline(self,stylestack=None): 274 | if (self.left and self.clwidth) or (not self.left and self.crwidth): 275 | self._txt(' ') 276 | 277 | def start_block(self,stylestack=None): 278 | self.start_inline(stylestack) 279 | 280 | def end_entity(self): 281 | pass 282 | 283 | def pre(self,text): 284 | if text: 285 | self._txt(text) 286 | def text(self,text): 287 | if text: 288 | text = utfstr(text) 289 | text = text.strip() 290 | text = re.sub('\s+',' ',text) 291 | if text: 292 | self._txt(text) 293 | 294 | def linebreak(self): 295 | pass 296 | def style(self,stylestack): 297 | pass 298 | def raw(self,raw): 299 | pass 300 | 301 | def start_right(self): 302 | self.left = False 303 | 304 | def get_line(self): 305 | return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer 306 | 307 | 308 | class Escpos: 309 | """ ESC/POS Printer object """ 310 | device = None 311 | encoding = None 312 | img_cache = {} 313 | 314 | def _check_image_size(self, size): 315 | """ Check and fix the size of the image to 32 bits """ 316 | if size % 32 == 0: 317 | return (0, 0) 318 | else: 319 | image_border = 32 - (size % 32) 320 | if (image_border % 2) == 0: 321 | return (image_border / 2, image_border / 2) 322 | else: 323 | return (image_border / 2, (image_border / 2) + 1) 324 | 325 | def _print_image(self, line, size): 326 | """ Print formatted image """ 327 | i = 0 328 | cont = 0 329 | buffer = "" 330 | 331 | 332 | self._raw(S_RASTER_N) 333 | buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0) 334 | self._raw(buffer.decode('hex')) 335 | buffer = "" 336 | 337 | while i < len(line): 338 | hex_string = int(line[i:i+8],2) 339 | buffer += "%02X" % hex_string 340 | i += 8 341 | cont += 1 342 | if cont % 4 == 0: 343 | self._raw(buffer.decode("hex")) 344 | buffer = "" 345 | cont = 0 346 | 347 | def _raw_print_image(self, line, size, output=None ): 348 | """ Print formatted image """ 349 | i = 0 350 | cont = 0 351 | buffer = "" 352 | raw = "" 353 | 354 | def __raw(string): 355 | if output: 356 | output(string) 357 | else: 358 | self._raw(string) 359 | 360 | raw += S_RASTER_N 361 | buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0) 362 | raw += buffer.decode('hex') 363 | buffer = "" 364 | 365 | while i < len(line): 366 | hex_string = int(line[i:i+8],2) 367 | buffer += "%02X" % hex_string 368 | i += 8 369 | cont += 1 370 | if cont % 4 == 0: 371 | raw += buffer.decode("hex") 372 | buffer = "" 373 | cont = 0 374 | 375 | return raw 376 | 377 | def _convert_image(self, im): 378 | """ Parse image and prepare it to a printable format """ 379 | pixels = [] 380 | pix_line = "" 381 | im_left = "" 382 | im_right = "" 383 | switch = 0 384 | img_size = [ 0, 0 ] 385 | 386 | 387 | if im.size[0] > 512: 388 | print "WARNING: Image is wider than 512 and could be truncated at print time " 389 | if im.size[1] > 255: 390 | raise ImageSizeError() 391 | 392 | im_border = self._check_image_size(im.size[0]) 393 | for i in range(im_border[0]): 394 | im_left += "0" 395 | for i in range(im_border[1]): 396 | im_right += "0" 397 | 398 | for y in range(im.size[1]): 399 | img_size[1] += 1 400 | pix_line += im_left 401 | img_size[0] += im_border[0] 402 | for x in range(im.size[0]): 403 | img_size[0] += 1 404 | RGB = im.getpixel((x, y)) 405 | im_color = (RGB[0] + RGB[1] + RGB[2]) 406 | im_pattern = "1X0" 407 | pattern_len = len(im_pattern) 408 | switch = (switch - 1 ) * (-1) 409 | for x in range(pattern_len): 410 | if im_color <= (255 * 3 / pattern_len * (x+1)): 411 | if im_pattern[x] == "X": 412 | pix_line += "%d" % switch 413 | else: 414 | pix_line += im_pattern[x] 415 | break 416 | elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3): 417 | pix_line += im_pattern[-1] 418 | break 419 | pix_line += im_right 420 | img_size[0] += im_border[1] 421 | 422 | return (pix_line, img_size) 423 | 424 | def image(self,path_img): 425 | """ Open image file """ 426 | im_open = Image.open(path_img) 427 | im = im_open.convert("RGB") 428 | # Convert the RGB image in printable image 429 | pix_line, img_size = self._convert_image(im) 430 | self._print_image(pix_line, img_size) 431 | 432 | def print_base64_image(self,img): 433 | 434 | print 'print_b64_img' 435 | 436 | id = md5.new(img).digest() 437 | 438 | if id not in self.img_cache: 439 | print 'not in cache' 440 | 441 | img = img[img.find(',')+1:] 442 | f = io.BytesIO('img') 443 | f.write(base64.decodestring(img)) 444 | f.seek(0) 445 | img_rgba = Image.open(f) 446 | img = Image.new('RGB', img_rgba.size, (255,255,255)) 447 | channels = img_rgba.split() 448 | if len(channels) > 1: 449 | # use alpha channel as mask 450 | img.paste(img_rgba, mask=channels[3]) 451 | else: 452 | img.paste(img_rgba) 453 | 454 | print 'convert image' 455 | 456 | pix_line, img_size = self._convert_image(img) 457 | 458 | print 'print image' 459 | 460 | buffer = self._raw_print_image(pix_line, img_size) 461 | self.img_cache[id] = buffer 462 | 463 | print 'raw image' 464 | 465 | self._raw(self.img_cache[id]) 466 | 467 | def qr(self,text): 468 | """ Print QR Code for the provided string """ 469 | qr_code = qrcode.QRCode(version=4, box_size=4, border=1) 470 | qr_code.add_data(text) 471 | qr_code.make(fit=True) 472 | qr_img = qr_code.make_image() 473 | im = qr_img._img.convert("RGB") 474 | # Convert the RGB image in printable image 475 | self._convert_image(im) 476 | 477 | def barcode(self, code, bc, width=255, height=2, pos='below', font='a'): 478 | """ Print Barcode """ 479 | # Align Bar Code() 480 | self._raw(TXT_ALIGN_CT) 481 | # Height 482 | if height >=2 or height <=6: 483 | self._raw(BARCODE_HEIGHT) 484 | else: 485 | raise BarcodeSizeError() 486 | # Width 487 | if width >= 1 or width <=255: 488 | self._raw(BARCODE_WIDTH) 489 | else: 490 | raise BarcodeSizeError() 491 | # Font 492 | if font.upper() == "B": 493 | self._raw(BARCODE_FONT_B) 494 | else: # DEFAULT FONT: A 495 | self._raw(BARCODE_FONT_A) 496 | # Position 497 | if pos.upper() == "OFF": 498 | self._raw(BARCODE_TXT_OFF) 499 | elif pos.upper() == "BOTH": 500 | self._raw(BARCODE_TXT_BTH) 501 | elif pos.upper() == "ABOVE": 502 | self._raw(BARCODE_TXT_ABV) 503 | else: # DEFAULT POSITION: BELOW 504 | self._raw(BARCODE_TXT_BLW) 505 | # Type 506 | if bc.upper() == "UPC-A": 507 | self._raw(BARCODE_UPC_A) 508 | elif bc.upper() == "UPC-E": 509 | self._raw(BARCODE_UPC_E) 510 | elif bc.upper() == "EAN13": 511 | self._raw(BARCODE_EAN13) 512 | elif bc.upper() == "EAN8": 513 | self._raw(BARCODE_EAN8) 514 | elif bc.upper() == "CODE39": 515 | self._raw(BARCODE_CODE39) 516 | elif bc.upper() == "ITF": 517 | self._raw(BARCODE_ITF) 518 | elif bc.upper() == "NW7": 519 | self._raw(BARCODE_NW7) 520 | else: 521 | raise BarcodeTypeError() 522 | # Print Code 523 | if code: 524 | self._raw(code) 525 | else: 526 | raise exception.BarcodeCodeError() 527 | 528 | def receipt(self,xml): 529 | """ 530 | Prints an xml based receipt definition 531 | """ 532 | 533 | def strclean(string): 534 | if not string: 535 | string = '' 536 | string = string.strip() 537 | string = re.sub('\s+',' ',string) 538 | return string 539 | 540 | def format_value(value, decimals=3, width=0, decimals_separator='.', thousands_separator=',', autoint=False, symbol='', position='after'): 541 | decimals = max(0,int(decimals)) 542 | width = max(0,int(width)) 543 | value = float(value) 544 | 545 | if autoint and math.floor(value) == value: 546 | decimals = 0 547 | if width == 0: 548 | width = '' 549 | 550 | if thousands_separator: 551 | formatstr = "{:"+str(width)+",."+str(decimals)+"f}" 552 | else: 553 | formatstr = "{:"+str(width)+"."+str(decimals)+"f}" 554 | 555 | 556 | ret = formatstr.format(value) 557 | ret = ret.replace(',','COMMA') 558 | ret = ret.replace('.','DOT') 559 | ret = ret.replace('COMMA',thousands_separator) 560 | ret = ret.replace('DOT',decimals_separator) 561 | 562 | if symbol: 563 | if position == 'after': 564 | ret = ret + symbol 565 | else: 566 | ret = symbol + ret 567 | return ret 568 | 569 | def print_elem(stylestack, serializer, elem, indent=0): 570 | 571 | elem_styles = { 572 | 'h1': {'bold': 'on', 'size':'double'}, 573 | 'h2': {'size':'double'}, 574 | 'h3': {'bold': 'on', 'size':'double-height'}, 575 | 'h4': {'size': 'double-height'}, 576 | 'h5': {'bold': 'on'}, 577 | 'em': {'font': 'b'}, 578 | 'b': {'bold': 'on'}, 579 | } 580 | 581 | stylestack.push() 582 | if elem.tag in elem_styles: 583 | stylestack.set(elem_styles[elem.tag]) 584 | stylestack.set(elem.attrib) 585 | 586 | if elem.tag in ('p','div','section','article','receipt','header','footer','li','h1','h2','h3','h4','h5'): 587 | serializer.start_block(stylestack) 588 | serializer.text(elem.text) 589 | for child in elem: 590 | print_elem(stylestack,serializer,child) 591 | serializer.start_inline(stylestack) 592 | serializer.text(child.tail) 593 | serializer.end_entity() 594 | serializer.end_entity() 595 | 596 | elif elem.tag in ('span','em','b','left','right'): 597 | serializer.start_inline(stylestack) 598 | serializer.text(elem.text) 599 | for child in elem: 600 | print_elem(stylestack,serializer,child) 601 | serializer.start_inline(stylestack) 602 | serializer.text(child.tail) 603 | serializer.end_entity() 604 | serializer.end_entity() 605 | 606 | elif elem.tag == 'value': 607 | serializer.start_inline(stylestack) 608 | serializer.pre(format_value( 609 | elem.text, 610 | decimals=stylestack.get('value-decimals'), 611 | width=stylestack.get('value-width'), 612 | decimals_separator=stylestack.get('value-decimals-separator'), 613 | thousands_separator=stylestack.get('value-thousands-separator'), 614 | autoint=(stylestack.get('value-autoint') == 'on'), 615 | symbol=stylestack.get('value-symbol'), 616 | position=stylestack.get('value-symbol-position') 617 | )) 618 | serializer.end_entity() 619 | 620 | elif elem.tag == 'line': 621 | width = stylestack.get('width') 622 | if stylestack.get('size') in ('double', 'double-width'): 623 | width = width / 2 624 | 625 | lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio')) 626 | serializer.start_block(stylestack) 627 | for child in elem: 628 | if child.tag == 'left': 629 | print_elem(stylestack,lineserializer,child,indent=indent) 630 | elif child.tag == 'right': 631 | lineserializer.start_right() 632 | print_elem(stylestack,lineserializer,child,indent=indent) 633 | serializer.pre(lineserializer.get_line()) 634 | serializer.end_entity() 635 | 636 | elif elem.tag == 'ul': 637 | serializer.start_block(stylestack) 638 | bullet = stylestack.get('bullet') 639 | for child in elem: 640 | if child.tag == 'li': 641 | serializer.style(stylestack) 642 | serializer.raw(' ' * indent * stylestack.get('tabwidth') + bullet) 643 | print_elem(stylestack,serializer,child,indent=indent+1) 644 | serializer.end_entity() 645 | 646 | elif elem.tag == 'ol': 647 | cwidth = len(str(len(elem))) + 2 648 | i = 1 649 | serializer.start_block(stylestack) 650 | for child in elem: 651 | if child.tag == 'li': 652 | serializer.style(stylestack) 653 | serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth)) 654 | i = i + 1 655 | print_elem(stylestack,serializer,child,indent=indent+1) 656 | serializer.end_entity() 657 | 658 | elif elem.tag == 'pre': 659 | serializer.start_block(stylestack) 660 | serializer.pre(elem.text) 661 | serializer.end_entity() 662 | 663 | elif elem.tag == 'hr': 664 | width = stylestack.get('width') 665 | if stylestack.get('size') in ('double', 'double-width'): 666 | width = width / 2 667 | serializer.start_block(stylestack) 668 | serializer.text('-'*width) 669 | serializer.end_entity() 670 | 671 | elif elem.tag == 'br': 672 | serializer.linebreak() 673 | 674 | elif elem.tag == 'img': 675 | if 'src' in elem.attrib and 'data:' in elem.attrib['src']: 676 | self.print_base64_image(elem.attrib['src']) 677 | 678 | elif elem.tag == 'barcode' and 'encoding' in elem.attrib: 679 | serializer.start_block(stylestack) 680 | self.barcode(strclean(elem.text),elem.attrib['encoding']) 681 | serializer.end_entity() 682 | 683 | elif elem.tag == 'cut': 684 | self.cut() 685 | elif elem.tag == 'partialcut': 686 | self.cut(mode='part') 687 | elif elem.tag == 'cashdraw': 688 | self.cashdraw(2) 689 | self.cashdraw(5) 690 | 691 | stylestack.pop() 692 | 693 | try: 694 | stylestack = StyleStack() 695 | serializer = XmlSerializer(self) 696 | root = ET.fromstring(xml.encode('utf-8')) 697 | if 'sheet' in root.attrib and root.attrib['sheet'] == 'slip': 698 | self._raw(SHEET_SLIP_MODE) 699 | self.slip_sheet_mode = True 700 | else: 701 | self._raw(SHEET_ROLL_MODE) 702 | 703 | self._raw(stylestack.to_escpos()) 704 | 705 | print_elem(stylestack,serializer,root) 706 | 707 | if 'open-cashdrawer' in root.attrib and root.attrib['open-cashdrawer'] == 'true': 708 | self.cashdraw(2) 709 | self.cashdraw(5) 710 | if not 'cut' in root.attrib or root.attrib['cut'] == 'true' : 711 | if self.slip_sheet_mode: 712 | self._raw(CTL_FF) 713 | else: 714 | self.cut() 715 | 716 | except Exception as e: 717 | errmsg = str(e)+'\n'+'-'*48+'\n'+traceback.format_exc() + '-'*48+'\n' 718 | self.text(errmsg) 719 | self.cut() 720 | 721 | raise e 722 | 723 | def text(self,txt): 724 | """ Print Utf8 encoded alpha-numeric text """ 725 | if not txt: 726 | return 727 | try: 728 | txt = txt.decode('utf-8') 729 | except: 730 | try: 731 | txt = txt.decode('utf-16') 732 | except: 733 | pass 734 | 735 | self.extra_chars = 0 736 | 737 | def encode_char(char): 738 | """ 739 | Encodes a single utf-8 character into a sequence of 740 | esc-pos code page change instructions and character declarations 741 | """ 742 | char_utf8 = char.encode('utf-8') 743 | encoded = '' 744 | encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character 745 | encodings = { 746 | # TODO use ordering to prevent useless switches 747 | # TODO Support other encodings not natively supported by python ( Thai, Khazakh, Kanjis ) 748 | 'cp437': TXT_ENC_PC437, 749 | 'cp850': TXT_ENC_PC850, 750 | 'cp852': TXT_ENC_PC852, 751 | 'cp857': TXT_ENC_PC857, 752 | 'cp858': TXT_ENC_PC858, 753 | 'cp860': TXT_ENC_PC860, 754 | 'cp863': TXT_ENC_PC863, 755 | 'cp865': TXT_ENC_PC865, 756 | 'cp866': TXT_ENC_PC866, 757 | 'cp862': TXT_ENC_PC862, 758 | 'cp720': TXT_ENC_PC720, 759 | 'cp936': TXT_ENC_PC936, 760 | 'iso8859_2': TXT_ENC_8859_2, 761 | 'iso8859_7': TXT_ENC_8859_7, 762 | 'iso8859_9': TXT_ENC_8859_9, 763 | 'cp1254' : TXT_ENC_WPC1254, 764 | 'cp1255' : TXT_ENC_WPC1255, 765 | 'cp1256' : TXT_ENC_WPC1256, 766 | 'cp1257' : TXT_ENC_WPC1257, 767 | 'cp1258' : TXT_ENC_WPC1258, 768 | 'katakana' : TXT_ENC_KATAKANA, 769 | } 770 | remaining = copy.copy(encodings) 771 | 772 | if not encoding : 773 | encoding = 'cp437' 774 | 775 | while True: # Trying all encoding until one succeeds 776 | try: 777 | if encoding == 'katakana': # Japanese characters 778 | if jcconv: 779 | # try to convert japanese text to a half-katakanas 780 | kata = jcconv.kata2half(jcconv.hira2kata(char_utf8)) 781 | if kata != char_utf8: 782 | self.extra_chars += len(kata.decode('utf-8')) - 1 783 | # the conversion may result in multiple characters 784 | return encode_str(kata.decode('utf-8')) 785 | else: 786 | kata = char_utf8 787 | 788 | if kata in TXT_ENC_KATAKANA_MAP: 789 | encoded = TXT_ENC_KATAKANA_MAP[kata] 790 | break 791 | else: 792 | raise ValueError() 793 | else: 794 | encoded = char.encode(encoding) 795 | break 796 | 797 | except ValueError: #the encoding failed, select another one and retry 798 | if encoding in remaining: 799 | del remaining[encoding] 800 | if len(remaining) >= 1: 801 | encoding = remaining.items()[0][0] 802 | else: 803 | encoding = 'cp437' 804 | encoded = '\xb1' # could not encode, output error character 805 | break; 806 | 807 | if encoding != self.encoding: 808 | # if the encoding changed, remember it and prefix the character with 809 | # the esc-pos encoding change sequence 810 | self.encoding = encoding 811 | encoded = encodings[encoding] + encoded 812 | 813 | return encoded 814 | 815 | def encode_str(txt): 816 | buffer = '' 817 | for c in txt: 818 | buffer += encode_char(c) 819 | return buffer 820 | 821 | txt = encode_str(txt) 822 | 823 | # if the utf-8 -> codepage conversion inserted extra characters, 824 | # remove double spaces to try to restore the original string length 825 | # and prevent printing alignment issues 826 | while self.extra_chars > 0: 827 | dspace = txt.find(' ') 828 | if dspace > 0: 829 | txt = txt[:dspace] + txt[dspace+1:] 830 | self.extra_chars -= 1 831 | else: 832 | break 833 | 834 | self._raw(txt) 835 | 836 | def set(self, align='left', font='a', type='normal', width=1, height=1): 837 | """ Set text properties """ 838 | # Align 839 | if align.upper() == "CENTER": 840 | self._raw(TXT_ALIGN_CT) 841 | elif align.upper() == "RIGHT": 842 | self._raw(TXT_ALIGN_RT) 843 | elif align.upper() == "LEFT": 844 | self._raw(TXT_ALIGN_LT) 845 | # Font 846 | if font.upper() == "B": 847 | self._raw(TXT_FONT_B) 848 | else: # DEFAULT FONT: A 849 | self._raw(TXT_FONT_A) 850 | # Type 851 | if type.upper() == "B": 852 | self._raw(TXT_BOLD_ON) 853 | self._raw(TXT_UNDERL_OFF) 854 | elif type.upper() == "U": 855 | self._raw(TXT_BOLD_OFF) 856 | self._raw(TXT_UNDERL_ON) 857 | elif type.upper() == "U2": 858 | self._raw(TXT_BOLD_OFF) 859 | self._raw(TXT_UNDERL2_ON) 860 | elif type.upper() == "BU": 861 | self._raw(TXT_BOLD_ON) 862 | self._raw(TXT_UNDERL_ON) 863 | elif type.upper() == "BU2": 864 | self._raw(TXT_BOLD_ON) 865 | self._raw(TXT_UNDERL2_ON) 866 | elif type.upper == "NORMAL": 867 | self._raw(TXT_BOLD_OFF) 868 | self._raw(TXT_UNDERL_OFF) 869 | # Width 870 | if width == 2 and height != 2: 871 | self._raw(TXT_NORMAL) 872 | self._raw(TXT_2WIDTH) 873 | elif height == 2 and width != 2: 874 | self._raw(TXT_NORMAL) 875 | self._raw(TXT_2HEIGHT) 876 | elif height == 2 and width == 2: 877 | self._raw(TXT_2WIDTH) 878 | self._raw(TXT_2HEIGHT) 879 | else: # DEFAULT SIZE: NORMAL 880 | self._raw(TXT_NORMAL) 881 | 882 | 883 | def cut(self, mode=''): 884 | """ Cut paper """ 885 | # Fix the size between last line and cut 886 | # TODO: handle this with a line feed 887 | self._raw("\n\n\n\n\n\n") 888 | if mode.upper() == "PART": 889 | self._raw(PAPER_PART_CUT) 890 | else: # DEFAULT MODE: FULL CUT 891 | self._raw(PAPER_FULL_CUT) 892 | 893 | 894 | def cashdraw(self, pin): 895 | """ Send pulse to kick the cash drawer """ 896 | if pin == 2: 897 | self._raw(CD_KICK_2) 898 | elif pin == 5: 899 | self._raw(CD_KICK_5) 900 | else: 901 | raise CashDrawerError() 902 | 903 | 904 | def hw(self, hw): 905 | """ Hardware operations """ 906 | if hw.upper() == "INIT": 907 | self._raw(HW_INIT) 908 | elif hw.upper() == "SELECT": 909 | self._raw(HW_SELECT) 910 | elif hw.upper() == "RESET": 911 | self._raw(HW_RESET) 912 | else: # DEFAULT: DOES NOTHING 913 | pass 914 | 915 | 916 | def control(self, ctl): 917 | """ Feed control sequences """ 918 | if ctl.upper() == "LF": 919 | self._raw(CTL_LF) 920 | elif ctl.upper() == "FF": 921 | self._raw(CTL_FF) 922 | elif ctl.upper() == "CR": 923 | self._raw(CTL_CR) 924 | elif ctl.upper() == "HT": 925 | self._raw(CTL_HT) 926 | elif ctl.upper() == "VT": 927 | self._raw(CTL_VT) 928 | -------------------------------------------------------------------------------- /xmlescpos/exceptions.py: -------------------------------------------------------------------------------- 1 | """ ESC/POS Exceptions classes """ 2 | 3 | import os 4 | 5 | class Error(Exception): 6 | """ Base class for ESC/POS errors """ 7 | def __init__(self, msg, status=None): 8 | Exception.__init__(self) 9 | self.msg = msg 10 | self.resultcode = 1 11 | if status is not None: 12 | self.resultcode = status 13 | 14 | def __str__(self): 15 | return self.msg 16 | 17 | # Result/Exit codes 18 | # 0 = success 19 | # 10 = No Barcode type defined 20 | # 20 = Barcode size values are out of range 21 | # 30 = Barcode text not supplied 22 | # 40 = Image height is too large 23 | # 50 = No string supplied to be printed 24 | # 60 = Invalid pin to send Cash Drawer pulse 25 | 26 | 27 | class BarcodeTypeError(Error): 28 | def __init__(self, msg=""): 29 | Error.__init__(self, msg) 30 | self.msg = msg 31 | self.resultcode = 10 32 | 33 | def __str__(self): 34 | return "No Barcode type is defined" 35 | 36 | class BarcodeSizeError(Error): 37 | def __init__(self, msg=""): 38 | Error.__init__(self, msg) 39 | self.msg = msg 40 | self.resultcode = 20 41 | 42 | def __str__(self): 43 | return "Barcode size is out of range" 44 | 45 | class BarcodeCodeError(Error): 46 | def __init__(self, msg=""): 47 | Error.__init__(self, msg) 48 | self.msg = msg 49 | self.resultcode = 30 50 | 51 | def __str__(self): 52 | return "Code was not supplied" 53 | 54 | class ImageSizeError(Error): 55 | def __init__(self, msg=""): 56 | Error.__init__(self, msg) 57 | self.msg = msg 58 | self.resultcode = 40 59 | 60 | def __str__(self): 61 | return "Image height is longer than 255px and can't be printed" 62 | 63 | class TextError(Error): 64 | def __init__(self, msg=""): 65 | Error.__init__(self, msg) 66 | self.msg = msg 67 | self.resultcode = 50 68 | 69 | def __str__(self): 70 | return "Text string must be supplied to the text() method" 71 | 72 | 73 | class CashDrawerError(Error): 74 | def __init__(self, msg=""): 75 | Error.__init__(self, msg) 76 | self.msg = msg 77 | self.resultcode = 60 78 | 79 | def __str__(self): 80 | return "Valid pin must be set to send pulse" 81 | 82 | class NoStatusError(Error): 83 | def __init__(self, msg=""): 84 | Error.__init__(self, msg) 85 | self.msg = msg 86 | self.resultcode = 70 87 | 88 | def __str__(self): 89 | return "Impossible to get status from the printer" 90 | 91 | class TicketNotPrinted(Error): 92 | def __init__(self, msg=""): 93 | Error.__init__(self, msg) 94 | self.msg = msg 95 | self.resultcode = 80 96 | 97 | def __str__(self): 98 | return "A part of the ticket was not been printed" 99 | 100 | class NoDeviceError(Error): 101 | def __init__(self, msg=""): 102 | Error.__init__(self, msg) 103 | self.msg = msg 104 | self.resultcode = 90 105 | 106 | def __str__(self): 107 | return "Impossible to find the printer Device" 108 | 109 | class HandleDeviceError(Error): 110 | def __init__(self, msg=""): 111 | Error.__init__(self, msg) 112 | self.msg = msg 113 | self.resultcode = 100 114 | 115 | def __str__(self): 116 | return "Impossible to handle device" 117 | -------------------------------------------------------------------------------- /xmlescpos/printer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import usb.core 4 | import usb.util 5 | import serial 6 | import socket 7 | 8 | from escpos import * 9 | from constants import * 10 | from exceptions import * 11 | from time import sleep 12 | 13 | class Usb(Escpos): 14 | """ Define USB printer """ 15 | 16 | def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01): 17 | """ 18 | @param idVendor : Vendor ID 19 | @param idProduct : Product ID 20 | @param interface : USB device interface 21 | @param in_ep : Input end point 22 | @param out_ep : Output end point 23 | """ 24 | 25 | self.errorText = "ERROR PRINTER\n\n\n\n\n\n"+PAPER_FULL_CUT 26 | 27 | self.idVendor = idVendor 28 | self.idProduct = idProduct 29 | self.interface = interface 30 | self.in_ep = in_ep 31 | self.out_ep = out_ep 32 | self.open() 33 | 34 | def open(self): 35 | """ Search device on USB tree and set is as escpos device """ 36 | 37 | self.device = usb.core.find(idVendor=self.idVendor, idProduct=self.idProduct) 38 | if self.device is None: 39 | raise NoDeviceError() 40 | try: 41 | if self.device.is_kernel_driver_active(self.interface): 42 | self.device.detach_kernel_driver(self.interface) 43 | self.device.set_configuration() 44 | usb.util.claim_interface(self.device, self.interface) 45 | except usb.core.USBError as e: 46 | raise HandleDeviceError(e) 47 | 48 | def close(self): 49 | i = 0 50 | while True: 51 | try: 52 | if not self.device.is_kernel_driver_active(self.interface): 53 | usb.util.release_interface(self.device, self.interface) 54 | self.device.attach_kernel_driver(self.interface) 55 | usb.util.dispose_resources(self.device) 56 | else: 57 | self.device = None 58 | return True 59 | except usb.core.USBError as e: 60 | i += 1 61 | if i > 100: 62 | return False 63 | 64 | sleep(0.1) 65 | 66 | def _raw(self, msg): 67 | """ Print any command sent in raw format """ 68 | if len(msg) != self.device.write(self.out_ep, msg, self.interface): 69 | self.device.write(self.out_ep, self.errorText, self.interface) 70 | raise TicketNotPrinted() 71 | 72 | def __extract_status(self): 73 | maxiterate = 0 74 | rep = None 75 | while rep == None: 76 | maxiterate += 1 77 | if maxiterate > 10000: 78 | raise NoStatusError() 79 | r = self.device.read(self.in_ep, 20, self.interface).tolist() 80 | while len(r): 81 | rep = r.pop() 82 | return rep 83 | 84 | def get_printer_status(self): 85 | status = { 86 | 'printer': {}, 87 | 'offline': {}, 88 | 'error' : {}, 89 | 'paper' : {}, 90 | } 91 | 92 | self.device.write(self.out_ep, DLE_EOT_PRINTER, self.interface) 93 | printer = self.__extract_status() 94 | self.device.write(self.out_ep, DLE_EOT_OFFLINE, self.interface) 95 | offline = self.__extract_status() 96 | self.device.write(self.out_ep, DLE_EOT_ERROR, self.interface) 97 | error = self.__extract_status() 98 | self.device.write(self.out_ep, DLE_EOT_PAPER, self.interface) 99 | paper = self.__extract_status() 100 | 101 | status['printer']['status_code'] = printer 102 | status['printer']['status_error'] = not ((printer & 147) == 18) 103 | status['printer']['online'] = not bool(printer & 8) 104 | status['printer']['recovery'] = bool(printer & 32) 105 | status['printer']['paper_feed_on'] = bool(printer & 64) 106 | status['printer']['drawer_pin_high'] = bool(printer & 4) 107 | status['offline']['status_code'] = offline 108 | status['offline']['status_error'] = not ((offline & 147) == 18) 109 | status['offline']['cover_open'] = bool(offline & 4) 110 | status['offline']['paper_feed_on'] = bool(offline & 8) 111 | status['offline']['paper'] = not bool(offline & 32) 112 | status['offline']['error'] = bool(offline & 64) 113 | status['error']['status_code'] = error 114 | status['error']['status_error'] = not ((error & 147) == 18) 115 | status['error']['recoverable'] = bool(error & 4) 116 | status['error']['autocutter'] = bool(error & 8) 117 | status['error']['unrecoverable'] = bool(error & 32) 118 | status['error']['auto_recoverable'] = not bool(error & 64) 119 | status['paper']['status_code'] = paper 120 | status['paper']['status_error'] = not ((paper & 147) == 18) 121 | status['paper']['near_end'] = bool(paper & 12) 122 | status['paper']['present'] = not bool(paper & 96) 123 | 124 | return status 125 | 126 | def __del__(self): 127 | """ Release USB interface """ 128 | if self.device: 129 | self.close() 130 | self.device = None 131 | 132 | 133 | 134 | class Serial(Escpos): 135 | """ Define Serial printer """ 136 | 137 | def __init__(self, devfile="/dev/ttyS0", baudrate=9600, bytesize=8, timeout=1): 138 | """ 139 | @param devfile : Device file under dev filesystem 140 | @param baudrate : Baud rate for serial transmission 141 | @param bytesize : Serial buffer size 142 | @param timeout : Read/Write timeout 143 | """ 144 | self.devfile = devfile 145 | self.baudrate = baudrate 146 | self.bytesize = bytesize 147 | self.timeout = timeout 148 | self.open() 149 | 150 | 151 | def open(self): 152 | """ Setup serial port and set is as escpos device """ 153 | 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) 154 | 155 | if self.device is not None: 156 | print "Serial printer enabled" 157 | else: 158 | print "Unable to open serial printer on: %s" % self.devfile 159 | 160 | 161 | def _raw(self, msg): 162 | """ Print any command sent in raw format """ 163 | self.device.write(msg) 164 | 165 | 166 | def __del__(self): 167 | """ Close Serial interface """ 168 | if self.device is not None: 169 | self.device.close() 170 | 171 | 172 | 173 | class Network(Escpos): 174 | """ Define Network printer """ 175 | 176 | def __init__(self,host,port=9100): 177 | """ 178 | @param host : Printer's hostname or IP address 179 | @param port : Port to write to 180 | """ 181 | self.host = host 182 | self.port = port 183 | self.open() 184 | 185 | 186 | def open(self): 187 | """ Open TCP socket and set it as escpos device """ 188 | self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 189 | self.device.connect((self.host, self.port)) 190 | 191 | if self.device is None: 192 | print "Could not open socket for %s" % self.host 193 | 194 | 195 | def _raw(self, msg): 196 | self.device.send(msg) 197 | 198 | 199 | def __del__(self): 200 | """ Close TCP connection """ 201 | self.device.close() 202 | 203 | -------------------------------------------------------------------------------- /xmlescpos/supported_devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # This is a list of esc/pos compatible usb printers. The vendor and product ids can be found by 4 | # typing lsusb in a linux terminal, this will give you the ids in the form ID VENDOR:PRODUCT 5 | 6 | device_list = [ 7 | { 'vendor' : 0x04b8, 'product' : 0x0e03, 'name' : 'Epson TM-T20' }, 8 | { 'vendor' : 0x04b8, 'product' : 0x0202, 'name' : 'Epson TM-T70' }, 9 | { 'vendor' : 0x04b8, 'product' : 0x0e15, 'name' : 'Epson TM-T20II' }, 10 | ] 11 | 12 | --------------------------------------------------------------------------------