diff --git a/.travis.yml b/.travis.yml index 76794ac..cce6180 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,3 +34,7 @@ before_install: script: - tox - codecov +notifications: + email: + on_success: never + on_failure: change diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 47227ae..090a8e8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,8 @@ ********* Changelog ********* -2016-08-?? - Version 2.?.? - "Death and Gravity" + +2016-08-?? - Version 2.?.? - "?" ------------------------------------------------ changes @@ -14,6 +15,32 @@ contributors - Patrick Kanzler (with code by Frédéric Van der Essen) +2016-08-10 - Version 2.1.3 - "Ethics Gradient" +---------------------------------------------- + +changes +^^^^^^^ +- configure readthedocs and travis +- update doc with hint on image preprocessing +- add fix for printing large images (by splitting them into multiple images) + +contributors +^^^^^^^^^^^^ +- Patrick Kanzler + +2016-08-02 - Version 2.1.2 - "Death and Gravity" +------------------------------------------------ + +changes +^^^^^^^ +- fix File-printer: flush after every call of _raw() +- fix lists in documentation +- fix CODE128: by adding the control character to the barcode-selection-sequence the barcode became unusable + +contributors +^^^^^^^^^^^^ +- Patrick Kanzler + 2016-08-02 - Version 2.1.1 - "Contents May Differ" -------------------------------------------------- diff --git a/doc/index.rst b/doc/index.rst index 8e876a0..060fc7f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -18,6 +18,7 @@ Content user/raspi user/todo user/usage + user/barcode .. toctree:: :maxdepth: 1 diff --git a/doc/user/barcode.rst b/doc/user/barcode.rst new file mode 100644 index 0000000..5d45419 --- /dev/null +++ b/doc/user/barcode.rst @@ -0,0 +1,34 @@ +Printing Barcodes +----------------- +:Last Reviewed: 2016-07-31 + +Most ESC/POS-printers implement barcode-printing. +The barcode-commandset is implemented in the barcode-method. +For a list of compatible barcodes you should check the manual of your printer. +As a rule of thumb: even older Epson-models support most 1D-barcodes. +To be sure just try some implementations and have a look at the notices below. + +barcode-method +~~~~~~~~~~~~~~ +The barcode-method is rather low-level and orients itself on the implementation of ESC/POS. +In the future this class could be supplemented by a high-level class that helps the user generating the payload. + +.. py:currentmodule:: escpos.escpos + +.. automethod:: Escpos.barcode + :noindex: + +CODE128 +~~~~~~~ +Code128 barcodes need a certain format. +For now the user has to make sure that the payload is correct. +For alphanumeric CODE128 you have to preface your payload with `{B`. + +.. code-block:: Python + + from escpos.printer import Dummy, Serial + p = Serial() + # print CODE128 012ABCDabcd + p.barcode("{B012ABCDabcd", "CODE128", function_type="B") + +A very good description on CODE128 is also on `Wikipedia `_. diff --git a/doc/user/usage.rst b/doc/user/usage.rst index 3ef1c58..e507cb5 100644 --- a/doc/user/usage.rst +++ b/doc/user/usage.rst @@ -218,3 +218,34 @@ Here you can download an example, that will print a set of common barcodes: * :download:`barcode.bin ` by `@mike42 `_ +Hint: preprocess printing +------------------------- + +Printing images directly to the printer is rather slow. +One factor that slows down the process is the transmission over e.g. serial port. + +Apart from configuring your printer to use the maximum baudrate (in the case of serial-printers), there is not much +that you can do. +However you could use the :py:class:`escpos.printer.Dummy`-printer to preprocess your image. +This is probably best explained by an example: + +.. code-block:: Python + + from escpos.printer import Serial, Dummy + + p = Serial() + d = Dummy() + + # create ESC/POS for the print job, this should go really fast + d.text("This is my image:\n") + d.image("funny_cat.png") + d.cut() + + # send code to printer + p._raw(d.output) + +This way you could also store the code in a file and print later. +You could then for example print the code from another process than your main-program and thus reduce the waiting time. +(Of course this will not make the printer print faster.) + + diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 0000000..ecf365d --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,7 @@ +formats: + - pdf + - epub +requirements_file: doc/requirements.txt +python: + version: 2 + setup_py_install: true \ No newline at end of file diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py new file mode 100644 index 0000000..2b08083 --- /dev/null +++ b/src/escpos/capabilities.py @@ -0,0 +1,83 @@ +import re +from os import path +import yaml + + +with open(path.join(path.dirname(__file__), 'capabilities.yml')) as f: + PROFILES = yaml.load(f) + + +class Profile(object): + + profile_data = {} + + def __init__(self, columns=None): + self.default_columns = columns + + def __getattr__(self, name): + return self.profile_data[name] + + def get_columns(self, font): + """ Return the number of columns for the given font. + """ + if self.default_columns: + return self.default_columns + + if 'columnConfigs' in self.profile_data: + columns_def = self.columnConfigs[self.defaultColumnConfig] + + elif 'columns' in self.profile_data: + columns_def = self.columns + + if isinstance(columns_def, int): + return columns_def + return columns_def[font] + + +def get_profile(name=None, **kwargs): + if isinstance(name, Profile): + return name + + clazz = get_profile_class(name or 'default') + return clazz(**kwargs) + + + +CLASS_CACHE = {} + + +def get_profile_class(name): + if not name in CLASS_CACHE: + profile_data = resolve_profile_data(name) + class_name = '%sProfile' % clean(name) + new_class = type(class_name, (Profile,), {'profile_data': profile_data}) + CLASS_CACHE[name] = new_class + + return CLASS_CACHE[name] + + +def clean(s): + # Remove invalid characters + s = re.sub('[^0-9a-zA-Z_]', '', s) + # Remove leading characters until we find a letter or underscore + s = re.sub('^[^a-zA-Z_]+', '', s) + return str(s) + + +def resolve_profile_data(name): + data = PROFILES[name] + inherits = data.get('inherits') + if not inherits: + return data + + if not isinstance(inherits, (tuple, list)): + inherits = [inherits] + + merged = {} + for base in reversed(inherits): + base_data = resolve_profile_data(base) + merged.update(base_data) + merged.update(data) + return merged + + diff --git a/src/escpos/capabilities.yml b/src/escpos/capabilities.yml new file mode 100644 index 0000000..e105687 --- /dev/null +++ b/src/escpos/capabilities.yml @@ -0,0 +1,207 @@ +# Description of the format +abstract: + # Defines non-standard code pages that the printer supports, but + # that we won't find in Python's encoding system. If you define one + # here, don't forget to add it to codePageMap to assign it to a slot. + customCodePages: + sample: + + # This maps the indexed code page slots to code page names. + # Often, the slot assignment is the same, but the device only + # supports a subset. + codePageMap: + 0: "CP437" + 1: "CP932" + 3: "sample" + + # Maybe not all of the codepages in the map are supported. This + # is for subprofiles to select which ones the device knows. + codePages: [sample, cp932] + + +# Many recent Epson-branded thermal receipt printers. +default: + columns: 42 + + barcodeB: true + bitImage: true + graphics: true + starCommands: false + qrCode: true + + customCodePages: + TCVN-3-1: [ + " ", + " ", + " ăâêôơưđ ", + " àảãáạ ằẳẵắ ", + " ặầẩẫấậè ẻẽ", + "éẹềểễếệìỉ ĩíịò", + " ỏõóọồổỗốộờởỡớợù", + " ủũúụừửữứựỳỷỹýỵ ", + ] + TCVN-3-2: [ + " ", + " ", + " ĂÂ Ð ÊÔƠƯ ", + " ÀẢÃÁẠ ẰẲẴẮ ", + " ẶẦẨẪẤẬÈ ẺẼ", + "ÉẸỀỂỄẾỆÌỈ ĨÍỊÒ", + " ỎÕÓỌỒỔỖỐỘỜỞỠỚỢÙ", + " ỦŨÚỤỪỬỮỨỰỲỶỸÝỴ " + ] + + + # Commented-out slots are TODO (might just need uncomment, might + # need verification/research) + codePageMap: + 0: "CP437" + 1: "CP932" + 2: "CP850" + 3: "CP860" + 4: "CP863" + 5: "CP865" + #6: // Hiragana + #7: // One-pass printing Kanji characters + #8: // Page 8 [One-pass printing Kanji characters] + 11: "CP851" + 12: "CP853" + 13: "CP857" + 14: "CP737" + 15: "ISO8859_7" + 16: "CP1252" + 17: "CP866" + 18: "CP852" + 19: "CP858" + #20: // Thai Character Code 42 + #21: // Thai Character Code 1" + #22: // Thai Character Code 13 + #23: // Thai Character Code 14 + #24: // Thai Character Code 16 + #25: // Thai Character Code 17 + #26: // Thai Character Code 18 + 30: 'TCVN-3-1', # TCVN-3: Vietnamese + 31: 'TCVN-3-2', # TCVN-3: Vietnamese + 32: "CP720" + 33: "CP775" + 34: "CP855" + 35: "CP861" + 36: "CP862" + 37: "CP864" + 38: "CP869" + 39: "ISO8859_2" + 40: "ISO8859_15" + 41: "CP1098" + 42: "CP774" + 43: "CP772" + 44: "CP1125" + 45: "CP1250" + 46: "CP1251" + 47: "CP1253" + 48: "CP1254" + 49: "CP1255" + 50: "CP1256" + 51: "CP1257" + 52: "CP1258" + 53: "RK1048" + #66: // Devanagari + #67: // Bengali + #68: // Tamil + #69: // Telugu + #70: // Assamese + #71: // Oriya + #72: // Kannada + #73: // Malayalam + #74: // Gujarati + #75: // Punjabi + #82: // Marathi + #254: + #255: + + +# Designed for non-Epson printers sold online. Without knowing +# their character encoding table, only CP437 output is assumed, +# and graphics() calls will be disabled, as it usually prints junk +# on these models. +simple: + codePages: + - cp437 + graphics: false + + +# Profile for Star-branded printers. +star: + inherits: default + starCommands: true + + +epson: + inherits: default + manufacturer: "Epson" + + +"P-822D": + inherits: default + graphics: false + + +# http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf +"TM-T88II": + inherits: epson + columns: + a: 42 + b: 56 + codePages: + - PC437 # 0 + - Katakana # 1 + - PC850 # 2 + - PC860 # 3 + - PC863 # 4 + - PC865 # 5 + - PC858 # 19 + - blank + +# http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf +"TM-T88III": + inherits: epson + columns: + a: 42 + b: 56 + codePages: + - PC437 # 0 + - Katakana # 1 + - PC850 # 2 + - PC860 # 3 + - PC863 # 4 + - PC865 # 5 + - WPC1252 # 16 + - PC866 # 17 + - PC852 # 18 + - PC858 # 19 + - blank + + +"TM-P80": + inherits: epson + defaultColumnConfig: default + columnConfigs: + default: {'a': 48, 'b': 64, 'kanji': 24} + '42_emulation': {'a': 42, 'b': 60, 'kanji': 21} + + +"TM-P60II 2": + inherits: epson + columnConfigs: + '58mm_paper': {'a': 35, 'b': 42, 'c': 52} + '60mm_paper': {'a': 36, 'b': 43, 'c': 54} + + +"TM-P20 2": + inherits: epson + # Has 5 fonts! + +"TM-T90": + inherits: epson + colors: + - black + - red \ No newline at end of file diff --git a/src/escpos/constants.py b/src/escpos/constants.py index f91cbdf..b9625c1 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -240,10 +240,7 @@ BARCODE_TYPE_B = { 'NW7': _SET_BARCODE_TYPE(71), 'CODABAR': _SET_BARCODE_TYPE(71), # Same as NW7 'CODE93': _SET_BARCODE_TYPE(72), - # These are all the same barcode, but using different charcter sets - 'CODE128A': _SET_BARCODE_TYPE(73) + b'{A', # CODE128 character set A - 'CODE128B': _SET_BARCODE_TYPE(73) + b'{B', # CODE128 character set B - 'CODE128C': _SET_BARCODE_TYPE(73) + b'{C', # CODE128 character set C + 'CODE128': _SET_BARCODE_TYPE(73), 'GS1-128': _SET_BARCODE_TYPE(74), 'GS1 DATABAR OMNIDIRECTIONAL': _SET_BARCODE_TYPE(75), 'GS1 DATABAR TRUNCATED': _SET_BARCODE_TYPE(76), diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 276ded9..a217db7 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -24,6 +24,7 @@ from .magicencode import MagicEncode from abc import ABCMeta, abstractmethod # abstract base class support from escpos.image import EscposImage +from escpos.capabilities import get_profile @six.add_metaclass(ABCMeta) @@ -35,11 +36,11 @@ class Escpos(object): """ device = None - def __init__(self, columns=32, **kwargs): + def __init__(self, profile=None, **kwargs): """ Initialize ESCPOS Printer - :param columns: Text columns used by the printer. Defaults to 32.""" - self.columns = columns + :param profile: Printer profile""" + self.profile = get_profile(profile) self.magic = MagicEncode(**kwargs) def __del__(self): @@ -57,7 +58,8 @@ class Escpos(object): """ pass - def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster"): + def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster", + fragment_height=1024): """ Print an image You can select whether the printer should print in high density or not. The default value is high density. @@ -77,9 +79,20 @@ class Escpos(object): :param high_density_vertical: print in high density in vertical direction *default:* True :param high_density_horizontal: print in high density in horizontal direction *default:* True :param impl: choose image printing mode between `bitImageRaster`, `graphics` or `bitImageColumn` + :param fragment_height: Images larger than this will be split into multiple fragments *default:* 1024 """ im = EscposImage(img_source) + + if im.height > fragment_height: + fragments = im.split(fragment_height) + for fragment in fragments: + self.image(fragment, + high_density_vertical=high_density_vertical, + high_density_horizontal=high_density_horizontal, + impl=impl, + fragment_height=fragment_height) + return if impl == "bitImageRaster": # GS v 0, raster format bit image @@ -362,7 +375,7 @@ class Escpos(object): txt = six.text_type(txt) self._raw(self.magic.encode_text(txt=txt)) - def block_text(self, txt, columns=None): + def block_text(self, txt, font=None, columns=None): """ Text is printed wrapped to specified columns Text has to be encoded in unicode. @@ -371,7 +384,7 @@ class Escpos(object): :param columns: amount of columns :return: None """ - col_count = self.columns if columns is None else columns + 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, diff --git a/src/escpos/image.py b/src/escpos/image.py index 1180614..abf2e22 100644 --- a/src/escpos/image.py +++ b/src/escpos/image.py @@ -8,6 +8,12 @@ This module contains the image format handler :py:class:`EscposImage`. :license: GNU GPL v3 """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import math from PIL import Image, ImageOps @@ -30,6 +36,9 @@ class EscposImage(object): else: img_original = Image.open(img_source) + # store image for eventual further processing (splitting) + self.img_original = img_original + # Convert to white RGB background, paste over white background # to strip alpha. img_original = img_original.convert('RGBA') @@ -88,3 +97,21 @@ class EscposImage(object): Convert image to raster-format binary """ return self._im.tobytes() + + def split(self, fragment_height): + """ + Split an image into multiple fragments after fragment_height pixels + + :param fragment_height: height of fragment + :return: list of PIL objects + """ + passes = int(math.ceil(self.height/fragment_height)) + fragments = [] + for n in range(0, passes): + left = 0 + right = self.width + upper = n * fragment_height + lower = min((n + 1) * fragment_height, self.height) + box = (left, upper, right, lower) + fragments.append(self.img_original.crop(box)) + return fragments diff --git a/test/test_function_image.py b/test/test_function_image.py index f60aa76..b1762f6 100644 --- a/test/test_function_image.py +++ b/test/test_function_image.py @@ -130,3 +130,12 @@ def test_graphics_transparent(): instance = printer.Dummy() instance.image('test/resources/black_transparent.png', impl="graphics") assert(instance.output == b'\x1d(L\x0c\x000p0\x01\x011\x02\x00\x02\x00\xc0\x00\x1d(L\x02\x0002') + + +def test_large_graphics(): + """ + Test whether 'large' graphics that induce a fragmentation are handled correctly. + """ + 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') diff --git a/test/test_function_qr_non-native.py b/test/test_function_qr_non-native.py new file mode 100644 index 0000000..5c0566a --- /dev/null +++ b/test/test_function_qr_non-native.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""tests for the non-native part of qr() + +:author: `Patrick Kanzler `_ +:organization: `python-escpos `_ +:copyright: Copyright (c) 2016 `python-escpos `_ +:license: GNU GPL v3 +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import mock + +from escpos.printer import Dummy +from PIL import Image + + +@mock.patch('escpos.printer.Dummy.image', spec=Dummy) +def test_type_of_object_passed_to_image_function(img_function): + """ + Test the type of object that is passed to the image function during non-native qr-printing. + + The type should be PIL.Image + """ + d = Dummy() + d.qr("LoremIpsum") + args, kwargs = img_function.call_args + assert isinstance(args[0], Image.Image) diff --git a/test/test_function_text.py b/test/test_function_text.py index c9b0bd0..e03c305 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -15,9 +15,10 @@ from __future__ import unicode_literals import mock from hypothesis import given import hypothesis.strategies as st - +from escpos.printer import Dummy import escpos.printer as printer + @given(text=st.text()) def test_function_text_dies_ist_ein_test_lf(text): """test the text printing function with simple string and compare output""" @@ -25,3 +26,11 @@ def test_function_text_dies_ist_ein_test_lf(text): instance.magic.encode_text = mock.Mock() instance.text(text) instance.magic.encode_text.assert_called_with(txt=text) + + +def test_block_text(): + printer = Dummy() + printer.block_text( + "All the presidents men were eating falafel for breakfast.", font='a') + assert printer.output == \ + 'All the presidents men were eating falafel\nfor breakfast.' diff --git a/test/test_image.py b/test/test_image.py index dbfe5b0..6fda6dd 100644 --- a/test/test_image.py +++ b/test/test_image.py @@ -43,6 +43,20 @@ def test_image_white(): _load_and_check_img('canvas_white.' + img_format, 1, 1, b'\x00', [b'\x00']) +def test_split(): + """ + test whether the split-function works as expected + """ + im = EscposImage('test/resources/black_white.png') + (upper_part, lower_part) = im.split(1) + upper_part = EscposImage(upper_part) + lower_part = EscposImage(lower_part) + assert(upper_part.width == lower_part.width == 2) + assert(upper_part.height == lower_part.height == 1) + assert(upper_part.to_raster_format() == b'\xc0') + assert(lower_part.to_raster_format() == b'\x00') + + def _load_and_check_img(filename, width_expected, height_expected, raster_format_expected, column_format_expected): """ Load an image, and test whether raster & column formatted output, sizes, etc match expectations.