diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c58997 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# python temporary files +*.pyc + +# editor autosaves, data and file browser files +$~ +.idea/ +.directory + +# temporary data +temp + +# packaging and testing +.tox/ +*.egg-info/ diff --git a/.hgignore b/.hgignore deleted file mode 100644 index aa3937a..0000000 --- a/.hgignore +++ /dev/null @@ -1,10 +0,0 @@ -# python temporary files -syntax: glob -*.pyc - -# editor autosaves -$~ - -# temporary data -syntax: regexp -temp diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..21ba88d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +sudo: false +cache: pip +before_install: + - pip install tox +# command to run tests +script: + - tox \ No newline at end of file diff --git a/doc/api.rst b/doc/api.rst deleted file mode 100644 index a3b00b1..0000000 --- a/doc/api.rst +++ /dev/null @@ -1,46 +0,0 @@ -escpos package -============== - -Submodules ----------- - -escpos.constants module ------------------------ - -.. automodule:: escpos.constants - :members: - :undoc-members: - :show-inheritance: - -escpos.escpos module --------------------- - -.. automodule:: escpos.escpos - :members: - :undoc-members: - :show-inheritance: - -escpos.exceptions module ------------------------- - -.. automodule:: escpos.exceptions - :members: - :undoc-members: - :show-inheritance: - -escpos.printer module ---------------------- - -.. automodule:: escpos.printer - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: escpos - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/constants.rst b/doc/api/constants.rst new file mode 100644 index 0000000..0165d61 --- /dev/null +++ b/doc/api/constants.rst @@ -0,0 +1,10 @@ +Constants +--------- +Module :py:mod:`escpos.constants` + +.. automodule:: escpos.constants + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :member-order: bysource diff --git a/doc/api/escpos.rst b/doc/api/escpos.rst new file mode 100644 index 0000000..2a85256 --- /dev/null +++ b/doc/api/escpos.rst @@ -0,0 +1,10 @@ +Esc/Pos +------- +Module :py:mod:`escpos.escpos` + +.. automodule:: escpos.escpos + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :member-order: bysource diff --git a/doc/api/exceptions.rst b/doc/api/exceptions.rst new file mode 100644 index 0000000..6edb5ea --- /dev/null +++ b/doc/api/exceptions.rst @@ -0,0 +1,9 @@ +Exceptions +---------- +Module :py:mod:`escpos.exceptions` + +.. automodule:: escpos.exceptions + :members: + :inherited-members: + :show-inheritance: + :member-order: bysource \ No newline at end of file diff --git a/doc/api/printer.rst b/doc/api/printer.rst new file mode 100644 index 0000000..fe4554e --- /dev/null +++ b/doc/api/printer.rst @@ -0,0 +1,9 @@ +Printer implementations +----------------------- +Module :py:mod:`escpos.printer` + +.. automodule:: escpos.printer + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource diff --git a/doc/conf.py b/doc/conf.py index dac6859..b110f59 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -34,8 +34,12 @@ extensions = [ 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', + 'sphinx.ext.todo', ] +# enable todos +todo_include_todos = True + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/doc/index.rst b/doc/index.rst index 14cd7be..e8d1fc0 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -25,11 +25,12 @@ sending my PayPal info so you can donate. Thank you! -User Documentation: -------------------- +Content +------- .. toctree:: :maxdepth: 1 + :caption: User Documentation user/dependencies user/installation @@ -39,13 +40,15 @@ User Documentation: user/todo user/usage -API: ----- .. toctree:: :maxdepth: 1 - - api + :caption: API Documentation + + api/escpos + api/printer + api/constants + api/exceptions Indices and tables ================== diff --git a/doc/user/methods.rst b/doc/user/methods.rst index b4ce495..976bde6 100644 --- a/doc/user/methods.rst +++ b/doc/user/methods.rst @@ -2,7 +2,8 @@ Methods ******* -.. note:: **TODO** Merge this page into the API-description. +.. note:: **TODO** Merge this page with the API-description. (Make the API-description more pretty and then + replace this with the API-description.) Escpos class ------------ diff --git a/doc/user/raspi.rst b/doc/user/raspi.rst index 8786768..387c648 100644 --- a/doc/user/raspi.rst +++ b/doc/user/raspi.rst @@ -7,6 +7,11 @@ This instructions were tested on Raspbian. Unless you have done any distro with libusb-1.0 on the Raspberry Pi, the following instructions should works fine on your raspberry distro. +.. warning:: You should **never** directly connect an printer with RS232-interface (serial port) directly to + a Raspberry PI or similar interface (e.g. those simple USB-sticks without encasing). Those interfaces are + based on 5V- or 3,3V-logic (the latter in the case of Raspberry PI). Classical RS232 uses 12V-logic and would + **thus destroy your interface**. Connect both systems with an appropriate *level shifter*. + Dependencies ------------ diff --git a/doc/user/todo.rst b/doc/user/todo.rst index 3777a7d..8e79fde 100644 --- a/doc/user/todo.rst +++ b/doc/user/todo.rst @@ -21,6 +21,7 @@ Testing ~~~~~~~ * Test on many printers as possible (USB, Serial, Network) +* automate testing Design ~~~~~~ @@ -32,4 +33,11 @@ Design * Windows compatibility (hidapi instead libusb?) * PDF417 support +* use something similar to the `capabilities` in escpos-php + +Todos in the codebase +~~~~~~~~~~~~~~~~~~~~~ + +.. todolist:: + diff --git a/escpos/__init__.py b/escpos/__init__.py index 22a5af6..1d72614 100644 --- a/escpos/__init__.py +++ b/escpos/__init__.py @@ -1 +1,5 @@ -__all__ = ["constants","escpos","exceptions","printer"] +import constants +import escpos +import exceptions +import printer +__all__ = ["constants", "escpos", "exceptions", "printer"] diff --git a/escpos/cli.py b/escpos/cli.py new file mode 100755 index 0000000..4b52381 --- /dev/null +++ b/escpos/cli.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +"""A simple command-line interface for common python-escpos functionality + +Usage: python -m escpos.cli --help + +Dependencies: +- DavisGoglin/python-escpos or better +- A file named weather.png (for the 'test' subcommand) + +Reasons for using the DavisGoglin/python-escpos fork: +- image() accepts a PIL.Image object rather than requiring me to choose + between writing a temporary file to disk or calling a "private" method. +- fullimage() allows me to print images of arbitrary length using slicing. + +How to print unsupported barcodes: + barcode -b 'BARCODE' -e 'code39' -E | convert -density 200% eps:- code.png + python test_escpos.py --images code.png + +Copyright (C) 2014 Stephan Sokolow (deitarion/SSokolow) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +from __future__ import absolute_import + +__author__ = "Stephan Sokolow (deitarion/SSokolow)" +__license__ = "MIT" + +import re + +from escpos import printer + +epson = printer.Usb(0x0416, 0x5011) +# TODO: Un-hardcode this + + +def _print_text_file(path): + """Print the given text file""" + epson.set(align='left') + with open(path, 'rU') as fobj: + for line in fobj: + epson.text(line) + + +def _print_image_file(path): + """Print the given image file.""" + epson.fullimage(path, histeq=False, width=384) + + +def print_files(args): + """The 'print' subcommand""" + for path in args.paths: + if args.images: + _print_image_file(path) + else: + _print_text_file(path) + epson.cut() + +# {{{ 'echo' Subcommand + +KNOWN_BARCODE_TYPES = ['UPC-A', 'UPC-E', 'EAN13', 'ITF'] +re_barcode_escape = re.compile(r'^%(?P\S+)\s(?P[0-9X]+)$') + + +def echo(args): # pylint: disable=unused-argument + """TTY-like line-by-line keyboard-to-printer echo loop.""" + try: + while True: + line = raw_input() + match = re_barcode_escape.match(line) + if match and match.group('type') in KNOWN_BARCODE_TYPES: + bctype, data = match.groups() + epson.barcode(data, bctype, 48, 2, '', '') + epson.set(align='left') + else: + epson.text('{0}\n'.format(line)) + except KeyboardInterrupt: + epson.cut() + +# }}} +# {{{ 'test' Subcommand + +from PIL import Image, ImageDraw + + +def _stall_test(width, height): + """Generate a pattern to detect print glitches due to vertical stalling.""" + img = Image.new('1', (width, height)) + for pos in [(x, y) for y in range(0, height) for x in range(0, width)]: + img.putpixel(pos, not sum(pos) % 10) + return img + + +def _test_basic(): + """The original test code from python-escpos's Usage wiki page""" + epson.set(align='left') + # Print text + epson.text("TODO:\n") # pylint: disable=fixme + epson.text("[ ] Task 1\n") + epson.text("[ ] Task 2\n") + # Print image + # TODO: Bundle an image so this can be used + # epson.image("weather.png") + # Print QR Code (must have a white border to be scanned) + epson.set(align='center') + epson.text("Scan to recall TODO list") # pylint: disable=fixme + epson.qr("http://www.example.com/") + # Print barcode + epson.barcode('1234567890128', 'EAN13', 32, 2, '', '') + # Cut paper + epson.cut() + + +def _test_barcodes(): + """Print test barcodes for all ESCPOS-specified formats.""" + for name, data in ( + # pylint: disable=bad-continuation + ('UPC-A', '123456789012\x00'), + ('UPC-E', '02345036\x00'), + ('EAN13', '1234567890128\x00'), + ('EAN8', '12345670\x00'), + ('CODE39', 'BARCODE12345678\x00'), + ('ITF', '123456\x00'), + ('CODABAR', 'A40156B'), + # TODO: CODE93 and CODE128 + ): + # TODO: Fix the library to restore old alignment somehow + epson.set(align='center') + epson.text('\n{0}\n'.format(name)) + epson.barcode(data, name, 64, 2, '', '') + + +def _test_patterns(width=384, height=255): + """Print a set of test patterns for raster image output.""" + # Test our guess of the paper width + img = Image.new('1', (width, height), color=1) + draw = ImageDraw.Draw(img) + draw.polygon(((0, 0), img.size, (0, img.size[1])), fill=0) + epson.image(img) + del draw, img + + # Test the consistency of printing large data and whether stall rate is + # affected by data rate + epson.image(_stall_test(width, height)) + epson.image(_stall_test(width / 2, height)) + + +def test(args): + """The 'test' subcommand""" + if args.barcodes: + _test_barcodes() + elif args.patterns: + _test_patterns() + else: + _test_basic() + + +# }}} + +def main(): + """Wrapped in a function for import and entry point compatibility""" + # pylint: disable=bad-continuation + + import argparse + + parser = argparse.ArgumentParser( + description="Command-line interface to python-escpos") + subparsers = parser.add_subparsers(title='subcommands') + + echo_parser = subparsers.add_parser('echo', help='Echo the keyboard to ' + 'the printer line-by-line (Exit with Ctrl+C)') + echo_parser.set_defaults(func=echo) + + print_parser = subparsers.add_parser('print', help='Print the given files') + print_parser.add_argument('--images', action='store_true', + help="Provided files are images rather than text files.") + print_parser.add_argument('paths', metavar='path', nargs='+') + print_parser.set_defaults(func=print_files) + + test_parser = subparsers.add_parser('test', help='Print test patterns') + test_modes = test_parser.add_mutually_exclusive_group() + test_modes.add_argument('--barcodes', action='store_true', + help="Test supported barcode types (Warning: Some printers must be " + "reset after attempting an unsupported barcode type.)") + test_modes.add_argument('--patterns', action='store_true', + help="Print test patterns") + test_parser.set_defaults(func=test) + + args = parser.parse_args() + args.func(args) + + +if __name__ == '__main__': + main() + +# vim: set sw=4 sts=4 : diff --git a/escpos/constants.py b/escpos/constants.py index 09a833a..c7e5309 100644 --- a/escpos/constants.py +++ b/escpos/constants.py @@ -1,86 +1,136 @@ -""" ESC/POS Commands (Constants) """ +""" Set of ESC/POS Commands (Constants) + +This module contains constants that are described in the esc/pos-documentation. +Since there is no definitive and unified specification for all esc/pos-like printers the constants could later be +moved to `capabilities` as in `escpos-php by @mike42 `_. + +:author: `Manuel F Martinez `_ and others +:organization: Bashlinux and `python-escpos `_ +:copyright: Copyright (c) 2012 Bashlinux +:license: GNU GPL v3 +""" + +# Control characters +# as labelled in http://www.novopos.ch/client/EPSON/TM-T20/TM-T20_eng_qr.pdf +NUL = '\x00' +EOT = '\x04' +ENQ = '\x05' +DLE = '\x10' +DC4 = '\x14' +CAN = '\x18' +ESC = '\x1b' +FS = '\x1c' +GS = '\x1d' # Feed control sequences -CTL_LF = '\x0a' # Print and line feed -CTL_FF = '\x0c' # Form feed -CTL_CR = '\x0d' # Carriage return -CTL_HT = '\x09' # Horizontal tab -CTL_SET_HT = '\x1b\x44' # Set horizontal tab positions -CTL_VT = '\x1b\x64\x04' # Vertical tab +CTL_LF = '\n' # Print and line feed +CTL_FF = '\f' # Form feed +CTL_CR = '\r' # Carriage return +CTL_HT = '\t' # Horizontal tab +CTL_SET_HT = ESC + '\x44' # Set horizontal tab positions +CTL_VT = '\v' # Vertical tab + # Printer hardware -HW_INIT = '\x1b\x40' # Clear data in buffer and reset modes -HW_SELECT = '\x1b\x3d\x01' # Printer select -HW_RESET = '\x1b\x3f\x0a\x00' # Reset printer hardware -# Cash Drawer -CD_KICK_2 = '\x1b\x70\x00' # Sends a pulse to pin 2 [] -CD_KICK_5 = '\x1b\x70\x01' # Sends a pulse to pin 5 [] -# Paper -PAPER_FULL_CUT = '\x1d\x56\x00' # Full cut paper -PAPER_PART_CUT = '\x1d\x56\x01' # Partial cut paper -# Text format -TXT_NORMAL = '\x1b\x21\x00' # Normal text -TXT_2HEIGHT = '\x1b\x21\x10' # Double height text -TXT_2WIDTH = '\x1b\x21\x20' # Double width text -TXT_4SQUARE = '\x1b\x21\x30' # Quad area text -TXT_UNDERL_OFF = '\x1b\x2d\x00' # Underline font OFF -TXT_UNDERL_ON = '\x1b\x2d\x01' # Underline font 1-dot ON -TXT_UNDERL2_ON = '\x1b\x2d\x02' # Underline font 2-dot ON -TXT_BOLD_OFF = '\x1b\x45\x00' # Bold font OFF -TXT_BOLD_ON = '\x1b\x45\x01' # Bold font ON -TXT_FONT_A = '\x1b\x4d\x00' # Font type A -TXT_FONT_B = '\x1b\x4d\x01' # Font type B -TXT_ALIGN_LT = '\x1b\x61\x00' # Left justification -TXT_ALIGN_CT = '\x1b\x61\x01' # Centering -TXT_ALIGN_RT = '\x1b\x61\x02' # Right justification +HW_INIT = ESC + '@' # Clear data in buffer and reset modes +HW_SELECT = ESC + '=\x01' # Printer select + +HW_RESET = ESC + '\x3f\x0a\x00' # Reset printer hardware + # (TODO: Where is this specified?) + +#{ Cash Drawer (ESC p ) +_CASH_DRAWER = lambda m, t1='', t2='': ESC + 'p' + m + chr(t1) + chr(t2) +CD_KICK_2 = _CASH_DRAWER('\x00', 50, 50) # Sends a pulse to pin 2 [] +CD_KICK_5 = _CASH_DRAWER('\x01', 50, 50) # Sends a pulse to pin 5 [] + +# Paper Cutter +_CUT_PAPER = lambda m: GS + 'V' + m +PAPER_FULL_CUT = _CUT_PAPER('\x00') # Full cut paper +PAPER_PART_CUT = _CUT_PAPER('\x01') # Partial cut paper + +# Text format +# TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll +# Printers" and tidy up this stuff too. +TXT_NORMAL = ESC + '!\x00' # Normal text +TXT_2HEIGHT = ESC + '!\x10' # Double height text +TXT_2WIDTH = ESC + '!\x20' # Double width text +TXT_4SQUARE = ESC + '!\x30' # Quad area text +TXT_UNDERL_OFF = ESC + '\x2d\x00' # Underline font OFF +TXT_UNDERL_ON = ESC + '\x2d\x01' # Underline font 1-dot ON +TXT_UNDERL2_ON = ESC + '\x2d\x02' # Underline font 2-dot ON +TXT_BOLD_OFF = ESC + '\x45\x00' # Bold font OFF +TXT_BOLD_ON = ESC + '\x45\x01' # Bold font ON +TXT_FONT_A = ESC + '\x4d\x00' # Font type A +TXT_FONT_B = ESC + '\x4d\x01' # Font type B +TXT_ALIGN_LT = ESC + '\x61\x00' # Left justification +TXT_ALIGN_CT = ESC + '\x61\x01' # Centering +TXT_ALIGN_RT = ESC + '\x61\x02' # Right justification + # Char code table -CHARCODE_PC437 = '\x1b\x74\x00' # USA: Standard Europe -CHARCODE_JIS = '\x1b\x74\x01' # Japanese Katakana -CHARCODE_PC850 = '\x1b\x74\x02' # Multilingual -CHARCODE_PC860 = '\x1b\x74\x03' # Portuguese -CHARCODE_PC863 = '\x1b\x74\x04' # Canadian-French -CHARCODE_PC865 = '\x1b\x74\x05' # Nordic -CHARCODE_WEU = '\x1b\x74\x06' # Simplified Kanji, Hirakana -CHARCODE_GREEK = '\x1b\x74\x07' # Simplified Kanji -CHARCODE_HEBREW = '\x1b\x74\x08' # Simplified Kanji -CHARCODE_PC1252 = '\x1b\x74\x10' # Western European Windows Code Set -CHARCODE_PC866 = '\x1b\x74\x12' # Cirillic #2 -CHARCODE_PC852 = '\x1b\x74\x13' # Latin 2 -CHARCODE_PC858 = '\x1b\x74\x14' # Euro -CHARCODE_THAI42 = '\x1b\x74\x15' # Thai character code 42 -CHARCODE_THAI11 = '\x1b\x74\x16' # Thai character code 11 -CHARCODE_THAI13 = '\x1b\x74\x17' # Thai character code 13 -CHARCODE_THAI14 = '\x1b\x74\x18' # Thai character code 14 -CHARCODE_THAI16 = '\x1b\x74\x19' # Thai character code 16 -CHARCODE_THAI17 = '\x1b\x74\x1a' # Thai character code 17 -CHARCODE_THAI18 = '\x1b\x74\x1b' # Thai character code 18 +CHARCODE_PC437 = ESC + '\x74\x00' # USA: Standard Europe +CHARCODE_JIS = ESC + '\x74\x01' # Japanese Katakana +CHARCODE_PC850 = ESC + '\x74\x02' # Multilingual +CHARCODE_PC860 = ESC + '\x74\x03' # Portuguese +CHARCODE_PC863 = ESC + '\x74\x04' # Canadian-French +CHARCODE_PC865 = ESC + '\x74\x05' # Nordic +CHARCODE_WEU = ESC + '\x74\x06' # Simplified Kanji, Hirakana +CHARCODE_GREEK = ESC + '\x74\x07' # Simplified Kanji +CHARCODE_HEBREW = ESC + '\x74\x08' # Simplified Kanji +CHARCODE_PC1252 = ESC + '\x74\x11' # Western European Windows Code Set +CHARCODE_PC866 = ESC + '\x74\x12' # Cirillic #2 +CHARCODE_PC852 = ESC + '\x74\x13' # Latin 2 +CHARCODE_PC858 = ESC + '\x74\x14' # Euro +CHARCODE_THAI42 = ESC + '\x74\x15' # Thai character code 42 +CHARCODE_THAI11 = ESC + '\x74\x16' # Thai character code 11 +CHARCODE_THAI13 = ESC + '\x74\x17' # Thai character code 13 +CHARCODE_THAI14 = ESC + '\x74\x18' # Thai character code 14 +CHARCODE_THAI16 = ESC + '\x74\x19' # Thai character code 16 +CHARCODE_THAI17 = ESC + '\x74\x1a' # Thai character code 17 +CHARCODE_THAI18 = ESC + '\x74\x1b' # Thai character code 18 + # Barcode format -BARCODE_TXT_OFF = '\x1d\x48\x00' # HRI barcode chars OFF -BARCODE_TXT_ABV = '\x1d\x48\x01' # HRI barcode chars above -BARCODE_TXT_BLW = '\x1d\x48\x02' # HRI barcode chars below -BARCODE_TXT_BTH = '\x1d\x48\x03' # HRI barcode chars both above and below -BARCODE_FONT_A = '\x1d\x66\x00' # Font type A for HRI barcode chars -BARCODE_FONT_B = '\x1d\x66\x01' # Font type B for HRI barcode chars -BARCODE_HEIGHT = '\x1d\x68\x64' # Barcode Height [1-255] -BARCODE_WIDTH = '\x1d\x77\x03' # Barcode Width [2-6] -BARCODE_UPC_A = '\x1d\x6b\x00' # Barcode type UPC-A -BARCODE_UPC_E = '\x1d\x6b\x01' # Barcode type UPC-E -BARCODE_EAN13 = '\x1d\x6b\x02' # Barcode type EAN13 -BARCODE_EAN8 = '\x1d\x6b\x03' # Barcode type EAN8 -BARCODE_CODE39 = '\x1d\x6b\x04' # Barcode type CODE39 -BARCODE_ITF = '\x1d\x6b\x05' # Barcode type ITF -BARCODE_NW7 = '\x1d\x6b\x06' # Barcode type NW7 -# Image format -S_RASTER_N = '\x1d\x76\x30\x00' # Set raster image normal size -S_RASTER_2W = '\x1d\x76\x30\x01' # Set raster image double width -S_RASTER_2H = '\x1d\x76\x30\x02' # Set raster image double height -S_RASTER_Q = '\x1d\x76\x30\x03' # Set raster image quadruple +_SET_BARCODE_TXT_POS = lambda n: GS + 'H' + n +BARCODE_TXT_OFF = _SET_BARCODE_TXT_POS('\x00') # HRI barcode chars OFF +BARCODE_TXT_ABV = _SET_BARCODE_TXT_POS('\x01') # HRI barcode chars above +BARCODE_TXT_BLW = _SET_BARCODE_TXT_POS('\x02') # HRI barcode chars below +BARCODE_TXT_BTH = _SET_BARCODE_TXT_POS('\x03') # HRI both above and below + +_SET_HRI_FONT = lambda n: GS + 'f' + n +BARCODE_FONT_A = _SET_HRI_FONT('\x00') # Font type A for HRI barcode chars +BARCODE_FONT_B = _SET_HRI_FONT('\x01') # Font type B for HRI barcode chars + +BARCODE_HEIGHT = GS + 'h' # Barcode Height [1-255] +BARCODE_WIDTH = GS + 'w' # Barcode Width [2-6] + +#NOTE: This isn't actually an ESC/POS command. It's the common prefix to the +# two "print bar code" commands: +# - "GS k NUL" +# - "GS k " +# The latter command supports more barcode types +_SET_BARCODE_TYPE = lambda m: GS + 'k' + m +BARCODE_UPC_A = _SET_BARCODE_TYPE('\x00') # Barcode type UPC-A +BARCODE_UPC_E = _SET_BARCODE_TYPE('\x01') # Barcode type UPC-E +BARCODE_EAN13 = _SET_BARCODE_TYPE('\x02') # Barcode type EAN13 +BARCODE_EAN8 = _SET_BARCODE_TYPE('\x03') # Barcode type EAN8 +BARCODE_CODE39 = _SET_BARCODE_TYPE('\x04') # Barcode type CODE39 +BARCODE_ITF = _SET_BARCODE_TYPE('\x05') # Barcode type ITF +BARCODE_NW7 = _SET_BARCODE_TYPE('\x06') # Barcode type NW7 + +# Image format +# NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image" +# command. The constants include a fragment of the data's header. +_PRINT_RASTER_IMG = lambda data: GS + 'v0' + data +S_RASTER_N = _PRINT_RASTER_IMG('\x00') # Set raster image normal size +S_RASTER_2W = _PRINT_RASTER_IMG('\x01') # Set raster image double width +S_RASTER_2H = _PRINT_RASTER_IMG('\x02') # Set raster image double height +S_RASTER_Q = _PRINT_RASTER_IMG('\x03') # Set raster image quadruple + # Printing Density -PD_N50 = '\x1d\x7c\x00' # Printing Density -50% -PD_N37 = '\x1d\x7c\x01' # Printing Density -37.5% -PD_N25 = '\x1d\x7c\x02' # Printing Density -25% -PD_N12 = '\x1d\x7c\x03' # Printing Density -12.5% -PD_0 = '\x1d\x7c\x04' # Printing Density 0% -PD_P50 = '\x1d\x7c\x08' # Printing Density +50% -PD_P37 = '\x1d\x7c\x07' # Printing Density +37.5% -PD_P25 = '\x1d\x7c\x06' # Printing Density +25% -PD_P12 = '\x1d\x7c\x05' # Printing Density +12.5% +PD_N50 = GS + '\x7c\x00' # Printing Density -50% +PD_N37 = GS + '\x7c\x01' # Printing Density -37.5% +PD_N25 = GS + '\x7c\x02' # Printing Density -25% +PD_N12 = GS + '\x7c\x03' # Printing Density -12.5% +PD_0 = GS + '\x7c\x04' # Printing Density 0% +PD_P50 = GS + '\x7c\x08' # Printing Density +50% +PD_P37 = GS + '\x7c\x07' # Printing Density +37.5% +PD_P25 = GS + '\x7c\x06' # Printing Density +25% +PD_P12 = GS + '\x7c\x05' # Printing Density +12.5% diff --git a/escpos/escpos.py b/escpos/escpos.py index 045a4fd..b8b8342 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -1,9 +1,12 @@ #!/usr/bin/python -""" -@author: Manuel F Martinez -@organization: Bashlinux -@copyright: Copyright (c) 2012 Bashlinux -@license: GNU GPL v3 +""" Main class + +This module contains the abstract base class :py:class:`Escpos`. + +:author: `Manuel F Martinez `_ and others +:organization: Bashlinux and `python-escpos `_ +:copyright: Copyright (c) 2012 Bashlinux +:license: GNU GPL v3 """ try: @@ -12,62 +15,98 @@ except ImportError: from PIL import Image import qrcode -import time +import textwrap +import binascii +import operator -from constants import * -from exceptions import * +from .constants import * +from .exceptions import * -class Escpos: - """ ESC/POS Printer object """ - device = None +from abc import ABCMeta, abstractmethod # abstract base class support - def _check_image_size(self, size): - """ Check and fix the size of the image to 32 bits """ +class Escpos(object): + """ ESC/POS Printer object + + This class is the abstract base class for an esc/pos-printer. The printer implementations are children of this + class. + """ + __metaclass__ = ABCMeta + device = None + + def __init__(self, columns=32): + """ Initialize ESCPOS Printer + + :param columns: Text columns used by the printer. Defaults to 32.""" + self.columns = columns + + @abstractmethod + def _raw(self, msg): + """ Sends raw data to the printer + + This function has to be individually implemented by the implementations. + + :param msg: message string to be sent to the printer + """ + pass + + @staticmethod + def _check_image_size(size): + """ Check and fix the size of the image to 32 bits + + :param size: size of the image + :returns: tuple of image borders + :rtype: (int, int) + """ if size % 32 == 0: - return (0, 0) + return 0, 0 else: image_border = 32 - (size % 32) if (image_border % 2) == 0: - return (image_border / 2, image_border / 2) + return round(image_border / 2), round(image_border / 2) else: - return (image_border / 2, (image_border / 2) + 1) - + return round(image_border / 2), round((image_border / 2) + 1) def _print_image(self, line, size): - """ Print formatted image """ + """ Print formatted image + + :param line: + :param size: + """ i = 0 cont = 0 - buffer = "" - + pbuffer = "" + self._raw(S_RASTER_N) - buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1]&0xff, size[1]>>8) - self._raw(buffer.decode('hex')) - buffer = "" + pbuffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1] & 0xff, size[1] >> 8) + self._raw(binascii.unhexlify(pbuffer)) + pbuffer = "" while i < len(line): - hex_string = int(line[i:i+8],2) - buffer += "%02X" % hex_string + hex_string = int(line[i:i+8], 2) + pbuffer += "%02X" % hex_string i += 8 cont += 1 if cont % 4 == 0: - self._raw(buffer.decode("hex")) - buffer = "" + self._raw(binascii.unhexlify(pbuffer)) + pbuffer = "" cont = 0 - def _convert_image(self, im): - """ Parse image and prepare it to a printable format """ - pixels = [] - pix_line = "" - im_left = "" - im_right = "" - switch = 0 - img_size = [ 0, 0 ] + """ Parse image and prepare it to a printable format + :param im: image data + :raises: :py:exc:`~escpos.exceptions.ImageSizeError` + """ + pixels = [] + pix_line = "" + im_left = "" + im_right = "" + switch = 0 + img_size = [0, 0] if im.size[0] > 512: - print ("WARNING: Image is wider than 512 and could be truncated at print time ") + print ("WARNING: Image is wider than 512 and could be truncated at print time ") if im.size[1] > 0xffff: raise ImageSizeError() @@ -87,7 +126,7 @@ class Escpos: im_color = (RGB[0] + RGB[1] + RGB[2]) im_pattern = "1X0" pattern_len = len(im_pattern) - switch = (switch - 1 ) * (-1) + switch = (switch - 1) * (-1) for x in range(pattern_len): if im_color <= (255 * 3 / pattern_len * (x+1)): if im_pattern[x] == "X": @@ -95,34 +134,124 @@ class Escpos: else: pix_line += im_pattern[x] break - elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3): + elif (255 * 3 / pattern_len * pattern_len) < im_color <= (255 * 3): pix_line += im_pattern[-1] - break + break pix_line += im_right img_size[0] += im_border[1] self._print_image(pix_line, img_size) + def image(self, path_img): + """ Open and print an image file - def image(self,path_img): - """ Open image file """ + Prints an image. The image is automatically adjusted in size in order to print it. + + .. todo:: Seems to be broken. Write test that simply executes function with a dummy printer in order to + check for bugs like these in the future. + + :param path_img: complete filename and path to image of type `jpg`, `gif`, `png` or `bmp` + """ im_open = Image.open(path_img) - # Remove the alpha channel on transparent images - if im_open.mode == 'RGBA': - im_open.load() - im = Image.new("RGB", im_open.size, (255, 255, 255)) - im.paste(im_open, mask=im_open.split()[3]) - else: - im = im_open.convert("RGB") + # Remove the alpha channel on transparent images + if im_open.mode == 'RGBA': + im_open.load() + im = Image.new("RGB", im_open.size, (255, 255, 255)) + im.paste(im_open, mask=im_open.split()[3]) + else: + im = im_open.convert("RGB") # Convert the RGB image in printable image self._convert_image(im) + def fullimage(self, img, max_height=860, width=512, histeq=True, bandsize=255): + """ Resizes and prints an arbitrarily sized image - def qr(self,text): - """ Print QR Code for the provided string """ - qr_code = qrcode.QRCode(version=4, box_size=4, border=1) + .. todo:: Seems to be broken. Write test that simply executes function with a dummy printer in order to + check for bugs like these in the future. + """ + if isinstance(img, (Image, Image.Image)): + im = img.convert("RGB") + else: + im = Image.open(img).convert("RGB") + + if histeq: + # Histogram equaliztion + h = im.histogram() + lut = [] + for b in range(0, len(h), 256): + # step size + step = reduce(operator.add, h[b:b+256]) / 255 + # create equalization lookup table + n = 0 + for i in range(256): + lut.append(n / step) + n = n + h[i+b] + im = im.point(lut) + + if width: + ratio = float(width) / im.size[0] + newheight = int(ratio * im.size[1]) + + # Resize the image + im = im.resize((width, newheight), Image.ANTIALIAS) + + if max_height and im.size[1] > max_height: + im = im.crop((0, 0, im.size[0], max_height)) + + # Divide into bands + current = 0 + while current < im.size[1]: + self.image(im.crop((0, current, width or im.size[0], + min(im.size[1], current + bandsize)))) + current += bandsize + + def direct_image(self, image): + """ Send image to printer + + :param image: + """ + mask = 0x80 + i = 0 + temp = 0 + + (width, height) = image.size + self._raw(S_RASTER_N) + headerX = int(width / 8) + headerY = height + buf = "%02X" % (headerX & 0xff) + buf += "%02X" % ((headerX >> 8) & 0xff) + buf += "%02X" % (headerY & 0xff) + buf += "%02X" % ((headerY >> 8) & 0xff) + #self._raw(binascii.unhexlify(buf)) + for y in range(height): + for x in range(width): + value = image.getpixel((x, y)) + value |= (value << 8) + if value == 0: + temp |= mask + + mask >>= 1 + + i += 1 + if i == 8: + buf += ("%02X" % temp) + mask = 0x80 + i = 0 + temp = 0 + self._raw(binascii.unhexlify(bytes(buf, "ascii"))) + self._raw('\n') + + def qr(self, text): + """ Print QR Code for the provided string + + Prints a QR-code. The size has been adjusted to version 4, so it is small enough to be + printed but also big enough to be read by a smartphone. + + :param text: text to generate a QR-Code from + """ + qr_code = qrcode.QRCode(version=4, box_size=4, border=1, error_correction=qrcode.constants.ERROR_CORRECT_H) qr_code.add_data(text) qr_code.make(fit=True) qr_img = qr_code.make_image() @@ -131,9 +260,15 @@ class Escpos: # Convert the RGB image in printable image self._convert_image(im) + def charcode(self, code): + """ Set Character Code Table - def charcode(self,code): - """ Set Character Code Table """ + Sends the control sequence from :py:mod:`escpos.constants` to the printer + with :py:meth:`escpos.printer.'implementation'._raw()`. + + :param code: Name of CharCode + :raises: :py:exc:`~escpos.exceptions.CharCodeError` + """ if code.upper() == "USA": self._raw(CHARCODE_PC437) elif code.upper() == "JIS": @@ -152,8 +287,8 @@ class Escpos: self._raw(CHARCODE_GREEK) elif code.upper() == "HEBREW": self._raw(CHARCODE_HEBREW) - elif code.upper() == "LATVIAN": - self._raw(CHARCODE_PC755) + # elif code.upper() == "LATVIAN": # this is not listed in the constants + # self._raw(CHARCODE_PC755) elif code.upper() == "WPC1252": self._raw(CHARCODE_PC1252) elif code.upper() == "CIRILLIC2": @@ -179,24 +314,86 @@ class Escpos: else: raise CharCodeError() - def barcode(self, code, bc, width, height, pos, font): - """ Print Barcode """ + def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True): + """ Print Barcode + + This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to + be supported by the unit. Currently you have to check manually whether your barcode text is correct. Uncorrect + barcodes may lead to unexpected printer behaviour. + + .. todo:: Add a method to check barcode codes. Alternatively or as an addition write explanations about each + barcode-type. Research whether the check digits can be computed autmatically. + + Use the parameters `height` and `width` for adjusting of the barcode size. Please take notice that the barcode + will not be printed if it is outside of the printable area. (Which should be impossible with this method, so + this information is probably more useful for debugging purposes.) + + .. todo:: On TM-T88II width from 1 to 6 is accepted. Try to acquire command reference and correct the code. + .. todo:: Supplying pos does not have an effect for every barcode type. Check and document for which types this + is true. + + If you do not want to center the barcode you can call the method with `align_ct=False`, which will disable + automatic centering. Please note that when you use center alignment, then the alignment of text will be changed + automatically to centered. You have to manually restore the alignment if necessary. + + .. todo:: If further barcode-types are needed they could be rendered transparently as an image. (This could also + be of help if the printer does not support types that others do.) + + :param code: alphanumeric data to be printed as bar code + :param bc: barcode format, possible values are: + + * UPC-A + * UPC-E + * EAN13 + * EAN8 + * CODE39 + * ITF + * NW7 + + If none is specified, the method raises :py:exc:`~escpos.exceptions.BarcodeTypeError`. + :param height: barcode height, has to be between 1 and 255 + *default*: 64 + :type height: int + :param width: barcode width, has to be between 2 and 6 + *default*: 3 + :type width: int + :param pos: where to place the text relative to the barcode, *default*: BELOW + + * ABOVE + * BELOW + * BOTH + * OFF + + :param font: select font (see ESC/POS-documentation, the device often has two fonts), *default*: A + + * A + * B + + :param align_ct: If this parameter is True the barcode will be centered. Otherwise no alignment command will be + issued. + :type align_ct: bool + + :raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`, + :py:exc:`~escpos.exceptions.BarcodeTypeError`, + :py:exc:`~escpos.exceptions.BarcodeCodeError` + """ # Align Bar Code() - self._raw(TXT_ALIGN_CT) + if align_ct: + self._raw(TXT_ALIGN_CT) # Height - if height >=2 or height <=6: - self._raw(BARCODE_HEIGHT) + if 1 <= height <= 255: + self._raw(BARCODE_HEIGHT + chr(height)) else: - raise BarcodeSizeError() + raise BarcodeSizeError("height = {height}".format(height=height)) # Width - if width >= 1 or width <=255: - self._raw(BARCODE_WIDTH) + if 2 <= width <= 6: + self._raw(BARCODE_WIDTH + chr(width)) else: - raise BarcodeSizeError() + raise BarcodeSizeError("width = {width}".format(width=width)) # Font if font.upper() == "B": self._raw(BARCODE_FONT_B) - else: # DEFAULT FONT: A + else: # DEFAULT FONT: A self._raw(BARCODE_FONT_A) # Position if pos.upper() == "OFF": @@ -205,9 +402,9 @@ class Escpos: self._raw(BARCODE_TXT_BTH) elif pos.upper() == "ABOVE": self._raw(BARCODE_TXT_ABV) - else: # DEFAULT POSITION: BELOW + else: # DEFAULT POSITION: BELOW self._raw(BARCODE_TXT_BLW) - # Type + # Type if bc.upper() == "UPC-A": self._raw(BARCODE_UPC_A) elif bc.upper() == "UPC-E": @@ -220,27 +417,66 @@ class Escpos: self._raw(BARCODE_CODE39) elif bc.upper() == "ITF": self._raw(BARCODE_ITF) - elif bc.upper() == "NW7": + elif bc.upper() in ("NW7", "CODABAR"): self._raw(BARCODE_NW7) else: - raise BarcodeTypeError() + raise BarcodeTypeError(bc) # Print Code if code: self._raw(code) else: - raise exception.BarcodeCodeError() + raise BarcodeCodeError() - def text(self, txt): - """ Print alpha-numeric text """ + """ Print alpha-numeric text + + The text has to be encoded in the currently selected codepage. + + :param txt: text to be printed + :raises: :py:exc:`~escpos.exceptions.TextError` + """ if txt: self._raw(txt) else: + # TODO: why is it problematic to print an empty string? raise TextError() + def block_text(self, txt, columns=None): + """ Text is printed wrapped to specified columns - def set(self, align='left', font='a', type='normal', width=1, height=1, density=9): - """ Set text properties """ + :param txt: text to be printed + :param columns: amount of columns + :return: None + """ + colCount = self.columns if columns is None else columns + self.text(textwrap.fill(txt, colCount)) + + def set(self, align='left', font='a', text_type='normal', width=1, height=1, density=9): + """ Set text properties by sending them to the printer + + :param align: horizontal position for text, possible values are: + + * CENTER + * LEFT + * RIGHT + + *default*: LEFT + :param font: font type, possible values are A or B, *default*: A + :param text_type: text type, possible values are: + + * B for bold + * U for underlined + * B2 for bold, version 2 + * U2 for underlined, version 2 + * BU for bold and underlined + * BU2 for bold and underlined, version 2 + * NORMAL for normal text + + *default*: NORMAL + :param width: text width, normal (1) or double width (2), *default*: 1 + :param height: text height, normal (1) or double height (2), *default*: 1 + :param density: print density, value from 0-8, if something else is supplied the density remains unchanged + """ # Width if height == 2 and width == 2: self._raw(TXT_NORMAL) @@ -251,25 +487,25 @@ class Escpos: elif width == 2 and height != 2: self._raw(TXT_NORMAL) self._raw(TXT_2WIDTH) - else: # DEFAULT SIZE: NORMAL + else: # DEFAULT SIZE: NORMAL self._raw(TXT_NORMAL) # Type - if type.upper() == "B": + if text_type.upper() == "B": self._raw(TXT_BOLD_ON) self._raw(TXT_UNDERL_OFF) - elif type.upper() == "U": + elif text_type.upper() == "U": self._raw(TXT_BOLD_OFF) self._raw(TXT_UNDERL_ON) - elif type.upper() == "U2": + elif text_type.upper() == "U2": self._raw(TXT_BOLD_OFF) self._raw(TXT_UNDERL2_ON) - elif type.upper() == "BU": + elif text_type.upper() == "BU": self._raw(TXT_BOLD_ON) self._raw(TXT_UNDERL_ON) - elif type.upper() == "BU2": + elif text_type.upper() == "BU2": self._raw(TXT_BOLD_ON) self._raw(TXT_UNDERL2_ON) - elif type.upper == "NORMAL": + elif text_type.upper == "NORMAL": self._raw(TXT_BOLD_OFF) self._raw(TXT_UNDERL_OFF) # Font @@ -303,23 +539,35 @@ class Escpos: self._raw(PD_P37) elif density == 8: self._raw(PD_P50) - else:# DEFAULT: DOES NOTHING + else: # DEFAULT: DOES NOTHING pass - def cut(self, mode=''): - """ Cut paper """ + """ Cut paper. + + Without any arguments the paper will be cut completely. With 'mode=PART' a partial cut will + be attempted. Note however, that not all models can do a partial cut. See the documentation of + your printer for details. + .. todo:: Check this function on TM-T88II. + + :param mode: set to 'PART' for a partial cut + """ # Fix the size between last line and cut # TODO: handle this with a line feed self._raw("\n\n\n\n\n\n") if mode.upper() == "PART": self._raw(PAPER_PART_CUT) - else: # DEFAULT MODE: FULL CUT + else: # DEFAULT MODE: FULL CUT self._raw(PAPER_FULL_CUT) - def cashdraw(self, pin): - """ Send pulse to kick the cash drawer """ + """ Send pulse to kick the cash drawer + + Kick cash drawer on pin 2 or pin 5 according to parameter. + + :param pin: pin number, 2 or 5 + :raises: :py:exc:`~escpos.exceptions.CashDrawerError` + """ if pin == 2: self._raw(CD_KICK_2) elif pin == 5: @@ -327,26 +575,43 @@ class Escpos: else: raise CashDrawerError() - def hw(self, hw): - """ Hardware operations """ + """ Hardware operations + + :param hw: hardware action, may be: + + * INIT + * SELECT + * RESET + """ if hw.upper() == "INIT": self._raw(HW_INIT) elif hw.upper() == "SELECT": self._raw(HW_SELECT) elif hw.upper() == "RESET": self._raw(HW_RESET) - else: # DEFAULT: DOES NOTHING + else: # DEFAULT: DOES NOTHING pass - def control(self, ctl, pos=4): - """ Feed control sequences """ + """ Feed control sequences + + :param ctl: string for the following control sequences: + + * LF *for Line Feed* + * FF *for Form Feed* + * CR *for Carriage Return* + * HT *for Horizontal Tab* + * VT *for Vertical Tab* + + :param pos: integer between 1 and 16, controls the horizontal tab position + :raises: :py:exc:`~escpos.exceptions.TabPosError` + """ # Set tab positions if pos < 1 or pos > 16: - raise TabError() + raise TabPosError() else: - self._raw("".join([CTL_SET_HT,hex(pos)])) + self._raw("".join([CTL_SET_HT, hex(pos)])) # Set position if ctl.upper() == "LF": self._raw(CTL_LF) diff --git a/escpos/exceptions.py b/escpos/exceptions.py index 0d482aa..e91ed5e 100644 --- a/escpos/exceptions.py +++ b/escpos/exceptions.py @@ -1,6 +1,24 @@ -""" ESC/POS Exceptions classes """ +""" ESC/POS Exceptions classes + +Result/Exit codes: + + - `0` = success + - `10` = No Barcode type defined :py:exc:`~escpos.exceptions.BarcodeTypeError` + - `20` = Barcode size values are out of range :py:exc:`~escpos.exceptions.BarcodeSizeError` + - `30` = Barcode text not supplied :py:exc:`~escpos.exceptions.BarcodeCodeError` + - `40` = Image height is too large :py:exc:`~escpos.exceptions.ImageSizeError` + - `50` = No string supplied to be printed :py:exc:`~escpos.exceptions.TextError` + - `60` = Invalid pin to send Cash Drawer pulse :py:exc:`~escpos.exceptions.CashDrawerError` + - `70` = Invalid number of tab positions :py:exc:`~escpos.exceptions.TabPosError` + - `80` = Invalid char code :py:exc:`~escpos.exceptions.CharCodeError` + - `90` = USB device not found :py:exc:`~escpos.exceptions.USBNotFoundError` + +:author: `Manuel F Martinez `_ and others +:organization: Bashlinux and `python-escpos `_ +:copyright: Copyright (c) 2012 Bashlinux +:license: GNU GPL v3 +""" -import os class Error(Exception): """ Base class for ESC/POS errors """ @@ -14,46 +32,59 @@ class Error(Exception): def __str__(self): return self.msg -# Result/Exit codes -# 0 = success -# 10 = No Barcode type defined -# 20 = Barcode size values are out of range -# 30 = Barcode text not supplied -# 40 = Image height is too large -# 50 = No string supplied to be printed -# 60 = Invalid pin to send Cash Drawer pulse -# 70 = Invalid number of tab positions -# 80 = Invalid char code - class BarcodeTypeError(Error): + """ No Barcode type defined. + + This exception indicates that no known barcode-type has been entered. The barcode-type has to be + one of those specified in :py:meth:`escpos.escpos.Escpos.barcode`. + The returned error code is `10`. + """ def __init__(self, msg=""): Error.__init__(self, msg) self.msg = msg self.resultcode = 10 def __str__(self): - return "No Barcode type is defined" + return "No Barcode type is defined ({msg})".format(msg=self.msg) + class BarcodeSizeError(Error): + """ Barcode size is out of range. + + This exception indicates that the values for the barcode size are out of range. + The size of the barcode has to be in the range that is specified in :py:meth:`escpos.escpos.Escpos.barcode`. + The resulting returncode is `20`. + """ def __init__(self, msg=""): Error.__init__(self, msg) self.msg = msg self.resultcode = 20 def __str__(self): - return "Barcode size is out of range" + return "Barcode size is out of range ({msg})".format(msg=self.msg) + class BarcodeCodeError(Error): + """ No Barcode code was supplied. + + No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode`. + The returncode for this exception is `30`. + """ def __init__(self, msg=""): Error.__init__(self, msg) self.msg = msg self.resultcode = 30 def __str__(self): - return "Code was not supplied" + return "No Barcode code was supplied" + class ImageSizeError(Error): + """ Image height is longer than 255px and can't be printed. + + The returncode for this exception is `40`. + """ def __init__(self, msg=""): Error.__init__(self, msg) self.msg = msg @@ -62,7 +93,13 @@ class ImageSizeError(Error): def __str__(self): return "Image height is longer than 255px and can't be printed" + class TextError(Error): + """ Text string must be supplied to the `text()` method. + + This exception is raised when an empty string is passed to :py:meth:`escpos.escpos.Escpos.text`. + The returncode for this exception is `50`. + """ def __init__(self, msg=""): Error.__init__(self, msg) self.msg = msg @@ -73,6 +110,11 @@ class TextError(Error): class CashDrawerError(Error): + """ Valid pin must be set in order to send pulse. + + A valid pin number has to be passed onto the method :py:meth:`escpos.escpos.Escpos.cashdraw`. + The returncode for this exception is `60`. + """ def __init__(self, msg=""): Error.__init__(self, msg) self.msg = msg @@ -82,7 +124,12 @@ class CashDrawerError(Error): return "Valid pin must be set to send pulse" -class TabError(Error): +class TabPosError(Error): + """ Valid tab positions must be in the range 0 to 16. + + This exception is raised by :py:meth:`escpos.escpos.Escpos.control`. + The returncode for this exception is `70`. + """ def __init__(self, msg=""): Error.__init__(self, msg) self.msg = msg @@ -93,10 +140,30 @@ class TabError(Error): class CharCodeError(Error): + """ Valid char code must be set. + + The supplied charcode-name in :py:meth:`escpos.escpos.Escpos.charcode` is unknown. + Ths returncode for this exception is `80`. + """ def __init__(self, msg=""): Error.__init__(self, msg) self.msg = msg - self.resultcode = 70 + self.resultcode = 80 def __str__(self): return "Valid char code must be set" + + +class USBNotFoundError(Error): + """ Device wasn't found (probably not plugged in) + + The USB device seems to be not plugged in. + Ths returncode for this exception is `90`. + """ + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 90 + + def __str__(self): + return "USB device not found" diff --git a/escpos/printer.py b/escpos/printer.py index 3a3bc84..be8aed8 100644 --- a/escpos/printer.py +++ b/escpos/printer.py @@ -1,9 +1,10 @@ #!/usr/bin/python -""" -@author: Manuel F Martinez -@organization: Bashlinux -@copyright: Copyright (c) 2012 Bashlinux -@license: GNU GPL v3 +""" This module contains the implentations of abstract base class :py:class:`Escpos`. + +:author: `Manuel F Martinez `_ and others +:organization: Bashlinux and `python-escpos `_ +:copyright: Copyright (c) 2012 Bashlinux +:license: GNU GPL v3 """ import usb.core @@ -11,34 +12,37 @@ import usb.util import serial import socket -from escpos import * -from constants import * -from exceptions import * +from .escpos import * +from .exceptions import * + class Usb(Escpos): - """ Define USB printer """ + """ USB printer - def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01): + This class describes a printer that natively speaks USB. + """ + + def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01, *args, **kwargs): """ - @param idVendor : Vendor ID - @param idProduct : Product ID - @param interface : USB device interface - @param in_ep : Input end point - @param out_ep : Output end point + :param idVendor: Vendor ID + :param idProduct: Product ID + :param interface: USB device interface + :param in_ep: Input end point + :param out_ep: Output end point """ - self.idVendor = idVendor + Escpos.__init__(self, *args, **kwargs) + self.idVendor = idVendor self.idProduct = idProduct self.interface = interface - self.in_ep = in_ep - self.out_ep = out_ep + self.in_ep = in_ep + self.out_ep = out_ep self.open() - def open(self): - """ Search device on USB tree and set is as escpos device """ + """ Search device on USB tree and set it as escpos device """ self.device = usb.core.find(idVendor=self.idVendor, idProduct=self.idProduct) if self.device is None: - print "Cable isn't plugged in" + raise USBNotFoundError("Device not found or cable not plugged in.") check_driver = None @@ -52,19 +56,20 @@ class Usb(Escpos): self.device.detach_kernel_driver(0) except usb.core.USBError as e: if check_driver is not None: - print "Could not detatch kernel driver: %s" % str(e) + print("Could not detatch kernel driver: {0}".format(str(e))) try: self.device.set_configuration() self.device.reset() except usb.core.USBError as e: - print "Could not set configuration: %s" % str(e) - + print("Could not set configuration: {0}".format(str(e))) def _raw(self, msg): - """ Print any command sent in raw format """ - self.device.write(self.out_ep, msg, self.interface) + """ Print any command sent in raw format + :param msg: arbitrary code to be printed + """ + self.device.write(self.out_ep, msg, self.interface) def __del__(self): """ Release USB interface """ @@ -73,36 +78,37 @@ class Usb(Escpos): self.device = None - class Serial(Escpos): - """ Define Serial printer """ + """ Serial printer + + This class describes a printer that is connected by serial interface. + """ def __init__(self, devfile="/dev/ttyS0", baudrate=9600, bytesize=8, timeout=1, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, - xonxoff=False , dsrdtr=True): + xonxoff=False, dsrdtr=True, *args, **kwargs): """ - @param devfile : Device file under dev filesystem - @param baudrate : Baud rate for serial transmission - @param bytesize : Serial buffer size - @param timeout : Read/Write timeout - - @param parity : Parity checking - @param stopbits : Number of stop bits - @param xonxoff : Software flow control - @param dsrdtr : Hardware flow control (False to enable RTS/CTS) + + :param devfile: Device file under dev filesystem + :param baudrate: Baud rate for serial transmission + :param bytesize: Serial buffer size + :param timeout: Read/Write timeout + :param parity: Parity checking + :param stopbits: Number of stop bits + :param xonxoff: Software flow control + :param dsrdtr: Hardware flow control (False to enable RTS/CTS) """ - self.devfile = devfile + Escpos.__init__(self, *args, **kwargs) + self.devfile = devfile self.baudrate = baudrate self.bytesize = bytesize - self.timeout = timeout - + self.timeout = timeout self.parity = parity self.stopbits = stopbits self.xonxoff = xonxoff self.dsrdtr = dsrdtr - - self.open() + self.open() def open(self): """ Setup serial port and set is as escpos device """ @@ -112,15 +118,16 @@ class Serial(Escpos): xonxoff=self.xonxoff, dsrdtr=self.dsrdtr) if self.device is not None: - print "Serial printer enabled" + print("Serial printer enabled") else: - print "Unable to open serial printer on: %s" % self.devfile - + print("Unable to open serial printer on: {0}".format(str(self.devfile))) def _raw(self, msg): - """ Print any command sent in raw format """ - self.device.write(msg) + """ Print any command sent in raw format + :param msg: arbitrary code to be printed + """ + self.device.write(msg) def __del__(self): """ Close Serial interface """ @@ -128,63 +135,94 @@ class Serial(Escpos): self.device.close() - class Network(Escpos): - """ Define Network printer """ + """ Network printer - def __init__(self,host,port=9100): + This class is used to attach to a networked printer. You can also use this in order to attach to a printer that + is forwarded with ``socat``. + + If you have a local printer on parallel port ``/dev/usb/lp0`` then you could start ``socat`` with: + + .. code-block:: none + + socat -u TCP4-LISTEN:4242,reuseaddr,fork OPEN:/dev/usb/lp0 + + Then you should be able to attach to port ``4242`` with this class. + Otherwise the normal usecase would be to have a printer with ethernet interface. This type of printer should + work the same with this class. For the address of the printer check its manuals. + """ + + def __init__(self, host, port=9100, timeout=60, *args, **kwargs): """ - @param host : Printer's hostname or IP address - @param port : Port to write to + + :param host : Printer's hostname or IP address + :param port : Port to write to + :param timeout : timeout in seconds for the socket-library """ + Escpos.__init__(self, *args, **kwargs) self.host = host self.port = port + self.timeout = timeout self.open() - def open(self): - """ Open TCP socket and set it as escpos device """ + """ Open TCP socket with ``socket``-library and set it as escpos device """ self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.device.settimeout(self.timeout) self.device.connect((self.host, self.port)) if self.device is None: - print "Could not open socket for %s" % self.host - + print("Could not open socket for {0}".format(self.host)) def _raw(self, msg): - """ Print any command sent in raw format """ - self.device.send(msg) + """ Print any command sent in raw format + :param msg: arbitrary code to be printed + """ + self.device.sendall(msg) def __del__(self): """ Close TCP connection """ self.device.close() - class File(Escpos): - """ Define Generic file printer """ + """ Generic file printer - def __init__(self, devfile="/dev/usb/lp0"): + This class is used for parallel port printer or other printers that are directly attached to the filesystem. + Note that you should stay away from using USB-to-Parallel-Adapter since they are unreliable + and produce arbitrary errors. + """ + + def __init__(self, devfile="/dev/usb/lp0", *args, **kwargs): """ - @param devfile : Device file under dev filesystem + + :param devfile : Device file under dev filesystem """ + Escpos.__init__(self, *args, **kwargs) self.devfile = devfile self.open() - def open(self): """ Open system file """ self.device = open(self.devfile, "wb") if self.device is None: - print "Could not open the specified file %s" % self.devfile + print("Could not open the specified file {0}".format(self.devfile)) + def flush(self): + """ Flush printing content """ + self.device.flush() def _raw(self, msg): - """ Print any command sent in raw format """ - self.device.write(msg); + """ Print any command sent in raw format + :param msg: arbitrary code to be printed + """ + if type(msg) is str: + self.device.write(msg.encode()) + else: + self.device.write(msg) def __del__(self): """ Close system file """ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ecf975e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +-e . \ No newline at end of file diff --git a/setup.py b/setup.py index 8c64d6a..ca26552 100755 --- a/setup.py +++ b/setup.py @@ -1,15 +1,50 @@ #!/usr/bin/python -from distutils.core import setup +import os +import sys +from setuptools import setup +from setuptools.command.test import test as TestCommand + + +def read(fname): + """read file from same path as setup.py""" + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +class Tox(TestCommand): + """proxy class that enables tox to be run with setup.py test""" + user_options = [('tox-args=', 'a', "Arguments to pass to tox")] + + def initialize_options(self): + """initialize the user-options""" + TestCommand.initialize_options(self) + self.tox_args = None + + def finalize_options(self): + """finalize user-options""" + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + """run tox and pass on user-options""" + # import here, cause outside the eggs aren't loaded + import tox + import shlex + args = self.tox_args + if args: + args = shlex.split(self.tox_args) + errno = tox.cmdline(args=args) + sys.exit(errno) setup( - name='escpos', - version='1.0.9', - url='https://github.com/manpaz/python-escpos', - download_url='https://github.com/manpaz/python-escpos.git', + name='python-escpos', + version='1.0.9-dev', + url='https://github.com/python-escpos/python-escpos', + download_url='https://github.com/python-escpos/python-escpos/archive/master.zip', description='Python library to manipulate ESC/POS Printers', license='GNU GPL v3', - long_description=open('README').read(), + long_description=read('README'), author='Manuel F Martinez', author_email='manpaz@bashlinux.com', platforms=['linux'], @@ -23,7 +58,19 @@ setup( 'Operating System :: GNU/Linux', 'Intended Audience :: Developers', 'Programming Language :: Python', - 'Topic :: System :: Pheripherals', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Topic :: System :: Peripherals', 'Topic :: Software Development :: Libraries :: Python Modules', ], + install_requires=[ + 'pyusb', + 'Pillow>=2.0', + 'qrcode>=4.0', + 'pyserial', + ], + tests_require=['tox', 'nose'], + cmdclass={'test': Tox}, ) diff --git a/test/test_load_module.py b/test/test_load_module.py new file mode 100644 index 0000000..9603cf1 --- /dev/null +++ b/test/test_load_module.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +"""very basic test cases that load the classes + +:author: `Patrick Kanzler `_ +:organization: `python-escpos `_ +:copyright: Copyright (c) 2016 `python-escpos `_ +:license: GNU GPL v3 +""" + +from nose.tools import with_setup + +import escpos.printer as printer +import os + +devfile = 'testfile' + +def setup_testfile(): + """create a testfile as devfile""" + fhandle = open(devfile, 'a') + try: + os.utime(devfile, None) + finally: + fhandle.close() + +def teardown_testfile(): + """destroy testfile again""" + os.remove(devfile) + +@with_setup(setup_testfile, teardown_testfile) +def test_instantiation(): + """test the instantiation of a escpos-printer class and basic printing""" + instance = printer.File(devfile=devfile) + instance.text('This is a test\n') diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..880e6cc --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py27, py34 + +[testenv] +deps = nose + coverage +# TODO: implement code coverage analysis (and of course tests at first)