From 4b04a5c425a9426da126eb3cc7d758225d83c0b0 Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Sat, 13 May 2017 18:42:05 +0200 Subject: [PATCH 01/37] Fixed bad format of :code: in documentation --- src/escpos/escpos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index c238259..3faba40 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -414,7 +414,7 @@ class Escpos(object): 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 """ From 737cc3176e36d571dffa83d7124c8a21d93322ab Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Tue, 16 May 2017 20:55:30 +0200 Subject: [PATCH 02/37] First implementation of software barcode Actually the hardware barcode implementation is very specific and not generic enough for just adding a `soft_render=True` argument to it. This is a first work that can be improved with other commits, maybe for merging this method in the `barcode` method after some cleanup. The width, height and text_distance were set using empiric print-and-retry tests so that the generated barcode looks nice to the eye (and to the eye of an Android scanner tool. !WARNING! Printing a barcode that is too large in width will result in the printer to go crazy trying to print an image that is too large for it. This may be fixed by raising an exception in the `image` method. --- examples/software_barcode.py | 8 ++++++++ setup.py | 3 ++- src/escpos/escpos.py | 23 +++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 examples/software_barcode.py diff --git a/examples/software_barcode.py b/examples/software_barcode.py new file mode 100644 index 0000000..8476bac --- /dev/null +++ b/examples/software_barcode.py @@ -0,0 +1,8 @@ +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') \ No newline at end of file diff --git a/setup.py b/setup.py index c5d77f6..bd4a3ed 100755 --- a/setup.py +++ b/setup.py @@ -115,7 +115,8 @@ setup( 'pyyaml', 'argparse', 'argcomplete', - 'future' + 'future', + 'pyBarcode==0.8b1' ], setup_requires=[ 'setuptools_scm', diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 3faba40..d23bb05 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -19,6 +19,9 @@ import qrcode import textwrap import six +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 @@ -396,6 +399,26 @@ class Escpos(object): if function_type.upper() == "A": self._raw(NUL) + def soft_barcode(self, barcode_type, data, module_height=5, module_width=0.2, text_distance=1): + image_writer = ImageWriter() + + if barcode_type not in barcode.PROVIDED_BARCODES: + raise BarcodeTypeError( + 'Barcode type {} not supported by software barcode renderer' + .format(barcode_type)) + + 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 + }) + + image = my_code.writer._image + self.image(image, impl='bitImageColumn') + def text(self, txt): """ Print alpha-numeric text From a16d6bde060bcde53f3be2aa9bf5ab30ebf4c28a Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Sun, 14 May 2017 10:38:21 +0200 Subject: [PATCH 03/37] Refactor of the set method, with tests --- src/escpos/constants.py | 124 ++++++++++------- src/escpos/escpos.py | 120 +++++----------- test/test_function_set.py | 280 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 387 insertions(+), 137 deletions(-) create mode 100644 test/test_function_set.py diff --git a/src/escpos/constants.py b/src/escpos/constants.py index e3225f8..821c143 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -71,51 +71,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 = { @@ -205,14 +244,3 @@ S_RASTER_N = _PRINT_RASTER_IMG(b'\x00') # Set raster image normal size 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% diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 3faba40..b7040cd 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -21,16 +21,15 @@ import six 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 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 TXT_STYLE from .exceptions import BarcodeTypeError, BarcodeSizeError, TabPosError from .exceptions import CashDrawerError, SetVariableError, BarcodeCodeError @@ -356,7 +355,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)) @@ -421,8 +420,9 @@ class Escpos(object): 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, + size='normal'): """ Set text properties by sending them to the printer :param align: horizontal position for text, possible values are: @@ -453,87 +453,29 @@ class Escpos(object): :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) - else: - self._raw(TXT_INVERT_OFF) + if size in TXT_STYLE['size']: + self._raw(TXT_NORMAL) + self._raw(TXT_STYLE['size'][size]) + elif size == 'custom': + 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() + + 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. diff --git a/test/test_function_set.py b/test/test_function_set.py new file mode 100644 index 0000000..274011e --- /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(size='2h') + + 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(size='2w') + + 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(size='2x') + + 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(size='custom', 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 From c0b4d0369261fb7d3c821b8390f649296fb77273 Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Sun, 14 May 2017 14:41:56 +0200 Subject: [PATCH 04/37] Updated documentation of set method --- src/escpos/escpos.py | 47 ++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index b7040cd..d2cd483 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -427,31 +427,44 @@ class Escpos(object): :param align: horizontal position for text, possible values are: - * CENTER - * LEFT - * RIGHT + * 'center' + * 'left' + * 'right' - *default*: LEFT + *default*: 'left' + + :param size: size modifier for text, possible values are: + + * 'normal' + * '2h' for double text height + * '2w' for double text width + * '2x' for double text height and width (doubles the text surface) + * 'custom' for custom text height and width + + In this last case, see the width and height parameters. + *default*: 'normal' :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 width: text width multiplier when size is set to custom, decimal range 1-8, *default*: 1 + :param height: text height multiplier when size is set to custom, 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 + :type bold: bool + :type underline: bool + :type smooth: bool + :type flip: bool + :type size: str + :type align: str + :type width: int + :type height: int + :type density: int """ if size in TXT_STYLE['size']: From a6e1d0df00c334c57ef547e80835d98381b1aeb8 Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Mon, 15 May 2017 18:54:54 +0200 Subject: [PATCH 05/37] Using booleans for handling text size --- src/escpos/escpos.py | 41 +++++++++++++++++++++------------------ test/test_function_set.py | 8 ++++---- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index d2cd483..d6ff2bb 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -422,7 +422,7 @@ class Escpos(object): def set(self, align='left', font='a', bold=False, underline=0, width=1, height=1, density=9, invert=False, smooth=False, flip=False, - size='normal'): + 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: @@ -433,50 +433,53 @@ class Escpos(object): *default*: 'left' - :param size: size modifier for text, possible values are: - - * 'normal' - * '2h' for double text height - * '2w' for double text width - * '2x' for double text height and width (doubles the text surface) - * 'custom' for custom text height and width - - In this last case, see the width and height parameters. - *default*: 'normal' - :param font: font given as an index, a name, or one of the 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 width: text width multiplier when size is set to custom, decimal range 1-8, *default*: 1 - :param height: text height multiplier when size is set to custom, decimal range 1-8, *default*: 1 + :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 font: str :type invert: bool :type bold: bool :type underline: bool :type smooth: bool :type flip: bool - :type size: str + :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 size in TXT_STYLE['size']: - self._raw(TXT_NORMAL) - self._raw(TXT_STYLE['size'][size]) - elif size == 'custom': + 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_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]) diff --git a/test/test_function_set.py b/test/test_function_set.py index 274011e..777eb32 100644 --- a/test/test_function_set.py +++ b/test/test_function_set.py @@ -33,7 +33,7 @@ def test_default_values(): def test_set_size_2h(): instance = printer.Dummy() - instance.set(size='2h') + instance.set(double_height=True) expected_sequence = ( TXT_NORMAL, TXT_STYLE['size']['2h'], # Double height text size @@ -51,7 +51,7 @@ def test_set_size_2h(): def test_set_size_2w(): instance = printer.Dummy() - instance.set(size='2w') + instance.set(double_width=True) expected_sequence = ( TXT_NORMAL, TXT_STYLE['size']['2w'], # Double width text size @@ -69,7 +69,7 @@ def test_set_size_2w(): def test_set_size_2x(): instance = printer.Dummy() - instance.set(size='2x') + instance.set(double_height=True, double_width=True) expected_sequence = ( TXT_NORMAL, TXT_STYLE['size']['2x'], # Double text size @@ -87,7 +87,7 @@ def test_set_size_2x(): def test_set_size_custom(): instance = printer.Dummy() - instance.set(size='custom', width=8, height=7) + instance.set(custom_size=True, width=8, height=7) expected_sequence = ( TXT_SIZE, # Custom text size, no normal reset From 1f427953a893076796c73b0f8773a1b654403d0c Mon Sep 17 00:00:00 2001 From: TAHRI Ahmed Date: Tue, 15 Nov 2016 16:24:44 +0100 Subject: [PATCH 06/37] Preliminary support of pos 'line display' printing --- src/escpos/constants.py | 5 +++++ src/escpos/escpos.py | 36 +++++++++++++++++++++++++++++++ test/test_function_linedisplay.py | 35 ++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 test/test_function_linedisplay.py diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 821c143..4e61419 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 diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index d6ff2bb..42c7305 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -26,6 +26,7 @@ from .constants import BARCODE_TXT_OFF, BARCODE_TXT_BTH, BARCODE_TXT_ABV, BARCOD 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 @@ -570,6 +571,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 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') + From 5bf26367538f9ffb572659b3c49582aa29dbff2a Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 22 May 2017 00:44:22 +0200 Subject: [PATCH 07/37] rewrite to Dummy() --- test/test_function_panel_button.py | 33 ++++-------------------------- test/test_load_module.py | 22 +------------------- 2 files changed, 5 insertions(+), 50 deletions(-) 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_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') From 22cf6ad00bb89128d626f647960403bf47fbd72f Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Mon, 22 May 2017 20:21:35 +0200 Subject: [PATCH 08/37] Allow users to change impl for soft_barcode --- src/escpos/escpos.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index d23bb05..96f8415 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -399,14 +399,18 @@ class Escpos(object): if function_type.upper() == "A": self._raw(NUL) - def soft_barcode(self, barcode_type, data, module_height=5, module_width=0.2, text_distance=1): + 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) @@ -416,8 +420,9 @@ class Escpos(object): 'text_distance': text_distance }) + # Retrieve the Pillow image and print it image = my_code.writer._image - self.image(image, impl='bitImageColumn') + self.image(image, impl=impl) def text(self, txt): """ Print alpha-numeric text From d34871243972338846d952ff297d7d435f0a155f Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Mon, 22 May 2017 20:25:51 +0200 Subject: [PATCH 09/37] PEP8 software barcode example --- examples/software_barcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/software_barcode.py b/examples/software_barcode.py index 8476bac..2fd3c18 100644 --- a/examples/software_barcode.py +++ b/examples/software_barcode.py @@ -1,8 +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') \ No newline at end of file +p.soft_barcode('code39', '123456') From c4dd4f2960bd6f6240b72515d071210cd35a0e77 Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Tue, 23 May 2017 15:13:28 +0200 Subject: [PATCH 10/37] Added ImageWidthError and its implementation (#226) * Added ImageWidthError and its implementation * Added unit tests for ImageWidthError * Parse max_width to int before compare --- src/escpos/escpos.py | 12 ++++++++++++ src/escpos/exceptions.py | 15 +++++++++++++++ test/test_function_image.py | 25 ++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 42c7305..606a52c 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -34,6 +34,7 @@ from .constants import TXT_STYLE from .exceptions import BarcodeTypeError, BarcodeSizeError, TabPosError from .exceptions import CashDrawerError, SetVariableError, BarcodeCodeError +from .exceptions import ImageWidthError from .magicencode import MagicEncode @@ -99,6 +100,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: diff --git a/src/escpos/exceptions.py b/src/escpos/exceptions.py index 8c574ff..0662792 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. 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 From 74ef9aed7fa1a238ebe060f3cbdcc5ad134a85df Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Wed, 24 May 2017 10:23:01 +0200 Subject: [PATCH 11/37] add .mailmap in order to normalize shortlog --- .mailmap | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .mailmap diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..46c0229 --- /dev/null +++ b/.mailmap @@ -0,0 +1,10 @@ + + +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 From 024b0df7d29f15382ca526f81c92305b902297d1 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Wed, 24 May 2017 10:58:55 +0200 Subject: [PATCH 12/37] added new trove for 3.6 and 3.7 --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index c5d77f6..2fd3051 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', From a0690096962dceb61d0a986a46d900685f2e3c66 Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Wed, 24 May 2017 20:24:51 +0200 Subject: [PATCH 13/37] Lists should not be right-espaced in reST --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 1a10540..d313390 100644 --- a/README.rst +++ b/README.rst @@ -47,10 +47,10 @@ 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 Documentation and Usage ----------------------- From 3f9d44ff15223e7f1319f9c71138f75d032c3afb Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Fri, 26 May 2017 00:27:17 +0200 Subject: [PATCH 14/37] Added authors file and generate_authors.sh (#227) * Added authors file Generated using `git shortlog -s -n` and sorted by alphabetical order using vim. * Added generate_authors.sh script and ordered author list * Regenerated AUTHORS with .mailmap --- AUTHORS | 26 ++++++++++++++++++++++++++ generate_authors.sh | 3 +++ 2 files changed, 29 insertions(+) create mode 100644 AUTHORS create mode 100755 generate_authors.sh diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..dd865d0 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,26 @@ +Ahmed Tahri +Asuki Kono +belono +Christoph Heuel +Cody (Quantified Code Bot) +Curtis // mashedkeyboard +Davis Goglin +Dean Rispin +Dmytro Katyukha +Hark +Joel Lehtonen +Kristi +ldos +Manuel F Martinez +Michael Billington +Michael Elsdörfer +Nathan Bookham +Patrick Kanzler +Qian Linfeng +Renato Lorenzi +Romain Porte +Sam Cheng +Stephan Sokolow +Thijs Triemstra +Thomas van den Berg +ysuolmai diff --git a/generate_authors.sh b/generate_authors.sh new file mode 100755 index 0000000..6ab31c4 --- /dev/null +++ b/generate_authors.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +git shortlog -s -n | cut -f2 | sort From 7c17141fb279015ee83b5cd5131f614eee8b31a9 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Fri, 26 May 2017 01:55:30 +0200 Subject: [PATCH 15/37] integrate author check into travis --- .travis.yml | 3 +++ CONTRIBUTING.rst | 9 +++++++++ doc/generate_authors.sh | 14 ++++++++++++++ generate_authors.sh | 3 --- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100755 doc/generate_authors.sh delete mode 100755 generate_authors.sh 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/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/doc/generate_authors.sh b/doc/generate_authors.sh new file mode 100755 index 0000000..b93e920 --- /dev/null +++ b/doc/generate_authors.sh @@ -0,0 +1,14 @@ +#!/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 + diff -q --from-file $AUTHORSFILE $TEMPAUTHORSFILE + else + echo "$GENLIST">$AUTHORSFILE +fi + diff --git a/generate_authors.sh b/generate_authors.sh deleted file mode 100755 index 6ab31c4..0000000 --- a/generate_authors.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -git shortlog -s -n | cut -f2 | sort From 4882c31531a2c671ed32e98dff9dfbec6322db5f Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sat, 10 Jun 2017 23:35:26 +0000 Subject: [PATCH 16/37] Clarifiy and update usage.rst relevant to #230 clarifies the config-file in the usage.rst --- doc/user/usage.rst | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) 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 ------------------- From b963c5668b8279a3943a827b0f8045a2e8f851af Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Sun, 11 Jun 2017 10:06:57 +0200 Subject: [PATCH 17/37] Using viivakoodi instead of pyBarcode --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bd4a3ed..c2460da 100755 --- a/setup.py +++ b/setup.py @@ -116,7 +116,7 @@ setup( 'argparse', 'argcomplete', 'future', - 'pyBarcode==0.8b1' + 'viivakoodi>=0.8' ], setup_requires=[ 'setuptools_scm', From c3e952befacf4ee027f0b7e7237c75cdb12951e4 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 19 Jun 2017 11:13:39 +0000 Subject: [PATCH 18/37] cat authorsfiles during check --- doc/generate_authors.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/generate_authors.sh b/doc/generate_authors.sh index b93e920..bd770e7 100755 --- a/doc/generate_authors.sh +++ b/doc/generate_authors.sh @@ -7,6 +7,11 @@ 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 From efec3e508c1fa487ff35225034bb03776fc80270 Mon Sep 17 00:00:00 2001 From: TAHRI Ahmed Date: Sat, 17 Jun 2017 00:58:47 +0200 Subject: [PATCH 19/37] Fix SerialException when trying to close device on __del__ without verifing if is actually opened. --- src/escpos/printer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/escpos/printer.py b/src/escpos/printer.py index d14b93d..a15fe21 100644 --- a/src/escpos/printer.py +++ b/src/escpos/printer.py @@ -131,6 +131,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, @@ -151,7 +153,7 @@ class Serial(Escpos): 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() From 662aa30f4b588b957cd820aa967c552ba3c4f10c Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Thu, 22 Jun 2017 15:54:21 +0200 Subject: [PATCH 20/37] Update readme list of dependencies add viivakoodi and links --- README.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index d313390..9f656ba 100644 --- a/README.rst +++ b/README.rst @@ -47,10 +47,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 ----------------------- From 89dfb6cf861e7e5dfb690cb2c389ee0a22b80d6b Mon Sep 17 00:00:00 2001 From: csoft2k Date: Mon, 24 Jul 2017 13:57:02 +0200 Subject: [PATCH 21/37] Added the DLE EOT querying command. (#237) * Added the DLE EOT querying command. Added a function to check whether the printer is online or not, as well as a reading method for USB printers. * Update AUTHORS * Add entry to .mailmap * currently USB only --- .mailmap | 1 + AUTHORS | 1 + src/escpos/constants.py | 4 ++++ src/escpos/escpos.py | 22 ++++++++++++++++++++++ src/escpos/printer.py | 4 ++++ 5 files changed, 32 insertions(+) diff --git a/.mailmap b/.mailmap index 46c0229..9de2698 100644 --- a/.mailmap +++ b/.mailmap @@ -8,3 +8,4 @@ Cody (Quantified Code Bot) Cody Renato.Lorenzi Ahmed Tahri TAHRI Ahmed Michael Elsdörfer Michael Elsdörfer +csoft2k \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index dd865d0..d0ab9b0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,6 +3,7 @@ Asuki Kono belono Christoph Heuel Cody (Quantified Code Bot) +csoft2k Curtis // mashedkeyboard Davis Goglin Dean Rispin diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 4e61419..a764290 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -249,3 +249,7 @@ S_RASTER_N = _PRINT_RASTER_IMG(b'\x00') # Set raster image normal size 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 + +# Status Command +RT_STATUS_ONLINE = DLE + EOT + b'\x01'; +RT_MASK_ONLINE = 8; diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 0af1e55..a986304 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -18,6 +18,7 @@ from __future__ import unicode_literals import qrcode import textwrap import six +import time import barcode from barcode.writer import ImageWriter @@ -34,6 +35,7 @@ from .constants import CD_KICK_DEC_SEQUENCE, CD_KICK_5, CD_KICK_2, PAPER_FULL_CU 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 TXT_STYLE +from .constants import RT_STATUS_ONLINE, RT_MASK_ONLINE from .exceptions import BarcodeTypeError, BarcodeSizeError, TabPosError from .exceptions import CashDrawerError, SetVariableError, BarcodeCodeError @@ -77,6 +79,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 @@ -733,6 +741,20 @@ class Escpos(object): else: self._raw(PANEL_BUTTON_OFF) + def query_status(self): + """ Queries the printer for its status, and returns an array of integers containing it. + :rtype: array(integer)""" + self._raw(RT_STATUS_ONLINE) + time.sleep(1) + status = self._read() + return status or [RT_MASK_ONLINE] + + def is_online(self): + """ Queries the printer its online status. + When online, returns True; False otherwise. + :rtype: bool: True if online, False if offline.""" + return not (self.query_status()[0] & RT_MASK_ONLINE) + class EscposIO(object): """ESC/POS Printer IO object diff --git a/src/escpos/printer.py b/src/escpos/printer.py index a15fe21..b658efb 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: From 5bd6dcf471c7e7f00355ee3c33d15cdc672e9b6a Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Mon, 24 Jul 2017 15:04:54 +0200 Subject: [PATCH 22/37] Ensure QR codes have a border large enough (#235) * Ensure QR codes have a border large enough (The QR code spec requires a border at least 4*box_size thick but we can't just set border=16 because that results in a QR code more than 255px tall and I'm not yet ready to use fullimage() as a backend for it) This fix was originally commited by Stephan Sokolow on 2014-05-22 * Let the user print stuff using qr example * fix tests --- examples/qr_code.py | 19 +++++++++++++++++++ src/escpos/escpos.py | 3 +++ test/test_function_qr_native.py | 6 ++++-- 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 examples/qr_code.py 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/src/escpos/escpos.py b/src/escpos/escpos.py index a986304..ec4df3b 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -211,7 +211,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 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) From 9bc3b30a6097326cdac6fbcb6d5c4c9430c75847 Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Tue, 18 Jul 2017 18:50:38 +0200 Subject: [PATCH 23/37] Optional feed for cut (closes #213) --- src/escpos/escpos.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index ec4df3b..64e8e05 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -574,7 +574,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 @@ -584,9 +584,11 @@ 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') """ - self.print_and_feed(6) + if feed: + self.print_and_feed(6) mode = mode.upper() if mode not in ('FULL', 'PART'): From 9e47ff2505d83469d589b90e50490863a04d4d56 Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Sun, 23 Jul 2017 11:11:22 +0200 Subject: [PATCH 24/37] Added test for cut without feed, fixed raw code for it --- src/escpos/escpos.py | 8 ++++++-- test/test_function_cut.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 test/test_function_cut.py diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 64e8e05..ef2c026 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -587,8 +587,12 @@ class Escpos(object): :param feed: print and feed before cutting. default: true :raises ValueError: if mode not in ('FULL', 'PART') """ - if feed: - self.print_and_feed(6) + + if not feed: + self._raw(GS + b'V' + six.int2byte(66) + b'\x00') + return + + self.print_and_feed(6) mode = mode.upper() if mode not in ('FULL', 'PART'): 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) From 82c67aa646db825e98e5048aa08291d1354e7da8 Mon Sep 17 00:00:00 2001 From: csoft2k Date: Wed, 26 Jul 2017 10:01:08 +0200 Subject: [PATCH 25/37] Fix tabs behaviour (#238) The changes done in this commit should help with the open issues: #5, #27 and #161. The old implementation lacked the NUL char at the end of the command, as defined on the Epson ESC/POS Reference Guide (see https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=53 ). Also, the horizontal tab control character (CTL_HT) shouldn't be there. This implementation allows setting up to 32 tabs with a given tab width. Both values are checked to be in the valid ranges defined on the guide. Also, the TabPosError exception text has been rewritten to define the stated above. --- src/escpos/escpos.py | 18 +++++++++++------- src/escpos/exceptions.py | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index ef2c026..cb72204 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -33,7 +33,7 @@ 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 @@ -695,7 +695,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: @@ -706,7 +706,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 @@ -717,13 +718,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) diff --git a/src/escpos/exceptions.py b/src/escpos/exceptions.py index 0662792..781e3e9 100644 --- a/src/escpos/exceptions.py +++ b/src/escpos/exceptions.py @@ -150,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`. From cf0cf127fe749af0698cf35a14fd4ea4cce6fd7a Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Thu, 27 Jul 2017 16:50:41 +0200 Subject: [PATCH 26/37] add changelog for next release --- CHANGELOG.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5ffef2b..1515095 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,32 @@ Changelog ********* +2017-07-27 - 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 +- ensure QR codes have a large enough border +- make feed for cut optional +- fix the behavior of horizontal tabs + +contributors +^^^^^^^^^^^^ +- csoft2k +- Patrick Kanzler +- 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 From c7080165a7420b5cfe8cf17c5717ee7da2b7b60f Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Thu, 27 Jul 2017 22:45:51 +0200 Subject: [PATCH 27/37] Added test script for hard and soft barcodes (#243) --- examples/barcodes.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 examples/barcodes.py 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') From 1f57b04974fa016eb91148e09142c25acac8680a Mon Sep 17 00:00:00 2001 From: csoft2k Date: Thu, 27 Jul 2017 23:05:50 +0200 Subject: [PATCH 28/37] Paper sensor querying command (#242) The DLE EOT command allows querying the status of several features of the printer. Added to the online/offline status developed in #237, this commit adds a paper sensor querying. Tested with an Epson TM-T20II, which only has an end-paper sensor. The near-end paper sensor should be tested with a compatible printer. However, the implementation is quite straight-forward. --- src/escpos/constants.py | 9 +++++++-- src/escpos/escpos.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/escpos/constants.py b/src/escpos/constants.py index a764290..75bdc1b 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -251,5 +251,10 @@ 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 # Status Command -RT_STATUS_ONLINE = DLE + EOT + b'\x01'; -RT_MASK_ONLINE = 8; +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 cb72204..f13948b 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -36,6 +36,7 @@ from .constants import HW_RESET, HW_SELECT, HW_INIT 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 @@ -754,19 +755,40 @@ class Escpos(object): else: self._raw(PANEL_BUTTON_OFF) - def query_status(self): + 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(RT_STATUS_ONLINE) + self._raw(mode) time.sleep(1) status = self._read() - return status or [RT_MASK_ONLINE] + 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.""" - return not (self.query_status()[0] & RT_MASK_ONLINE) + 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): From f8a2174108c4d2030922f5b7c6d7385518ad7e08 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Thu, 27 Jul 2017 23:06:59 +0200 Subject: [PATCH 29/37] fix typo --- src/escpos/escpos.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index f13948b..6d5cf5c 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -251,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) From b494c9a4bd296450d238875cff58d624cd220124 Mon Sep 17 00:00:00 2001 From: mrwunderbar666 Date: Tue, 1 Aug 2017 17:13:45 +0800 Subject: [PATCH 30/37] Weather Forecast Example Script (#239) * Example Weather forecast script Used Adafruits example as base and adapted it for python-escpos Weather icons taken from http://adamwhitcroft.com/climacons/ * Weather Icons from Adam Whitcroft Weather Icons from http://adamwhitcroft.com/climacons/ * update authors * Minor improvements * Weather Script Debugged Added one more Icon Attributed Icons in readme.md changed folder structure * Change formatting * Fixed pathing to graphics issue * fixed image size * autopep8 to clean up the code --- AUTHORS | 1 + examples/graphics/climacons/clear-day.png | Bin 0 -> 5142 bytes examples/graphics/climacons/clear-night.png | Bin 0 -> 21442 bytes examples/graphics/climacons/cloudy.png | Bin 0 -> 22565 bytes examples/graphics/climacons/fog.png | Bin 0 -> 20413 bytes .../graphics/climacons/partly-cloudy-day.png | Bin 0 -> 6088 bytes .../climacons/partly-cloudy-night.png | Bin 0 -> 5106 bytes examples/graphics/climacons/rain.png | Bin 0 -> 4771 bytes examples/graphics/climacons/readme.md | 10 ++ examples/graphics/climacons/sleet.png | Bin 0 -> 5091 bytes examples/graphics/climacons/snow.png | Bin 0 -> 5045 bytes examples/graphics/climacons/wind.png | Bin 0 -> 3136 bytes examples/weather.py | 127 ++++++++++++++++++ 13 files changed, 138 insertions(+) create mode 100644 examples/graphics/climacons/clear-day.png create mode 100644 examples/graphics/climacons/clear-night.png create mode 100644 examples/graphics/climacons/cloudy.png create mode 100644 examples/graphics/climacons/fog.png create mode 100644 examples/graphics/climacons/partly-cloudy-day.png create mode 100644 examples/graphics/climacons/partly-cloudy-night.png create mode 100644 examples/graphics/climacons/rain.png create mode 100644 examples/graphics/climacons/readme.md create mode 100644 examples/graphics/climacons/sleet.png create mode 100644 examples/graphics/climacons/snow.png create mode 100644 examples/graphics/climacons/wind.png create mode 100644 examples/weather.py diff --git a/AUTHORS b/AUTHORS index d0ab9b0..f5f64a5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ ldos Manuel F Martinez Michael Billington Michael Elsdörfer +mrwunderbar666 Nathan Bookham Patrick Kanzler Qian Linfeng diff --git a/examples/graphics/climacons/clear-day.png b/examples/graphics/climacons/clear-day.png new file mode 100644 index 0000000000000000000000000000000000000000..b0cadc9d958f101a166c979a11504c283dfa2909 GIT binary patch literal 5142 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 literal 0 HcmV?d00001 diff --git a/examples/graphics/climacons/cloudy.png b/examples/graphics/climacons/cloudy.png new file mode 100644 index 0000000000000000000000000000000000000000..867f2329a48c599a6596cea5a1d00e670d3f51e6 GIT binary patch literal 22565 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 literal 0 HcmV?d00001 diff --git a/examples/graphics/climacons/fog.png b/examples/graphics/climacons/fog.png new file mode 100644 index 0000000000000000000000000000000000000000..c90bc6f6d43de7408f8d01045706f87dcefcbec5 GIT binary patch literal 20413 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 CM6zK3 literal 0 HcmV?d00001 diff --git a/examples/graphics/climacons/partly-cloudy-day.png b/examples/graphics/climacons/partly-cloudy-day.png new file mode 100644 index 0000000000000000000000000000000000000000..d33b56b0f642c42889578b4957da78c8a87f4cfd GIT binary patch literal 6088 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�V_aUv_G4%ceH|TUw_HSWB_TkWST>J+XLXk=?76M# zB9-->+>A&8I+BX#E5?Qj^+1!cl;2|sk#-guz_E!#!X5TB#q3rtNrPF+!nMVn5<(o{ z{nBL+18PB9_!Y}B>sxq?#yU_NjEA0+Q*c{0Hvc8Qbwm8$kY{j0pAxa`bS9}uaZy*i zv=E1Vl<6FkJDLMJ%NA;F8|WpZ!Ow(?++xOTyDcOfsmm=v@kB30i)Qt(qvAcGnEPus zW=o(^D3W`(#D})7?oaS&r*;%TJ6rg@++h?>Y1#8Bzn&YCGFBr z?LcRX(O%RR*R`lM=oYh?}#L2gzSa@SF;2a(|IcPn1cpU)e0PU)u-t+3ffD^;iRlfRpsiOE1Lh4kvT|i zsd%@C&`Gqlci6~=ef~OIvY&>T{)qMeB|#h>Dw9TW+Z2#!31*=@VN;1fZ9s{HY2vSo zKwmIEYm5h;cL=0d{K?d()Fxa*MS(KIl&*|kDQjBIgIUar>yBIwIiCm<+puBLky|f6 zHeXVh(iU(mEB;xnanU`=jD$rgSX-+2@bN_89je6KQRVF3*!M#bG`k+2-|0Qh{L83* z9>T~dC^DMp(0xw7RQY5G^^NcV2_cjcpZs0T-kt8EN~Oxl+I5l1;LvaEU^~_tUnq`d zY|TTM#9a%MkL3$xv3b*6*-YEZ<~yH>#1O`?-mOds#XsqHY}4{A=a~$f=kG^(UO+2X zEvPSGhv3xO*!(A5W1=$v9$PZOySh{J*o;X9q>+u7bUGBhTm72MHgRf}(yW&;U%LLW z-!T}EcK%1QaoR~HiaVr!$&Jx;;nqeZ|oFDq}hp(#i3pLJ7nxln_8SomoT+Q_64>x#%?dpfF zr|=^c02IKr$m%8kQKiiAMbGkJ1AQxo_gJ3xv1QFXmS%r$Zyz>O?nb%ve^{|C!7iaG!Q literal 0 HcmV?d00001 diff --git a/examples/graphics/climacons/partly-cloudy-night.png b/examples/graphics/climacons/partly-cloudy-night.png new file mode 100644 index 0000000000000000000000000000000000000000..66eaa88aa11e9ac9fb5b5de2292be9302b809063 GIT binary patch literal 5106 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1cf53159403509594f7557c236f7c2d21611a4e8 GIT binary patch literal 5091 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 literal 0 HcmV?d00001 diff --git a/examples/graphics/climacons/wind.png b/examples/graphics/climacons/wind.png new file mode 100644 index 0000000000000000000000000000000000000000..f410f7fe1a42be203216022fd179e07a34dfadde GIT binary patch literal 3136 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 literal 0 HcmV?d00001 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") From df1193ab35ae36272846b37d3b336613155f6b28 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 1 Aug 2017 11:20:00 +0200 Subject: [PATCH 31/37] implement read for Serial --- src/escpos/printer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/escpos/printer.py b/src/escpos/printer.py index b658efb..5102890 100644 --- a/src/escpos/printer.py +++ b/src/escpos/printer.py @@ -154,6 +154,10 @@ class Serial(Escpos): :type msg: bytes """ 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 """ From 81426ab6dc1136865cc33c1ac0ffde67201615a3 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 1 Aug 2017 12:27:53 +0200 Subject: [PATCH 32/37] fix whitespace --- src/escpos/printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/escpos/printer.py b/src/escpos/printer.py index 5102890..b9869f6 100644 --- a/src/escpos/printer.py +++ b/src/escpos/printer.py @@ -154,7 +154,7 @@ class Serial(Escpos): :type msg: bytes """ self.device.write(msg) - + def _read(self): """ Reads a data buffer and returns it to the caller. """ return self.device.read(16) From b64b534394ad55136b7ba223bf023c6914aaa5da Mon Sep 17 00:00:00 2001 From: Romain Porte Date: Tue, 1 Aug 2017 17:09:24 +0200 Subject: [PATCH 33/37] Add methods for simpler newlines (#246) --- src/escpos/escpos.py | 22 ++++++++++++++++++++++ test/test_function_text.py | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 6d5cf5c..8d1fd81 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -460,6 +460,28 @@ 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 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' From f3da6a97252df307e6fc5f1165c1c6654a157163 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 1 Aug 2017 17:42:34 +0200 Subject: [PATCH 34/37] remove quanitifed-code-badge --- README.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.rst b/README.rst index 9f656ba..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 From 27c843935f4a9a4d69c7498788af4e51bf6da8ec Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Fri, 4 Aug 2017 13:42:07 +0200 Subject: [PATCH 35/37] add viivakoodi to dep in tox-file --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index d51853c..a6e6fca 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ deps = nose pytest-cov pytest-mock hypothesis + viivakoodi commands = py.test --cov escpos [testenv:docs] From c259263f26ee068c56d4ff8483d59f17586911a3 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Fri, 4 Aug 2017 14:01:26 +0200 Subject: [PATCH 36/37] blacklist pytest 3.2.0 because it breaks our tests see pytest-dev/pytest#2644 for reference --- doc/requirements.txt | 1 + setup.py | 2 +- tox.ini | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) 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/setup.py b/setup.py index 60a9209..f048bea 100755 --- a/setup.py +++ b/setup.py @@ -126,7 +126,7 @@ setup( tests_require=[ 'jaconv', 'tox', - 'pytest', + 'pytest!=3.2.0', 'pytest-cov', 'pytest-mock', 'nose', diff --git a/tox.ini b/tox.ini index a6e6fca..a61a4a8 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ deps = nose coverage scripttest mock - pytest + pytest!=3.2.0 pytest-cov pytest-mock hypothesis @@ -19,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] From f8b269d8590a64f05d8ed1ac57210bc2410cb18b Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Fri, 4 Aug 2017 16:30:31 +0200 Subject: [PATCH 37/37] update changelog for next release --- CHANGELOG.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1515095..eb5f2f8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,7 @@ Changelog ********* -2017-07-27 - Version 3.0a2 - "It's My Party And I'll Sing If I Want To" +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. @@ -16,15 +16,20 @@ changes - 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 +- 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