├── 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 |
--------------------------------------------------------------------------------