diff --git a/.gitignore b/.gitignore index 05ea79d..05228da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.py[cod] -.DS_Store \ No newline at end of file +.DS_Store +build 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 b5fca55..da13f98 100644 --- a/escpos/constants.py +++ b/escpos/constants.py @@ -1,54 +1,92 @@ """ ESC/POS Commands (Constants) """ -# Feed control sequences -CTL_LF = '\x0a' # Print and line feed -CTL_FF = '\x0c' # Form feed -CTL_CR = '\x0d' # Carriage return -CTL_HT = '\x09' # Horizontal tab -CTL_VT = '\x0b' # Vertical tab -# 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 -# 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 +#{ 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 = '\n' # Print and line feed +CTL_FF = '\f' # Form feed +CTL_CR = '\r' # Carriage return +CTL_HT = '\t' # Horizontal tab +CTL_VT = '\v' # Vertical tab + +#{ Printer hardware +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 + +#{ Barcode format +_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 diff --git a/escpos/escpos.py b/escpos/escpos.py index c79bea4..9f06bc8 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -53,7 +53,7 @@ class Escpos: buffer = "" cont = 0 - def fullimage(self, img, max_height=860, width=512, histeq=True): + def fullimage(self, img, max_height=860, width=512, histeq=True, bandsize=255): """ Resizes and prints an arbitrarily sized image """ if isinstance(img, Image.Image): im = img.convert("RGB") @@ -74,19 +74,21 @@ class Escpos: n = n + h[i+b] im = im.point(lut) - ratio = float(width) / im.size[0] - newheight = int(ratio * im.size[1]) + 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 im.size[1] > max_height: + # 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 - bandsize = 255 current = 0 while current < im.size[1]: - self.image(im.crop((0, current, width, min(im.size[1], current + bandsize)))) + self.image(im.crop((0, current, width or im.size[0], + min(im.size[1], current + bandsize)))) current += bandsize @@ -143,28 +145,31 @@ class Escpos: def qr(self, text): """ Print QR Code for the provided string """ - 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() # Convert the RGB image in printable image im = qr_img._img.convert("RGB") + self.text('\n') + self.set(align='center') self.image(im) + self.text('\n') - def barcode(self, code, bc, width, height, pos, font): + def barcode(self, code, bc, height, width, pos, font): """ Print Barcode """ # Align Bar Code() 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 = %s" % 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 = %s" % width) # Font if font.upper() == "B": self._raw(BARCODE_FONT_B) @@ -192,10 +197,10 @@ 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) diff --git a/escpos/exceptions.py b/escpos/exceptions.py index caad7a8..f9c7489 100644 --- a/escpos/exceptions.py +++ b/escpos/exceptions.py @@ -34,7 +34,7 @@ class BarcodeTypeError(Error): self.resultcode = 10 def __str__(self): - return "No Barcode type is defined" + return "No Barcode type is defined (%s)" % self.msg class BarcodeSizeError(Error): def __init__(self, msg=""): @@ -43,7 +43,7 @@ class BarcodeSizeError(Error): self.resultcode = 20 def __str__(self): - return "Barcode size is out of range" + return "Barcode size is out of range (%s)" % self.msg class BarcodeCodeError(Error): def __init__(self, msg=""):