diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..9de2698 --- /dev/null +++ b/.mailmap @@ -0,0 +1,11 @@ + + +Manuel F Martinez manpaz + +Davis Goglin davisgoglin +Michael Billington Michael +Cody (Quantified Code Bot) Cody +Renato Lorenzi Renato.Lorenzi +Ahmed Tahri TAHRI Ahmed +Michael Elsdörfer Michael Elsdörfer +csoft2k \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6f94072..f8fca72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: python sudo: false cache: pip +git: + depth: 100000 addons: apt: packages: @@ -37,6 +39,7 @@ matrix: - python: pypy3 before_install: - pip install tox codecov 'sphinx>=1.5.1' + - ./doc/generate_authors.sh --check script: - tox - codecov diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..f5f64a5 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,28 @@ +Ahmed Tahri +Asuki Kono +belono +Christoph Heuel +Cody (Quantified Code Bot) +csoft2k +Curtis // mashedkeyboard +Davis Goglin +Dean Rispin +Dmytro Katyukha +Hark +Joel Lehtonen +Kristi +ldos +Manuel F Martinez +Michael Billington +Michael Elsdörfer +mrwunderbar666 +Nathan Bookham +Patrick Kanzler +Qian Linfeng +Renato Lorenzi +Romain Porte +Sam Cheng +Stephan Sokolow +Thijs Triemstra +Thomas van den Berg +ysuolmai diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5ffef2b..eb5f2f8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,37 @@ Changelog ********* +2017-08-04 - Version 3.0a2 - "It's My Party And I'll Sing If I Want To" +----------------------------------------------------------------------- +This release is the third alpha release of the new version 3.0. Please +be aware that the API will still change until v3.0 is released. + +changes +^^^^^^^ +- refactor of the set-method +- preliminary support of POS "line display" printing +- improvement of tests +- added ImageWidthError +- list authors in repository +- add support for software-based barcode-rendering +- fix SerialException when trying to close device on __del__ +- added the DLE EOT querying command for USB and Serial +- ensure QR codes have a large enough border +- make feed for cut optional +- fix the behavior of horizontal tabs +- added test script for hard an soft barcodes +- implemented paper sensor querying command +- added weather forecast example script +- added a method for simpler newlines + +contributors +^^^^^^^^^^^^ +- csoft2k +- Patrick Kanzler +- mrwunderbar666 +- Romain Porte +- Ahmed Tahri + 2017-03-29 - Version 3.0a1 - "Headcrash" ---------------------------------------- This release is the second alpha release of the new version 3.0. Please diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9d5de8b..ad16384 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -12,6 +12,15 @@ The pull requests and issues will be prefilled with templates. Please fill in yo This project uses `semantic versioning `_ and tries to adhere to the proposed rules as well as possible. +Author-list +----------- + +This project keeps a list of authors. This can be auto-generated by calling `./doc/generate-authors.sh`. +When contributing the first time, please include a commit with the output of this script in place. +Otherwise the integration-check will fail. + +When you change your username or mail-address, please also update the `.mailmap` and the authors-list. + Style-Guide ----------- diff --git a/README.rst b/README.rst index 1a10540..31e6a3b 100644 --- a/README.rst +++ b/README.rst @@ -6,10 +6,6 @@ python-escpos - Python library to manipulate ESC/POS Printers :target: https://travis-ci.org/python-escpos/python-escpos :alt: Continous Integration -.. image:: https://www.quantifiedcode.com/api/v1/project/95748b89a3974700800b85e4ed3d32c4/badge.svg - :target: https://www.quantifiedcode.com/app/project/95748b89a3974700800b85e4ed3d32c4 - :alt: Code issues - .. image:: https://landscape.io/github/python-escpos/python-escpos/master/landscape.svg?style=flat :target: https://landscape.io/github/python-escpos/python-escpos/master :alt: Code Health @@ -47,10 +43,11 @@ Dependencies This library makes use of: - * pyusb for USB-printers - * Pillow for image printing - * qrcode for the generation of QR-codes - * pyserial for serial printers +* `pyusb `_ for USB-printers +* `Pillow `_ for image printing +* `qrcode `_ for the generation of QR-codes +* `pyserial `_ for serial printers +* `viivakoodi `_ for the generation of barcodes Documentation and Usage ----------------------- diff --git a/doc/generate_authors.sh b/doc/generate_authors.sh new file mode 100755 index 0000000..bd770e7 --- /dev/null +++ b/doc/generate_authors.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +GENLIST=$(git shortlog -s -n | cut -f2 | sort) +AUTHORSFILE="$(dirname $0)/../AUTHORS" +TEMPAUTHORSFILE="/tmp/python-escpos-authorsfile" + +if [ "$#" -eq 1 ] + then + echo "$GENLIST">$TEMPAUTHORSFILE + echo "\nAuthorsfile in version control:\n" + cat $AUTHORSFILE + echo "\nNew authorsfile:\n" + cat $TEMPAUTHORSFILE + echo "\nUsing diff on files...\n" + diff -q --from-file $AUTHORSFILE $TEMPAUTHORSFILE + else + echo "$GENLIST">$AUTHORSFILE +fi + diff --git a/doc/requirements.txt b/doc/requirements.txt index 682316c..9f38356 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -5,3 +5,4 @@ pyserial sphinx-rtd-theme setuptools-scm docutils>=0.12 +viivakoodi diff --git a/doc/user/usage.rst b/doc/user/usage.rst index 6d4e462..203389b 100644 --- a/doc/user/usage.rst +++ b/doc/user/usage.rst @@ -1,6 +1,7 @@ ***** Usage ***** +:Last Reviewed: 2017-06-10 Define your printer ------------------- @@ -133,13 +134,13 @@ format. For windows it is probably at:: And for linux:: - $HOME/.config/python-escpos/config.yaml + $HOME/.config/python-escpos/config.yaml If you aren't sure, run:: - from escpos import config - c = config.Config() - c.load() + from escpos import config + c = config.Config() + c.load() If it can't find the configuration file in the default location, it will tell you where it's looking. You can always pass a path, or a list of paths, to @@ -147,9 +148,9 @@ the ``load()`` method. To load the configured printer, run:: - from escpos import config - c = config.Config() - printer = c.printer() + from escpos import config + c = config.Config() + printer = c.printer() The printer section @@ -157,23 +158,34 @@ The printer section The ``printer`` configuration section defines a default printer to create. -The only required paramter is ``type``. The value of this should be one of the +The only required paramter is ``type``. The value of this has to be one of the printers defined in :doc:`/user/printers`. -The rest of the parameters are whatever you want to pass to the printer. +The rest of the given parameters will be passed on to the initialization of the printer class. +Use these to overwrite the default values as specified in :doc:`/user/printers`. +This implies that the parameters have to match the parameter-names of the respective printer class. An example file printer:: - printer: - type: File - devfile: /dev/someprinter + printer: + type: File + devfile: /dev/someprinter And for a network printer:: - printer: - type: network - host: 127.0.0.1 - port: 9000 + printer: + type: Network + host: 127.0.0.1 + port: 9000 + +An USB-printer could be defined by:: + + printer: + type: Usb + idVendor: 0x1234 + idProduct: 0x5678 + in_ep: 0x66 + out_ep: 0x01 Printing text right ------------------- diff --git a/examples/barcodes.py b/examples/barcodes.py new file mode 100644 index 0000000..7e9c242 --- /dev/null +++ b/examples/barcodes.py @@ -0,0 +1,11 @@ +from escpos.printer import Usb + + +# Adapt to your needs +p = Usb(0x0416, 0x5011, profile="POS-5890") + +# Print software and then hardware barcode with the same content +p.soft_barcode('code39', '123456') +p.text('\n') +p.text('\n') +p.barcode('123456', 'CODE39') diff --git a/examples/graphics/climacons/clear-day.png b/examples/graphics/climacons/clear-day.png new file mode 100644 index 0000000..b0cadc9 Binary files /dev/null and b/examples/graphics/climacons/clear-day.png differ diff --git a/examples/graphics/climacons/clear-night.png b/examples/graphics/climacons/clear-night.png new file mode 100644 index 0000000..13a6911 Binary files /dev/null and b/examples/graphics/climacons/clear-night.png differ diff --git a/examples/graphics/climacons/cloudy.png b/examples/graphics/climacons/cloudy.png new file mode 100644 index 0000000..867f232 Binary files /dev/null and b/examples/graphics/climacons/cloudy.png differ diff --git a/examples/graphics/climacons/fog.png b/examples/graphics/climacons/fog.png new file mode 100644 index 0000000..c90bc6f Binary files /dev/null and b/examples/graphics/climacons/fog.png differ diff --git a/examples/graphics/climacons/partly-cloudy-day.png b/examples/graphics/climacons/partly-cloudy-day.png new file mode 100644 index 0000000..d33b56b Binary files /dev/null and b/examples/graphics/climacons/partly-cloudy-day.png differ diff --git a/examples/graphics/climacons/partly-cloudy-night.png b/examples/graphics/climacons/partly-cloudy-night.png new file mode 100644 index 0000000..66eaa88 Binary files /dev/null and b/examples/graphics/climacons/partly-cloudy-night.png differ diff --git a/examples/graphics/climacons/rain.png b/examples/graphics/climacons/rain.png new file mode 100644 index 0000000..34f844f Binary files /dev/null and b/examples/graphics/climacons/rain.png differ diff --git a/examples/graphics/climacons/readme.md b/examples/graphics/climacons/readme.md new file mode 100644 index 0000000..d610a00 --- /dev/null +++ b/examples/graphics/climacons/readme.md @@ -0,0 +1,10 @@ +# Climacons by Adam Whitcroft + +75 climatically categorised pictographs for web and UI design by [@adamwhitcroft](http://www.twitter.com/#!/adamwhitcroft). + +Visit the [Climacons](http://adamwhitcroft.com/climacons/) website for more information. + +Visit [Adam Whitcroft on GitHub](https://github.com/AdamWhitcroft) + +## License +You are free to use any of the Climacons Icons (the "icons") in any personal or commercial work without obligation of payment (monetary or otherwise) or attribution, however a credit for the work would be appreciated. **Do not** redistribute or sell and **do not** claim creative credit. Intellectual property rights are not transferred with the download of the icons. \ No newline at end of file diff --git a/examples/graphics/climacons/sleet.png b/examples/graphics/climacons/sleet.png new file mode 100644 index 0000000..1cf5315 Binary files /dev/null and b/examples/graphics/climacons/sleet.png differ diff --git a/examples/graphics/climacons/snow.png b/examples/graphics/climacons/snow.png new file mode 100644 index 0000000..fabc4d6 Binary files /dev/null and b/examples/graphics/climacons/snow.png differ diff --git a/examples/graphics/climacons/wind.png b/examples/graphics/climacons/wind.png new file mode 100644 index 0000000..f410f7f Binary files /dev/null and b/examples/graphics/climacons/wind.png differ diff --git a/examples/qr_code.py b/examples/qr_code.py new file mode 100644 index 0000000..6db8b68 --- /dev/null +++ b/examples/qr_code.py @@ -0,0 +1,19 @@ +import sys + +from escpos.printer import Usb + + +def usage(): + print("usage: qr_code.py ") + + +if __name__ == '__main__': + if len(sys.argv) != 2: + usage() + sys.exit(1) + + content = sys.argv[1] + + # Adapt to your needs + p = Usb(0x0416, 0x5011, profile="POS-5890") + p.qr(content) diff --git a/examples/software_barcode.py b/examples/software_barcode.py new file mode 100644 index 0000000..2fd3c18 --- /dev/null +++ b/examples/software_barcode.py @@ -0,0 +1,9 @@ +from escpos.printer import Usb + + +# Adapt to your needs +p = Usb(0x0416, 0x5011, profile="POS-5890") + +# Some software barcodes +p.soft_barcode('code128', 'Hello') +p.soft_barcode('code39', '123456') diff --git a/examples/weather.py b/examples/weather.py new file mode 100644 index 0000000..fe9f0af --- /dev/null +++ b/examples/weather.py @@ -0,0 +1,127 @@ +#!/usr/bin/python + + +# Adapted script from Adafruit +# Weather forecast for Raspberry Pi w/Adafruit Mini Thermal Printer. +# Retrieves data from DarkSky.net's API, prints current conditions and +# forecasts for next two days. +# Weather example using nice bitmaps. +# Written by Adafruit Industries. MIT license. +# Adapted and enhanced for escpos library by MrWunderbar666 + +# Icons taken from http://adamwhitcroft.com/climacons/ +# Check out his github: https://github.com/AdamWhitcroft/climacons + + +from __future__ import print_function +from datetime import datetime +import calendar +import urllib +import json +import time +import os + +from escpos.printer import Usb + +""" Setting up the main pathing """ +this_dir, this_filename = os.path.split(__file__) +GRAPHICS_PATH = os.path.join(this_dir, "graphics/climacons/") + +# Adapt to your needs +printer = Usb(0x0416, 0x5011, profile="POS-5890") + +# You can get your API Key on www.darksky.net and register a dev account. +# Technically you can use any other weather service, of course :) +API_KEY = "YOUR API KEY" + +LAT = "22.345490" # Your Location +LONG = "114.189945" # Your Location + + +def forecast_icon(idx): + icon = data['daily']['data'][idx]['icon'] + image = GRAPHICS_PATH + icon + ".png" + return image + + +# Dumps one forecast line to the printer +def forecast(idx): + date = datetime.fromtimestamp(int(data['daily']['data'][idx]['time'])) + day = calendar.day_name[date.weekday()] + lo = data['daily']['data'][idx]['temperatureMin'] + hi = data['daily']['data'][idx]['temperatureMax'] + cond = data['daily']['data'][idx]['summary'] + print(date) + print(day) + print(lo) + print(hi) + print(cond) + time.sleep(1) + printer.set( + font='a', + height=2, + align='left', + bold=False, + double_height=False) + printer.text(day + ' \n ') + time.sleep(5) # Sleep to prevent printer buffer overflow + printer.text('\n') + printer.image(forecast_icon(idx)) + printer.text('low ' + str(lo)) + printer.text(deg) + printer.text('\n') + printer.text(' high ' + str(hi)) + printer.text(deg) + printer.text('\n') + # take care of pesky unicode dash + printer.text(cond.replace(u'\u2013', '-').encode('utf-8')) + printer.text('\n \n') + + +def icon(): + icon = data['currently']['icon'] + image = GRAPHICS_PATH + icon + ".png" + return image + + +deg = ' C' # Degree symbol on thermal printer, need to find a better way to use a proper degree symbol + +# if you want Fahrenheit change units= to 'us' +url = "https://api.darksky.net/forecast/" + API_KEY + "/" + LAT + "," + LONG + \ + "?exclude=[alerts,minutely,hourly,flags]&units=si" # change last bit to 'us' for Fahrenheit +response = urllib.urlopen(url) +data = json.loads(response.read()) + +printer.print_and_feed(n=1) +printer.control("LF") +printer.set(font='a', height=2, align='center', bold=True, double_height=True) +printer.text("Weather Forecast") +printer.text("\n") +printer.set(align='center') + + +# Print current conditions +printer.set(font='a', height=2, align='center', bold=True, double_height=False) +printer.text('Current conditions: \n') +printer.image(icon()) +printer.text("\n") + +printer.set(font='a', height=2, align='left', bold=False, double_height=False) +temp = data['currently']['temperature'] +cond = data['currently']['summary'] +printer.text(temp) +printer.text(' ') +printer.text(deg) +printer.text(' ') +printer.text('\n') +printer.text('Sky: ' + cond) +printer.text('\n') +printer.text('\n') + +# Print forecast +printer.set(font='a', height=2, align='center', bold=True, double_height=False) +printer.text('Forecast: \n') +forecast(0) +forecast(1) +printer.cut +printer.control("LF") diff --git a/setup.py b/setup.py index c5d77f6..f048bea 100755 --- a/setup.py +++ b/setup.py @@ -100,6 +100,8 @@ setup( 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', @@ -115,7 +117,8 @@ setup( 'pyyaml', 'argparse', 'argcomplete', - 'future' + 'future', + 'viivakoodi>=0.8' ], setup_requires=[ 'setuptools_scm', @@ -123,7 +126,7 @@ setup( tests_require=[ 'jaconv', 'tox', - 'pytest', + 'pytest!=3.2.0', 'pytest-cov', 'pytest-mock', 'nose', diff --git a/src/escpos/constants.py b/src/escpos/constants.py index e3225f8..75bdc1b 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -64,6 +64,11 @@ _PANEL_BUTTON = lambda n: ESC + b'c5' + six.int2byte(n) PANEL_BUTTON_ON = _PANEL_BUTTON(0) # enable all panel buttons PANEL_BUTTON_OFF = _PANEL_BUTTON(1) # disable all panel buttons +# Line display printing +LINE_DISPLAY_OPEN = ESC + b'\x3d\x02' +LINE_DISPLAY_CLEAR = ESC + b'\x40' +LINE_DISPLAY_CLOSE = ESC + b'\x3d\x01' + # Sheet modes SHEET_SLIP_MODE = ESC + b'\x63\x30\x04' # slip paper SHEET_ROLL_MODE = ESC + b'\x63\x30\x01' # paper roll @@ -71,51 +76,90 @@ SHEET_ROLL_MODE = ESC + b'\x63\x30\x01' # paper roll # Text format # TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll # Printers" and tidy up this stuff too. -TXT_FLIP_ON = ESC + b'\x7b\x01' -TXT_FLIP_OFF = ESC + b'\x7b\x00' -TXT_SMOOTH_ON = GS + b'\x62\x01' -TXT_SMOOTH_OFF = GS + b'\x62\x00' TXT_SIZE = GS + b'!' -TXT_WIDTH = {1: 0x00, - 2: 0x10, - 3: 0x20, - 4: 0x30, - 5: 0x40, - 6: 0x50, - 7: 0x60, - 8: 0x70} -TXT_HEIGHT = {1: 0x00, - 2: 0x01, - 3: 0x02, - 4: 0x03, - 5: 0x04, - 6: 0x05, - 7: 0x06, - 8: 0x07} + TXT_NORMAL = ESC + b'!\x00' # Normal text -TXT_2HEIGHT = ESC + b'!\x10' # Double height text -TXT_2WIDTH = ESC + b'!\x20' # Double width text -TXT_4SQUARE = ESC + b'!\x30' # Quad area text -TXT_UNDERL_OFF = ESC + b'\x2d\x00' # Underline font OFF -TXT_UNDERL_ON = ESC + b'\x2d\x01' # Underline font 1-dot ON -TXT_UNDERL2_ON = ESC + b'\x2d\x02' # Underline font 2-dot ON -TXT_BOLD_OFF = ESC + b'\x45\x00' # Bold font OFF -TXT_BOLD_ON = ESC + b'\x45\x01' # Bold font ON -TXT_ALIGN_LT = ESC + b'\x61\x00' # Left justification -TXT_ALIGN_CT = ESC + b'\x61\x01' # Centering -TXT_ALIGN_RT = ESC + b'\x61\x02' # Right justification -TXT_INVERT_ON = GS + b'\x42\x01' # Inverse Printing ON -TXT_INVERT_OFF = GS + b'\x42\x00' # Inverse Printing OFF + + +TXT_STYLE = { + 'bold': { + False: ESC + b'\x45\x00', # Bold font OFF + True: ESC + b'\x45\x01' # Bold font ON + }, + 'underline': { + 0: ESC + b'\x2d\x00', # Underline font OFF + 1: ESC + b'\x2d\x01', # Underline font 1-dot ON + 2: ESC + b'\x2d\x02' # Underline font 2-dot ON + }, + 'size': { + 'normal': TXT_NORMAL + ESC + b'!\x00', # Normal text + '2h': TXT_NORMAL + ESC + b'!\x10', # Double height text + '2w': TXT_NORMAL + ESC + b'!\x20', # Double width text + '2x': TXT_NORMAL + ESC + b'!\x30' # Quad area text + }, + 'font': { + 'a': ESC + b'\x4d\x00', # Font type A + 'b': ESC + b'\x4d\x00' # Font type B + }, + 'align': { + 'left': ESC + b'\x61\x00', # Left justification + 'center': ESC + b'\x61\x01', # Centering + 'right': ESC + b'\x61\x02' # Right justification + }, + 'invert': { + True: GS + b'\x42\x01', # Inverse Printing ON + False: GS + b'\x42\x00' # Inverse Printing OFF + }, + 'color': { + 'black': ESC + b'\x72\x00', # Default Color + 'red': ESC + b'\x72\x01' # Alternative Color, Usually Red + }, + 'flip': { + True: ESC + b'\x7b\x01', # Flip ON + False: ESC + b'\x7b\x00' # Flip OFF + }, + 'density': { + 0: GS + b'\x7c\x00', # Printing Density -50% + 1: GS + b'\x7c\x01', # Printing Density -37.5% + 2: GS + b'\x7c\x02', # Printing Density -25% + 3: GS + b'\x7c\x03', # Printing Density -12.5% + 4: GS + b'\x7c\x04', # Printing Density 0% + 5: GS + b'\x7c\x08', # Printing Density +50% + 6: GS + b'\x7c\x07', # Printing Density +37.5% + 7: GS + b'\x7c\x06', # Printing Density +25% + 8: GS + b'\x7c\x05' # Printing Density +12.5% + }, + 'smooth': { + True: GS + b'\x62\x01', # Smooth ON + False: GS + b'\x62\x00' # Smooth OFF + }, + 'height': { # Custom text height + 1: 0x00, + 2: 0x01, + 3: 0x02, + 4: 0x03, + 5: 0x04, + 6: 0x05, + 7: 0x06, + 8: 0x07 + }, + 'width': { # Custom text width + 1: 0x00, + 2: 0x10, + 3: 0x20, + 4: 0x30, + 5: 0x40, + 6: 0x50, + 7: 0x60, + 8: 0x70 + } +} # Fonts SET_FONT = lambda n: ESC + b'\x4d' + n TXT_FONT_A = SET_FONT(b'\x00') # Font type A TXT_FONT_B = SET_FONT(b'\x01') # Font type B -# Text colors -TXT_COLOR_BLACK = ESC + b'\x72\x00' # Default Color -TXT_COLOR_RED = ESC + b'\x72\x01' # Alternative Color (Usually Red) - # Spacing LINESPACING_RESET = ESC + b'2' LINESPACING_FUNCS = { @@ -206,13 +250,11 @@ S_RASTER_2W = _PRINT_RASTER_IMG(b'\x01') # Set raster image double width S_RASTER_2H = _PRINT_RASTER_IMG(b'\x02') # Set raster image double height S_RASTER_Q = _PRINT_RASTER_IMG(b'\x03') # Set raster image quadruple -# Printing Density -PD_N50 = GS + b'\x7c\x00' # Printing Density -50% -PD_N37 = GS + b'\x7c\x01' # Printing Density -37.5% -PD_N25 = GS + b'\x7c\x02' # Printing Density -25% -PD_N12 = GS + b'\x7c\x03' # Printing Density -12.5% -PD_0 = GS + b'\x7c\x04' # Printing Density 0% -PD_P50 = GS + b'\x7c\x08' # Printing Density +50% -PD_P37 = GS + b'\x7c\x07' # Printing Density +37.5% -PD_P25 = GS + b'\x7c\x06' # Printing Density +25% -PD_P12 = GS + b'\x7c\x05' # Printing Density +12.5% +# Status Command +RT_STATUS = DLE + EOT +RT_STATUS_ONLINE = RT_STATUS + b'\x01' +RT_STATUS_PAPER = RT_STATUS + b'\x04' +RT_MASK_ONLINE = 8 +RT_MASK_PAPER = 18 +RT_MASK_LOWPAPER = 30 +RT_MASK_NOPAPER = 114 \ No newline at end of file diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index c238259..8d1fd81 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -18,22 +18,29 @@ from __future__ import unicode_literals import qrcode import textwrap import six +import time + +import barcode +from barcode.writer import ImageWriter from .constants import ESC, GS, NUL, QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q from .constants import QR_MODEL_1, QR_MODEL_2, QR_MICRO, BARCODE_TYPES, BARCODE_HEIGHT, BARCODE_WIDTH -from .constants import TXT_ALIGN_CT, TXT_ALIGN_LT, TXT_ALIGN_RT, BARCODE_FONT_A, BARCODE_FONT_B +from .constants import BARCODE_FONT_A, BARCODE_FONT_B from .constants import BARCODE_TXT_OFF, BARCODE_TXT_BTH, BARCODE_TXT_ABV, BARCODE_TXT_BLW -from .constants import TXT_HEIGHT, TXT_WIDTH, TXT_SIZE, TXT_NORMAL, TXT_SMOOTH_OFF, TXT_SMOOTH_ON -from .constants import TXT_FLIP_OFF, TXT_FLIP_ON, TXT_2WIDTH, TXT_2HEIGHT, TXT_4SQUARE -from .constants import TXT_UNDERL_OFF, TXT_UNDERL_ON, TXT_BOLD_OFF, TXT_BOLD_ON, SET_FONT, TXT_UNDERL2_ON -from .constants import TXT_INVERT_OFF, TXT_INVERT_ON, LINESPACING_FUNCS, LINESPACING_RESET -from .constants import PD_0, PD_N12, PD_N25, PD_N37, PD_N50, PD_P50, PD_P37, PD_P25, PD_P12 +from .constants import TXT_SIZE, TXT_NORMAL +from .constants import SET_FONT +from .constants import LINESPACING_FUNCS, LINESPACING_RESET +from .constants import LINE_DISPLAY_OPEN, LINE_DISPLAY_CLEAR, LINE_DISPLAY_CLOSE from .constants import CD_KICK_DEC_SEQUENCE, CD_KICK_5, CD_KICK_2, PAPER_FULL_CUT, PAPER_PART_CUT from .constants import HW_RESET, HW_SELECT, HW_INIT -from .constants import CTL_VT, CTL_HT, CTL_CR, CTL_FF, CTL_LF, CTL_SET_HT, PANEL_BUTTON_OFF, PANEL_BUTTON_ON +from .constants import CTL_VT, CTL_CR, CTL_FF, CTL_LF, CTL_SET_HT, PANEL_BUTTON_OFF, PANEL_BUTTON_ON +from .constants import TXT_STYLE +from .constants import RT_STATUS_ONLINE, RT_MASK_ONLINE +from .constants import RT_STATUS_PAPER, RT_MASK_PAPER, RT_MASK_LOWPAPER, RT_MASK_NOPAPER from .exceptions import BarcodeTypeError, BarcodeSizeError, TabPosError from .exceptions import CashDrawerError, SetVariableError, BarcodeCodeError +from .exceptions import ImageWidthError from .magicencode import MagicEncode @@ -73,6 +80,12 @@ class Escpos(object): """ pass + def _read(self, msg): + """ Returns a NotImplementedError if the instance of the class doesn't override this method. + :raises NotImplementedError + """ + raise NotImplementedError() + def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster", fragment_height=960): """ Print an image @@ -99,6 +112,17 @@ class Escpos(object): """ im = EscposImage(img_source) + try: + max_width = int(self.profile.profile_data['media']['width']['pixels']) + if im.width > max_width: + raise ImageWidthError('{} > {}'.format(im.width, max_width)) + except KeyError: + # If the printer's pixel width is not known, print anyways... + pass + except ValueError: + # If the max_width cannot be converted to an int, print anyways... + pass + if im.height > fragment_height: fragments = im.split(fragment_height) for fragment in fragments: @@ -188,7 +212,10 @@ class Escpos(object): qr_img = qr_code.make_image() im = qr_img._img.convert("RGB") # Convert the RGB image in printable image + self.text('\n') self.image(im) + self.text('\n') + self.text('\n') return # Native 2D code printing cn = b'1' # Code type for QR code @@ -224,9 +251,9 @@ class Escpos(object): """ max_input = (256 << (out_bytes * 8) - 1) if not 1 <= out_bytes <= 4: - raise ValueError("Can only output 1-4 byes") + raise ValueError("Can only output 1-4 bytes") if not 0 <= inp_number <= max_input: - raise ValueError("Number too large. Can only output up to {0} in {1} byes".format(max_input, out_bytes)) + raise ValueError("Number too large. Can only output up to {0} in {1} bytes".format(max_input, out_bytes)) outp = b'' for _ in range(0, out_bytes): outp += six.int2byte(inp_number % 256) @@ -356,7 +383,7 @@ class Escpos(object): # Align Bar Code() if align_ct: - self._raw(TXT_ALIGN_CT) + self._raw(TXT_STYLE['align']['center']) # Height if 1 <= height <= 255: self._raw(BARCODE_HEIGHT + six.int2byte(height)) @@ -396,6 +423,31 @@ class Escpos(object): if function_type.upper() == "A": self._raw(NUL) + def soft_barcode(self, barcode_type, data, impl='bitImageColumn', + module_height=5, module_width=0.2, text_distance=1): + + image_writer = ImageWriter() + + # Check if barcode type exists + if barcode_type not in barcode.PROVIDED_BARCODES: + raise BarcodeTypeError( + 'Barcode type {} not supported by software barcode renderer' + .format(barcode_type)) + + # Render the barcode to a fake file + barcode_class = barcode.get_barcode_class(barcode_type) + my_code = barcode_class(data, writer=image_writer) + + my_code.write("/dev/null", { + 'module_height': module_height, + 'module_width': module_width, + 'text_distance': text_distance + }) + + # Retrieve the Pillow image and print it + image = my_code.writer._image + self.image(image, impl=impl) + def text(self, txt): """ Print alpha-numeric text @@ -408,132 +460,113 @@ class Escpos(object): txt = six.text_type(txt) self.magic.write(txt) + def textln(self, txt=''): + """Print alpha-numeric text with a newline + + The text has to be encoded in the currently selected codepage. + The input text has to be encoded in unicode. + + :param txt: text to be printed with a newline + :raises: :py:exc:`~escpos.exceptions.TextError` + """ + self.text('{}\n'.format(txt)) + + def ln(self, count=1): + """Print a newline or more + + :param count: number of newlines to print + :raises: :py:exc:`ValueError` if count < 0 + """ + if count < 0: + raise ValueError('Count cannot be lesser than 0') + if count > 0: + self.text('\n' * count) + def block_text(self, txt, font=None, columns=None): """ Text is printed wrapped to specified columns Text has to be encoded in unicode. :param txt: text to be printed - :param font: font to be used, can be :code:`a` or :code`b` + :param font: font to be used, can be :code:`a` or :code:`b` :param columns: amount of columns :return: None """ col_count = self.profile.get_columns(font) if columns is None else columns self.text(textwrap.fill(txt, col_count)) - def set(self, align='left', font='a', text_type='normal', width=1, - height=1, density=9, invert=False, smooth=False, flip=False): + def set(self, align='left', font='a', bold=False, underline=0, width=1, + height=1, density=9, invert=False, smooth=False, flip=False, + double_width=False, double_height=False, custom_size=False): """ Set text properties by sending them to the printer :param align: horizontal position for text, possible values are: - * CENTER - * LEFT - * RIGHT + * 'center' + * 'left' + * 'right' - *default*: LEFT + *default*: 'left' :param font: font given as an index, a name, or one of the - special values 'a' or 'b', refering to fonts 0 and 1. - :param text_type: text type, possible values are: - - * B for bold - * U for underlined - * 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 multiplier, decimal range 1-8, *default*: 1 - :param height: text height multiplier, decimal range 1-8, *default*: 1 + special values 'a' or 'b', referring to fonts 0 and 1. + :param bold: text in bold, *default*: False + :param underline: underline mode for text, decimal range 0-2, *default*: 0 + :param double_height: doubles the height of the text + :param double_width: doubles the width of the text + :param custom_size: uses custom size specified by width and height + parameters. Cannot be used with double_width or double_height. + :param width: text width multiplier when custom_size is used, decimal range 1-8, *default*: 1 + :param height: text height multiplier when custom_size is used, decimal range 1-8, *default*: 1 :param density: print density, value from 0-8, if something else is supplied the density remains unchanged :param invert: True enables white on black printing, *default*: False :param smooth: True enables text smoothing. Effective on 4x4 size text and larger, *default*: False :param flip: True enables upside-down printing, *default*: False - :type invert: bool - """ - # Width - if height == 2 and width == 2: - self._raw(TXT_NORMAL) - self._raw(TXT_4SQUARE) - elif height == 2 and width == 1: - self._raw(TXT_NORMAL) - self._raw(TXT_2HEIGHT) - elif width == 2 and height == 1: - self._raw(TXT_NORMAL) - self._raw(TXT_2WIDTH) - elif width == 1 and height == 1: - self._raw(TXT_NORMAL) - elif 1 <= width <= 8 and 1 <= height <= 8 and isinstance(width, int) and isinstance(height, int): - self._raw(TXT_SIZE + six.int2byte(TXT_WIDTH[width] + TXT_HEIGHT[height])) - else: - raise SetVariableError() - # Upside down - if flip: - self._raw(TXT_FLIP_ON) - else: - self._raw(TXT_FLIP_OFF) - # Smoothing - if smooth: - self._raw(TXT_SMOOTH_ON) - else: - self._raw(TXT_SMOOTH_OFF) - # Type - if text_type.upper() == "B": - self._raw(TXT_BOLD_ON) - self._raw(TXT_UNDERL_OFF) - elif text_type.upper() == "U": - self._raw(TXT_BOLD_OFF) - self._raw(TXT_UNDERL_ON) - elif text_type.upper() == "U2": - self._raw(TXT_BOLD_OFF) - self._raw(TXT_UNDERL2_ON) - elif text_type.upper() == "BU": - self._raw(TXT_BOLD_ON) - self._raw(TXT_UNDERL_ON) - elif text_type.upper() == "BU2": - self._raw(TXT_BOLD_ON) - self._raw(TXT_UNDERL2_ON) - elif text_type.upper() == "NORMAL": - self._raw(TXT_BOLD_OFF) - self._raw(TXT_UNDERL_OFF) - # Font - self._raw(SET_FONT(six.int2byte(self.profile.get_font(font)))) - # Align - if align.upper() == "CENTER": - self._raw(TXT_ALIGN_CT) - elif align.upper() == "RIGHT": - self._raw(TXT_ALIGN_RT) - elif align.upper() == "LEFT": - self._raw(TXT_ALIGN_LT) - # Density - if density == 0: - self._raw(PD_N50) - elif density == 1: - self._raw(PD_N37) - elif density == 2: - self._raw(PD_N25) - elif density == 3: - self._raw(PD_N12) - elif density == 4: - self._raw(PD_0) - elif density == 5: - self._raw(PD_P12) - elif density == 6: - self._raw(PD_P25) - elif density == 7: - self._raw(PD_P37) - elif density == 8: - self._raw(PD_P50) - else: # DEFAULT: DOES NOTHING - pass - # Invert Printing - if invert: - self._raw(TXT_INVERT_ON) + :type font: str + :type invert: bool + :type bold: bool + :type underline: bool + :type smooth: bool + :type flip: bool + :type custom_size: bool + :type double_width: bool + :type double_height: bool + :type align: str + :type width: int + :type height: int + :type density: int + """ + + if custom_size: + if 1 <= width <= 8 and 1 <= height <= 8 and isinstance(width, int) and\ + isinstance(height, int): + size_byte = TXT_STYLE['width'][width] + TXT_STYLE['height'][height] + self._raw(TXT_SIZE + six.int2byte(size_byte)) + else: + raise SetVariableError() else: - self._raw(TXT_INVERT_OFF) + self._raw(TXT_NORMAL) + if double_width and double_height: + self._raw(TXT_STYLE['size']['2x']) + elif double_width: + self._raw(TXT_STYLE['size']['2w']) + elif double_height: + self._raw(TXT_STYLE['size']['2h']) + else: + self._raw(TXT_STYLE['size']['normal']) + + self._raw(TXT_STYLE['flip'][flip]) + self._raw(TXT_STYLE['smooth'][smooth]) + self._raw(TXT_STYLE['bold'][bold]) + self._raw(TXT_STYLE['underline'][underline]) + self._raw(SET_FONT(six.int2byte(self.profile.get_font(font)))) + self._raw(TXT_STYLE['align'][align]) + + if density != 9: + self._raw(TXT_STYLE['density'][density]) + + self._raw(TXT_STYLE['invert'][invert]) def line_spacing(self, spacing=None, divisor=180): """ Set line character spacing. @@ -564,7 +597,7 @@ class Escpos(object): self._raw(LINESPACING_FUNCS[divisor] + six.int2byte(spacing)) - def cut(self, mode='FULL'): + def cut(self, mode='FULL', feed=True): """ Cut paper. Without any arguments the paper will be cut completely. With 'mode=PART' a partial cut will @@ -574,8 +607,14 @@ class Escpos(object): .. todo:: Check this function on TM-T88II. :param mode: set to 'PART' for a partial cut. default: 'FULL' + :param feed: print and feed before cutting. default: true :raises ValueError: if mode not in ('FULL', 'PART') """ + + if not feed: + self._raw(GS + b'V' + six.int2byte(66) + b'\x00') + return + self.print_and_feed(6) mode = mode.upper() @@ -612,6 +651,41 @@ class Escpos(object): except: raise CashDrawerError() + def linedisplay_select(self, select_display=False): + """ Selects the line display or the printer + + This method is used for line displays that are daisy-chained between your computer and printer. + If you set `select_display` to true, only the display is selected and if you set it to false, + only the printer is selected. + + :param select_display: whether the display should be selected or the printer + :type select_display: bool + """ + if select_display: + self._raw(LINE_DISPLAY_OPEN) + else: + self._raw(LINE_DISPLAY_CLOSE) + + def linedisplay_clear(self): + """ Clears the line display and resets the cursor + + This method is used for line displays that are daisy-chained between your computer and printer. + """ + self._raw(LINE_DISPLAY_CLEAR) + + def linedisplay(self, text): + """ + Display text on a line display connected to your printer + + You should connect a line display to your printer. You can do this by daisy-chaining + the display between your computer and printer. + :param text: Text to display + """ + self.linedisplay_select(select_display=True) + self.linedisplay_clear() + self.text(text) + self.linedisplay_select(select_display=False) + def hw(self, hw): """ Hardware operations @@ -644,7 +718,7 @@ class Escpos(object): else: raise ValueError("n must be betwen 0 and 255") - def control(self, ctl, pos=4): + def control(self, ctl, count=5, tab_size=8): """ Feed control sequences :param ctl: string for the following control sequences: @@ -655,7 +729,8 @@ class Escpos(object): * HT *for Horizontal Tab* * VT *for Vertical Tab* - :param pos: integer between 1 and 16, controls the horizontal tab position + :param count: integer between 1 and 32, controls the horizontal tab count. Defaults to 5. + :param tab_size: integer between 1 and 255, controls the horizontal tab size in characters. Defaults to 8 :raises: :py:exc:`~escpos.exceptions.TabPosError` """ # Set position @@ -666,13 +741,16 @@ class Escpos(object): elif ctl.upper() == "CR": self._raw(CTL_CR) elif ctl.upper() == "HT": - if not (1 <= pos <= 16): + if not (0 <= count <= 32 and + 1 <= tab_size <= 255 and + count * tab_size < 256): raise TabPosError() else: # Set tab positions - self._raw(CTL_SET_HT + six.int2byte(pos)) - - self._raw(CTL_HT) + self._raw(CTL_SET_HT) + for iterator in range(1, count): + self._raw(six.int2byte(iterator * tab_size)) + self._raw(NUL) elif ctl.upper() == "VT": self._raw(CTL_VT) @@ -699,6 +777,41 @@ class Escpos(object): else: self._raw(PANEL_BUTTON_OFF) + def query_status(self, mode): + """ Queries the printer for its status, and returns an array of integers containing it. + :param mode: Integer that sets the status mode queried to the printer. + RT_STATUS_ONLINE: Printer status. + RT_STATUS_PAPER: Paper sensor. + :rtype: array(integer)""" + self._raw(mode) + time.sleep(1) + status = self._read() + return status + + def is_online(self): + """ Queries the printer its online status. + When online, returns True; False otherwise. + :rtype: bool: True if online, False if offline.""" + status = self.query_status(RT_STATUS_ONLINE) + if len(status) == 0: + return False + return not (status & RT_MASK_ONLINE) + + def paper_status(self): + """ Queries the printer its paper status. + Returns 2 if there is plenty of paper, 1 if the paper has arrived to + the near-end sensor and 0 if there is no paper. + :rtype: int: 2: Paper is adequate. 1: Paper ending. 0: No paper.""" + status = self.query_status(RT_STATUS_PAPER) + if len(status) == 0: + return 2 + if (status[0] & RT_MASK_NOPAPER == RT_MASK_NOPAPER): + return 0 + if (status[0] & RT_MASK_LOWPAPER == RT_MASK_LOWPAPER): + return 1 + if (status[0] & RT_MASK_PAPER == RT_MASK_PAPER): + return 2 + class EscposIO(object): """ESC/POS Printer IO object diff --git a/src/escpos/exceptions.py b/src/escpos/exceptions.py index 8c574ff..781e3e9 100644 --- a/src/escpos/exceptions.py +++ b/src/escpos/exceptions.py @@ -8,6 +8,7 @@ Result/Exit codes: - `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` + - `41` = Image width is too large :py:exc:`~escpos.exceptions.ImageWidthError` - `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` @@ -104,6 +105,20 @@ class ImageSizeError(Error): return "Image height is longer than 255px and can't be printed ({msg})".format(msg=self.msg) +class ImageWidthError(Error): + """ Image width is too large. + + The return code for this exception is `41`. + """ + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 41 + + def __str__(self): + return "Image width is too large ({msg})".format(msg=self.msg) + + class TextError(Error): """ Text string must be supplied to the `text()` method. @@ -135,7 +150,8 @@ class CashDrawerError(Error): class TabPosError(Error): - """ Valid tab positions must be in the range 0 to 16. + """ Valid tab positions must be set by using from 1 to 32 tabs, and between 1 and 255 tab size values. + Both values multiplied must not exceed 255, since it is the maximum tab value. This exception is raised by :py:meth:`escpos.escpos.Escpos.control`. The returncode for this exception is `70`. diff --git a/src/escpos/printer.py b/src/escpos/printer.py index d14b93d..b9869f6 100644 --- a/src/escpos/printer.py +++ b/src/escpos/printer.py @@ -84,6 +84,10 @@ class Usb(Escpos): """ self.device.write(self.out_ep, msg, self.timeout) + def _read(self): + """ Reads a data buffer and returns it to the caller. """ + return self.device.read(self.in_ep, 16) + def close(self): """ Release USB interface """ if self.device: @@ -131,6 +135,8 @@ class Serial(Escpos): def open(self): """ Setup serial port and set is as escpos device """ + if self.device is not None and self.device.is_open: + self.close() self.device = serial.Serial(port=self.devfile, baudrate=self.baudrate, bytesize=self.bytesize, parity=self.parity, stopbits=self.stopbits, timeout=self.timeout, @@ -149,9 +155,13 @@ class Serial(Escpos): """ self.device.write(msg) + def _read(self): + """ Reads a data buffer and returns it to the caller. """ + return self.device.read(16) + def close(self): """ Close Serial interface """ - if self.device is not None: + if self.device is not None and self.device.is_open: self.device.flush() self.device.close() diff --git a/test/test_function_cut.py b/test/test_function_cut.py new file mode 100644 index 0000000..d485e0e --- /dev/null +++ b/test/test_function_cut.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import six + +import escpos.printer as printer +from escpos.constants import GS + + +def test_cut_without_feed(): + """Test cut without feeding paper""" + instance = printer.Dummy() + instance.cut(feed=False) + expected = GS + b'V' + six.int2byte(66) + b'\x00' + assert(instance.output == expected) diff --git a/test/test_function_image.py b/test/test_function_image.py index 27b4fd7..5aac41b 100644 --- a/test/test_function_image.py +++ b/test/test_function_image.py @@ -12,9 +12,13 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -import escpos.printer as printer +import pytest + from PIL import Image +import escpos.printer as printer +from escpos.exceptions import ImageWidthError + # Raster format print def test_bit_image_black(): @@ -139,3 +143,22 @@ def test_large_graphics(): instance = printer.Dummy() instance.image('test/resources/black_white.png', impl="bitImageRaster", fragment_height=1) assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\xc0\x1dv0\x00\x01\x00\x01\x00\x00') + + +def test_width_too_large(): + """ + Test printing an image that is too large in width. + """ + instance = printer.Dummy() + instance.profile.profile_data = { + 'media': { + 'width': { + 'pixels': 384 + } + } + } + + with pytest.raises(ImageWidthError): + instance.image(Image.new("RGB", (385, 200))) + + instance.image(Image.new("RGB", (384, 200))) \ No newline at end of file diff --git a/test/test_function_linedisplay.py b/test/test_function_linedisplay.py new file mode 100644 index 0000000..f5e92ca --- /dev/null +++ b/test/test_function_linedisplay.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +"""tests for line display + +:author: `Patrick Kanzler `_ +:organization: `python-escpos `_ +:copyright: Copyright (c) 2017 `python-escpos `_ +:license: MIT +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import escpos.printer as printer + + +def test_function_linedisplay_select_on(): + """test the linedisplay_select function (activate)""" + instance = printer.Dummy() + instance.linedisplay_select(select_display=True) + assert(instance.output == b'\x1B\x3D\x02') + +def test_function_linedisplay_select_off(): + """test the linedisplay_select function (deactivate)""" + instance = printer.Dummy() + instance.linedisplay_select(select_display=False) + assert(instance.output == b'\x1B\x3D\x01') + +def test_function_linedisplay_clear(): + """test the linedisplay_clear function""" + instance = printer.Dummy() + instance.linedisplay_clear() + assert(instance.output == b'\x1B\x40') + diff --git a/test/test_function_panel_button.py b/test/test_function_panel_button.py index 1006e5b..cdf840b 100644 --- a/test/test_function_panel_button.py +++ b/test/test_function_panel_button.py @@ -12,43 +12,18 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -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_function_panel_button_on(): """test the panel button function (enabling) by comparing output""" - instance = printer.File(devfile=devfile) + instance = printer.Dummy() instance.panel_buttons() - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'\x1B\x63\x35\x00') + assert(instance.output == b'\x1B\x63\x35\x00') -@with_setup(setup_testfile, teardown_testfile) def test_function_panel_button_off(): """test the panel button function (disabling) by comparing output""" - instance = printer.File(devfile=devfile) + instance = printer.Dummy() instance.panel_buttons(False) - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'\x1B\x63\x35\x01') + assert(instance.output == b'\x1B\x63\x35\x01') diff --git a/test/test_function_qr_native.py b/test/test_function_qr_native.py index 2fbaa64..4aa9c1d 100644 --- a/test/test_function_qr_native.py +++ b/test/test_function_qr_native.py @@ -86,9 +86,11 @@ def test_image(): instance = printer.Dummy() instance.qr("1", native=False, size=1) print(instance.output) - expected = b'\x1dv0\x00\x03\x00\x17\x00\x00\x00\x00\x7f]\xfcA\x19\x04]it]et' \ + expected = b'\x1bt\x00\n' \ + b'\x1dv0\x00\x03\x00\x17\x00\x00\x00\x00\x7f]\xfcA\x19\x04]it]et' \ b']ItA=\x04\x7fU\xfc\x00\x0c\x00y~t4\x7f =\xa84j\xd9\xf0\x05\xd4\x90\x00' \ - b'i(\x7f<\xa8A \xd8]\'\xc4]y\xf8]E\x80Ar\x94\x7fR@\x00\x00\x00' + b'i(\x7f<\xa8A \xd8]\'\xc4]y\xf8]E\x80Ar\x94\x7fR@\x00\x00\x00' \ + b'\n\n' assert(instance.output == expected) diff --git a/test/test_function_set.py b/test/test_function_set.py new file mode 100644 index 0000000..777eb32 --- /dev/null +++ b/test/test_function_set.py @@ -0,0 +1,280 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import six + +import escpos.printer as printer +from escpos.constants import TXT_NORMAL, TXT_STYLE, SET_FONT +from escpos.constants import TXT_SIZE + + +# Default test, please copy and paste this block to test set method calls + +def test_default_values(): + instance = printer.Dummy() + instance.set() + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert(instance.output == b''.join(expected_sequence)) + +# Size tests + +def test_set_size_2h(): + instance = printer.Dummy() + instance.set(double_height=True) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['2h'], # Double height text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert (instance.output == b''.join(expected_sequence)) + + +def test_set_size_2w(): + instance = printer.Dummy() + instance.set(double_width=True) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['2w'], # Double width text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert (instance.output == b''.join(expected_sequence)) + + +def test_set_size_2x(): + instance = printer.Dummy() + instance.set(double_height=True, double_width=True) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['2x'], # Double text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert (instance.output == b''.join(expected_sequence)) + + +def test_set_size_custom(): + instance = printer.Dummy() + instance.set(custom_size=True, width=8, height=7) + + expected_sequence = ( + TXT_SIZE, # Custom text size, no normal reset + six.int2byte(TXT_STYLE['width'][8] + TXT_STYLE['height'][7]), + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert (instance.output == b''.join(expected_sequence)) + +# Flip + +def test_set_flip(): + instance = printer.Dummy() + instance.set(flip=True) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][True], # Flip ON + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert (instance.output == b''.join(expected_sequence)) + +# Smooth + +def test_smooth(): + instance = printer.Dummy() + instance.set(smooth=True) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][True], # Smooth ON + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert(instance.output == b''.join(expected_sequence)) + + +# Type + + +def test_set_bold(): + instance = printer.Dummy() + instance.set(bold=True) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][True], # Bold ON + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert (instance.output == b''.join(expected_sequence)) + + +def test_set_underline(): + instance = printer.Dummy() + instance.set(underline=1) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][1], # Underline ON, type 1 + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert (instance.output == b''.join(expected_sequence)) + + +def test_set_underline2(): + instance = printer.Dummy() + instance.set(underline=2) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][2], # Underline ON, type 2 + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert (instance.output == b''.join(expected_sequence)) + + +# Align + +def test_align_center(): + instance = printer.Dummy() + instance.set(align='center') + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['center'], # Align center + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert(instance.output == b''.join(expected_sequence)) + + +def test_align_right(): + instance = printer.Dummy() + instance.set(align='right') + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['right'], # Align right + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert(instance.output == b''.join(expected_sequence)) + + +# Densities + +def test_densities(): + + for density in range(8): + instance = printer.Dummy() + instance.set(density=density) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['density'][density], # Custom density from 0 to 8 + TXT_STYLE['invert'][False] # Inverted OFF + ) + + assert(instance.output == b''.join(expected_sequence)) + + +# Invert + +def test_invert(): + instance = printer.Dummy() + instance.set(invert=True) + + expected_sequence = ( + TXT_NORMAL, TXT_STYLE['size']['normal'], # Normal text size + TXT_STYLE['flip'][False], # Flip OFF + TXT_STYLE['smooth'][False], # Smooth OFF + TXT_STYLE['bold'][False], # Bold OFF + TXT_STYLE['underline'][0], # Underline OFF + SET_FONT(b'\x00'), # Default font + TXT_STYLE['align']['left'], # Align left + TXT_STYLE['invert'][True] # Inverted ON + ) + + assert(instance.output == b''.join(expected_sequence)) \ No newline at end of file diff --git a/test/test_function_text.py b/test/test_function_text.py index 04222a1..81b0792 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -39,3 +39,27 @@ def test_block_text(): "All the presidents men were eating falafel for breakfast.", font='a') assert printer.output == \ b'All the presidents men were eating falafel\nfor breakfast.' + + +def test_textln(): + printer = get_printer() + printer.textln('hello, world') + assert printer.output == b'hello, world\n' + + +def test_textln_empty(): + printer = get_printer() + printer.textln() + assert printer.output == b'\n' + + +def test_ln(): + printer = get_printer() + printer.ln() + assert printer.output == b'\n' + + +def test_multiple_ln(): + printer = get_printer() + printer.ln(3) + assert printer.output == b'\n\n\n' diff --git a/test/test_load_module.py b/test/test_load_module.py index aeffc5b..efae1b3 100644 --- a/test/test_load_module.py +++ b/test/test_load_module.py @@ -12,30 +12,10 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -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 = printer.Dummy() instance.text('This is a test\n') diff --git a/tox.ini b/tox.ini index d51853c..a61a4a8 100644 --- a/tox.ini +++ b/tox.ini @@ -7,10 +7,11 @@ deps = nose coverage scripttest mock - pytest + pytest!=3.2.0 pytest-cov pytest-mock hypothesis + viivakoodi commands = py.test --cov escpos [testenv:docs] @@ -18,6 +19,7 @@ basepython = python changedir = doc deps = sphinx>=1.5.1 setuptools_scm + viivakoodi commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html [testenv:flake8]