diff --git a/escpos/cli.py b/escpos/cli.py new file mode 100755 index 0000000..6f1df41 --- /dev/null +++ b/escpos/cli.py @@ -0,0 +1,200 @@ +#!/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('%s\n' % 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%s\n' % 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 af71c48..e529b81 100644 --- a/escpos/constants.py +++ b/escpos/constants.py @@ -1,86 +1,98 @@ """ ESC/POS Commands (Constants) """ +# 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_SET_HT = ESC + '\x44' # Set horizontal tab positions +CTL_VT = ESC + '\x64\x04' # 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 +HW_INIT = ESC + '\x40' # Clear data in buffer and reset modes +HW_SELECT = ESC + '\x3d\x01' # Printer select +HW_RESET = ESC + '\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 [] +CD_KICK_2 = ESC + '\x70\x00' # Sends a pulse to pin 2 [] +CD_KICK_5 = ESC + '\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 +PAPER_FULL_CUT = GS + '\x56\x00' # Full cut paper +PAPER_PART_CUT = GS + '\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 +TXT_NORMAL = ESC + '\x21\x00' # Normal text +TXT_2HEIGHT = ESC + '\x21\x10' # Double height text +TXT_2WIDTH = ESC + '\x21\x20' # Double width text +TXT_4SQUARE = ESC + '\x21\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\x11' # 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 +BARCODE_TXT_OFF = GS + '\x48\x00' # HRI barcode chars OFF +BARCODE_TXT_ABV = GS + '\x48\x01' # HRI barcode chars above +BARCODE_TXT_BLW = GS + '\x48\x02' # HRI barcode chars below +BARCODE_TXT_BTH = GS + '\x48\x03' # HRI barcode chars both above and below +BARCODE_FONT_A = GS + '\x66\x00' # Font type A for HRI barcode chars +BARCODE_FONT_B = GS + '\x66\x01' # Font type B for HRI barcode chars +BARCODE_HEIGHT = GS + '\x68\x64' # Barcode Height [1-255] +BARCODE_WIDTH = GS + '\x77\x03' # Barcode Width [2-6] +BARCODE_UPC_A = GS + '\x6b\x00' # Barcode type UPC-A +BARCODE_UPC_E = GS + '\x6b\x01' # Barcode type UPC-E +BARCODE_EAN13 = GS + '\x6b\x02' # Barcode type EAN13 +BARCODE_EAN8 = GS + '\x6b\x03' # Barcode type EAN8 +BARCODE_CODE39 = GS + '\x6b\x04' # Barcode type CODE39 +BARCODE_ITF = GS + '\x6b\x05' # Barcode type ITF +BARCODE_NW7 = GS + '\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 +S_RASTER_N = GS + '\x76\x30\x00' # Set raster image normal size +S_RASTER_2W = GS + '\x76\x30\x01' # Set raster image double width +S_RASTER_2H = GS + '\x76\x30\x02' # Set raster image double height +S_RASTER_Q = GS + '\x76\x30\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 65b673e..ef8ccfd 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -134,6 +134,7 @@ class Escpos(object): self._print_image(pix_line, img_size) +<<<<<<< HEAD def image(self, path_img): """ Open image file @@ -185,12 +186,12 @@ class Escpos(object): self._raw(binascii.unhexlify(bytes(buf, "ascii"))) self._raw('\n') - def qr(self, text): + def qr(self, text, error_correct=qrcode.constants.ERROR_CORRECT_M): """ Print QR Code for the provided string :param text: text to generate a QR-Code from """ - qr_code = qrcode.QRCode(version=4, box_size=4, border=1) + 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() @@ -278,7 +279,7 @@ class Escpos(object): # 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": @@ -302,7 +303,7 @@ class Escpos(object): 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() @@ -414,7 +415,7 @@ class Escpos(object): 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): @@ -442,7 +443,7 @@ class Escpos(object): 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): diff --git a/escpos/printer.py b/escpos/printer.py index bf6dbe5..36a59a7 100644 --- a/escpos/printer.py +++ b/escpos/printer.py @@ -31,8 +31,8 @@ class Usb(Escpos): 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): @@ -146,7 +146,7 @@ class Network(Escpos): def _raw(self, msg): """ Print any command sent in raw format """ - self.device.send(msg) + self.device.sendall(msg) def __del__(self): """ Close TCP connection """