From 59afcf778f34d52815954c603ef6740acc5dd8b2 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 3 Apr 2016 14:29:17 +1000 Subject: [PATCH 1/8] remove image handling support --- escpos/escpos.py | 204 +----------------------------------- test/255x255.png | Bin 16680 -> 0 bytes test/400x400.png | Bin 27709 -> 0 bytes test/50x50.png | Bin 653 -> 0 bytes test/test_function_image.py | 50 --------- 5 files changed, 1 insertion(+), 253 deletions(-) delete mode 100644 test/255x255.png delete mode 100644 test/400x400.png delete mode 100644 test/50x50.png delete mode 100644 test/test_function_image.py diff --git a/escpos/escpos.py b/escpos/escpos.py index 435b95e..8f30a62 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -14,14 +14,8 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -import six - -from PIL import Image - import qrcode import textwrap -import binascii -import operator from .constants import * from .exceptions import * @@ -55,98 +49,6 @@ class Escpos(object): """ pass - @staticmethod - def _check_image_size(size): - """ Check and fix the size of the image to 32 bits - - :param size: size of the image - :returns: tuple of image borders - :rtype: (int, int) - """ - if size % 32 == 0: - return 0, 0 - else: - image_border = 32 - (size % 32) - if (image_border % 2) == 0: - return image_border // 2, image_border // 2 - else: - return image_border // 2, (image_border // 2) + 1 - - def _print_image(self, line, size): - """ Print formatted image - - :param line: - :param size: - """ - i = 0 - cont = 0 - pbuffer = b'' - - self._raw(S_RASTER_N) - pbuffer = "{0:02X}{1:02X}{2:02X}{3:02X}".format(((size[0]//size[1])//8), 0, size[1] & 0xff, size[1] >> 8) - self._raw(binascii.unhexlify(pbuffer)) - pbuffer = "" - - while i < len(line): - hex_string = int(line[i:i+8], 2) - pbuffer += "{0:02X}".format(hex_string) - i += 8 - cont += 1 - if cont % 4 == 0: - self._raw(binascii.unhexlify(pbuffer)) - pbuffer = "" - cont = 0 - - def _convert_image(self, im): - """ Parse image and prepare it to a printable format - - :param im: image data - :raises: :py:exc:`~escpos.exceptions.ImageSizeError` - """ - pixels = [] - pix_line = "" - im_left = "" - im_right = "" - switch = 0 - img_size = [0, 0] - - if im.size[0] > 512: - print ("WARNING: Image is wider than 512 and could be truncated at print time ") - if im.size[1] > 0xffff: - raise ImageSizeError() - - im_border = self._check_image_size(im.size[0]) - for i in range(im_border[0]): - im_left += "0" - for i in range(im_border[1]): - im_right += "0" - - for y in range(im.size[1]): - img_size[1] += 1 - pix_line += im_left - img_size[0] += im_border[0] - for x in range(im.size[0]): - img_size[0] += 1 - RGB = im.getpixel((x, y)) - im_color = (RGB[0] + RGB[1] + RGB[2]) - im_pattern = "1X0" - pattern_len = len(im_pattern) - switch = (switch - 1) * (-1) - for x in range(pattern_len): - if im_color <= (255 * 3 / pattern_len * (x+1)): - if im_pattern[x] == "X": - pix_line += "{0:d}".format(switch) - else: - pix_line += im_pattern[x] - break - elif (255 * 3 / pattern_len * pattern_len) < im_color <= (255 * 3): - pix_line += im_pattern[-1] - break - pix_line += im_right - img_size[0] += im_border[1] - - self._print_image(pix_line, img_size) - def image(self, path_img): """ Open and print an image file @@ -157,111 +59,7 @@ class Escpos(object): :param path_img: complete filename and path to image of type `jpg`, `gif`, `png` or `bmp` """ - if not isinstance(path_img, Image.Image): - im_open = Image.open(path_img) - else: - im_open = path_img - - # Remove the alpha channel on transparent images - if im_open.mode == 'RGBA': - im_open.load() - im = Image.new("RGB", im_open.size, (255, 255, 255)) - im.paste(im_open, mask=im_open.split()[3]) - else: - im = im_open.convert("RGB") - - # Convert the RGB image in printable image - self._convert_image(im) - - def fullimage(self, img, max_height=860, width=512, histeq=True, bandsize=255): - """ Resizes and prints an arbitrarily sized image - - .. warning:: The image-printing-API is currently under development. Please do not consider this method part - of the API. It might be subject to change without further notice. - - .. todo:: Seems to be broken. Write test that simply executes function with a dummy printer in order to - check for bugs like these in the future. - """ - print("WARNING: The image-printing-API is currently under development. Please do not consider this " - "function part of the API yet.") - if isinstance(img, Image.Image): - im = img.convert("RGB") - else: - im = Image.open(img).convert("RGB") - - if histeq: - # Histogram equaliztion - h = im.histogram() - lut = [] - for b in range(0, len(h), 256): - # step size - step = reduce(operator.add, h[b:b+256]) / 255 - # create equalization lookup table - n = 0 - for i in range(256): - lut.append(n / step) - n = n + h[i+b] - im = im.point(lut) - - if width: - ratio = float(width) / im.size[0] - newheight = int(ratio * im.size[1]) - - # Resize the image - im = im.resize((width, newheight), Image.ANTIALIAS) - - if max_height and im.size[1] > max_height: - im = im.crop((0, 0, im.size[0], max_height)) - - # Divide into bands - current = 0 - while current < im.size[1]: - self.image(im.crop((0, current, width or im.size[0], - min(im.size[1], current + bandsize)))) - current += bandsize - - def direct_image(self, image): - """ Direct printing function for pictures - - .. warning:: The image-printing-API is currently under development. Please do not consider this method part - of the API. It might be subject to change without further notice. - - This function is rather fragile and will fail when the Image object is not suited. - - :param image: PIL image object, containing a 1-bit picture - """ - print("WARNING: The image-printing-API is currently under development. Please do not consider this " - "function part of the API yet.") - mask = 0x80 - i = 0 - temp = 0 - - (width, height) = image.size - self._raw(S_RASTER_N) - header_x = int(width / 8) - header_y = height - buf = "{0:02X}".format((header_x & 0xff)) - buf += "{0:02X}".format(((header_x >> 8) & 0xff)) - buf += "{0:02X}".format((header_y & 0xff)) - buf += "{0:02X}".format(((header_y >> 8) & 0xff)) - #self._raw(binascii.unhexlify(buf)) - for y in range(height): - for x in range(width): - value = image.getpixel((x, y)) - value |= (value << 8) - if value == 0: - temp |= mask - - mask >>= 1 - - i += 1 - if i == 8: - buf += ("{0:02X}".format(temp)) - mask = 0x80 - i = 0 - temp = 0 - self._raw(binascii.unhexlify(bytes(buf, "ascii"))) - self._raw(b'\n') + pass def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False): """ Print QR Code for the provided string diff --git a/test/255x255.png b/test/255x255.png deleted file mode 100644 index a99fb5f98869c4d5157882e23c189112ef56f7d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16680 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"?18B7%Y8+B||O_`2HL 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 ABLDyZ diff --git a/test/400x400.png b/test/400x400.png deleted file mode 100644 index cab3db6d5df20dbf6e0dc1d2c8d47c97529def44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27709 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�!zIrT#wTZD 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 IM6N<$f>F5c$p8QV diff --git a/test/50x50.png b/test/50x50.png deleted file mode 100644 index 67b483b6f7ad452c00af7e85ea4092b2a6a1e5af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 653 zcmV;80&@L{P)eSad^g zZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00H?)L_t(o!|j#NYZFlvg}*m8q*_so zixyN+i3@cjB8VbXH-d{IUAeO$QgB=U0zq&mBCZ50>K~wPbRoD3rF5$z2nDwyMAH~T zw5Z#{`B0 zJhEQzm_3n@21i(k-X;lv7H4?FRjNGZB5me-zD`R;$tS5Vs_06hwkY&KlGM>DZCAbL ztiNMmpRABIcK=Y>;2~Q%4?u|p8Z2ja2<*e#BxDyGG~?5C-m#yF=vE!$z5u^W}FV?_`C!(Q^zMB0XgZu&<(9= zv!>(P2cZj@UpX%0IbeknJNZNy)#||BA~${=s8HrGtF)rpTi|$JlXlNN-veebT&BfS z0KRgQGACR&KB?=x;57T_@|>6SWC^|R)+ zMQD$1E3|Yl(*N^U|2LWB$I<}Ma>J5`e6ZxIzz0iSnYd%g!^8(m&cXX($uao+SdtsG z{IKM>@qSnuz$8DG1_-_YEZKw4kEIc`MkfEXiTREY4W)U)q-Z1av+FWLvFOXh48bBL ncvHB=?;BxL@b4IHyraGWRE4iMl&_Lb00000NkvXXu0mjfr~w+` diff --git a/test/test_function_image.py b/test/test_function_image.py deleted file mode 100644 index 89fcade..0000000 --- a/test/test_function_image.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/python -"""tests for the image printing function - -: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 - -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_image_with_50x50_png(): - """test the image function with 50x50.png (grayscale png)""" - instance = printer.File(devfile=devfile) - instance.image("test/50x50.png") - -@with_setup(setup_testfile, teardown_testfile) -def test_function_image_with_255x255_png(): - """test the image function with 255x255.png (grayscale png)""" - instance = printer.File(devfile=devfile) - instance.image("test/255x255.png") - -@with_setup(setup_testfile, teardown_testfile) -def test_function_image_with_400x400_png(): - """test the image function with 400x400.png (grayscale png)""" - instance = printer.File(devfile=devfile) - instance.image("test/400x400.png") From b45afbb29772d7eb040ce74df5d1887c652a06a1 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 3 Apr 2016 17:36:54 +1000 Subject: [PATCH 2/8] add implementation of GS v 0, GS ( L and GS *. - ported test cases for EscposImage class, copied over 1px and 2px test images from escpos-php - added test cases over image print function - updated QR tests to also include image output check - updated CLI to match new image function options --- escpos/cli.py | 24 +++++- escpos/escpos.py | 58 ++++++++++--- escpos/image.py | 85 +++++++++++++++++++ escpos/printer.py | 35 +++++++- test/resources/black_transparent.gif | Bin 0 -> 65 bytes test/resources/black_transparent.png | Bin 0 -> 167 bytes test/resources/black_white.gif | Bin 0 -> 65 bytes test/resources/black_white.jpg | Bin 0 -> 175 bytes test/resources/black_white.png | Bin 0 -> 156 bytes test/resources/canvas_black.gif | Bin 0 -> 72 bytes test/resources/canvas_black.jpg | Bin 0 -> 160 bytes test/resources/canvas_black.png | Bin 0 -> 239 bytes test/resources/canvas_white.gif | Bin 0 -> 72 bytes test/resources/canvas_white.jpg | Bin 0 -> 160 bytes test/resources/canvas_white.png | Bin 0 -> 239 bytes test/test_function_image.py | 115 ++++++++++++++++++++++++++ test/test_function_qr_native.py | 118 ++++++++++++++------------- test/test_image.py | 52 ++++++++++++ 18 files changed, 416 insertions(+), 71 deletions(-) create mode 100644 escpos/image.py create mode 100644 test/resources/black_transparent.gif create mode 100644 test/resources/black_transparent.png create mode 100644 test/resources/black_white.gif create mode 100644 test/resources/black_white.jpg create mode 100644 test/resources/black_white.png create mode 100644 test/resources/canvas_black.gif create mode 100644 test/resources/canvas_black.jpg create mode 100644 test/resources/canvas_black.png create mode 100644 test/resources/canvas_white.gif create mode 100644 test/resources/canvas_white.jpg create mode 100644 test/resources/canvas_white.png create mode 100644 test/test_function_image.py create mode 100644 test/test_image.py diff --git a/escpos/cli.py b/escpos/cli.py index d250852..8a18676 100644 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -37,8 +37,8 @@ DEMO_FUNCTIONS = { {'txt': 'Hello, World!\n',} ], 'qr': [ - {'text': 'This tests a QR code'}, - {'text': 'https://en.wikipedia.org/'} + {'content': 'This tests a QR code'}, + {'content': 'https://en.wikipedia.org/'} ], 'barcodes_a': [ {'bc': 'UPC-A', 'code': '13243546576'}, @@ -87,7 +87,7 @@ ESCPOS_COMMANDS = [ }, 'arguments': [ { - 'option_strings': ('--text',), + 'option_strings': ('--content',), 'help': 'Text to print as a qr code', 'required': True, } @@ -223,10 +223,26 @@ ESCPOS_COMMANDS = [ }, 'arguments': [ { - 'option_strings': ('--path_img',), + 'option_strings': ('--img_source',), 'help': 'Path to image', 'required': True, }, + { + 'option_strings': ('--impl',), + 'help': 'Implementation to use', + 'choices': ['bitImageRaster', 'bitImageColumn', 'graphics'], + }, + { + 'option_strings': ('--high_density_horizontal',), + 'help': 'Image density (horizontal)', + 'type': str_to_bool, + }, + { + 'option_strings': ('--high_density_vertical',), + 'help': 'Image density (vertical)', + 'type': str_to_bool, + }, + ], }, { diff --git a/escpos/escpos.py b/escpos/escpos.py index 8f30a62..6ff75bf 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -21,7 +21,7 @@ from .constants import * from .exceptions import * from abc import ABCMeta, abstractmethod # abstract base class support - +from escpos.image import EscposImage class Escpos(object): """ ESC/POS Printer object @@ -49,17 +49,53 @@ class Escpos(object): """ pass - def image(self, path_img): - """ Open and print an image file + def image(self, img_source, high_density_vertical = True, high_density_horizontal = True, impl = "graphics"): + """ Print an image - Prints an image. The image is automatically adjusted in size in order to print it. + :param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp` + + """ + im = EscposImage(img_source) + + if impl == "bitImageRaster": + # GS v 0, raster format bit image + density_byte = (0 if high_density_vertical else 1) + (0 if high_density_horizontal else 2) + header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) + self._int_low_high(im.height, 2); + self._raw(header + im.to_raster_format()) + + if impl == "graphics": + # GS ( L raster format graphics + img_header = self._int_low_high(im.width, 2) + self._int_low_high(im.height, 2); + tone = b'0'; + colors = b'1'; + ym = six.int2byte(1 if high_density_vertical else 2) + xm = six.int2byte(1 if high_density_horizontal else 2) + header = tone + xm + ym + colors + img_header + raster_data = im.to_raster_format() + self._image_send_graphics_data(b'0', b'p', header + raster_data); + self._image_send_graphics_data(b'0', b'2', b''); + + if impl == "bitImageColumn": + # ESC *, column format bit image + density_byte = (1 if high_density_horizontal else 0) + (32 if high_density_vertical else 0); + header = ESC + b"*" + six.int2byte(density_byte) + self._int_low_high( im.width, 2 ); + outp = [] + outp.append(ESC + b"3" + six.int2byte(16)) # Adjust line-feed size + for blob in im.to_column_format(high_density_vertical): + outp.append(header + blob + b"\n") + outp.append(ESC + b"2"); # Reset line-feed size + self._raw(b''.join(outp)) - .. todo:: Seems to be broken. Write test that simply executes function with a dummy printer in order to - check for bugs like these in the future. - - :param path_img: complete filename and path to image of type `jpg`, `gif`, `png` or `bmp` + def _image_send_graphics_data(self, m, fn, data): """ - pass + Wrapper for GS ( L, to calculate and send correct data length. + + :param m: Modifier//variant for function. Usually '0' + :param fn: Function number to use, as byte + :param data: Data to send + """ + header = self._int_low_high(len(data) + 2, 2); + self._raw(GS + b'(L' + header + m + fn + data) def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False): """ Print QR Code for the provided string @@ -84,7 +120,7 @@ class Escpos(object): if not native: # Map ESC/POS error correction levels to python 'qrcode' library constant and render to an image if model != QR_MODEL_2: - raise ValueError("Invalid QR mocel for qrlib rendering (must be QR_MODEL_2)") + raise ValueError("Invalid QR model for qrlib rendering (must be QR_MODEL_2)") python_qr_ec = { QR_ECLEVEL_H: qrcode.constants.ERROR_CORRECT_H, QR_ECLEVEL_L: qrcode.constants.ERROR_CORRECT_L, @@ -97,7 +133,7 @@ class Escpos(object): qr_img = qr_code.make_image() im = qr_img._img.convert("RGB") # Convert the RGB image in printable image - self._convert_image(im) + self.image(im) return # Native 2D code printing cn = b'1' # Code type for QR code diff --git a/escpos/image.py b/escpos/image.py new file mode 100644 index 0000000..af150ba --- /dev/null +++ b/escpos/image.py @@ -0,0 +1,85 @@ +""" Image format handling class + +This module contains the image format handler :py:class:`EscposImage`. + +The class is designed to efficiently delegate image processing to +PIL, rather than spend CPU cycles looping over pixels. + +:author: `Michael Billington `_ +:organization: `python-escpos `_ +:copyright: Copyright (c) 2016 Michael Billington +:license: GNU GPL v3 +""" + +from PIL import Image, ImageOps + +class EscposImage(object): + def __init__(self, img_source): + """ + Load in an image + + :param img_source: PIL.Image, or filename to load one from. + """ + if isinstance(img_source, Image.Image): + img_original = img_source + else: + img_original = Image.open(img_source) + + # Convert to white RGB background, paste over white background + # to strip alpha. + img_original = img_original.convert('RGBA') + im = Image.new("RGB", img_original.size, (255, 255, 255)) + im.paste(img_original, mask=img_original.split()[3]) + # Convert down to greyscale + im = im.convert("L") + # Invert: Only works on 'L' images + im = ImageOps.invert(im) + # Pure black and white + self._im = im.convert("1") + + @property + def width(self): + """ + Width of image in pixels + """ + width_pixels, _ = self._im.size + return width_pixels + + @property + def width_bytes(self): + """ + Width of image if you use 8 pixels per byte and 0-pad at the end. + """ + return (self.width + 7) >> 3 + + @property + def height(self): + """ + Height of image in pixels + """ + _, height_pixels = self._im.size + return height_pixels + + def to_column_format(self, high_density_vertical = True): + """ + Extract slices of an image as equal-sized blobs of column-format data. + + :param high_density_vertical: Printed line height in dots + """ + im = self._im.transpose(Image.ROTATE_270).transpose(Image.FLIP_LEFT_RIGHT) + line_height = 24 if high_density_vertical else 8 + width_pixels, height_pixels = im.size + top = 0 + left = 0 + while left < width_pixels: + box = (left, top, left + line_height, top + height_pixels) + im_slice = im.transform((line_height, height_pixels), Image.EXTENT, box) + im_bytes = im_slice.tobytes() + yield(im_bytes) + left += line_height + + def to_raster_format(self): + """ + Convert image to raster-format binary + """ + return self._im.tobytes() diff --git a/escpos/printer.py b/escpos/printer.py index ddd6ae5..086f8ff 100644 --- a/escpos/printer.py +++ b/escpos/printer.py @@ -20,7 +20,6 @@ import socket from .escpos import Escpos from .exceptions import * - class Usb(Escpos): """ USB printer @@ -260,3 +259,37 @@ class File(Escpos): """ Close system file """ self.device.flush() self.device.close() + +class Dummy(Escpos): + """ Dummy printer + + This class is used for saving commands to a variable, for use in situations where + there is no need to send commands to an actual printer. This includes + generating print jobs for later use, or testing output. + + inheritance: + + .. inheritance-diagram:: escpos.printer.Dummy + :parts: 1 + + """ + + def __init__(self, *args, **kwargs): + """ + + :param devfile : Device file under dev filesystem + """ + Escpos.__init__(self, *args, **kwargs) + self._output_list = [] + + def _raw(self, msg): + """ Print any command sent in raw format + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + self._output_list.append(msg) + + @property + def output(self): + return b''.join(self._output_list) diff --git a/test/resources/black_transparent.gif b/test/resources/black_transparent.gif new file mode 100644 index 0000000000000000000000000000000000000000..6c54bad9eea0d1cd39e55504d181b6607b6028d4 GIT binary patch literal 65 zcmZ?wbhEHbWMW`q_`m=Kia%Kx85kHD6#of27o{eaq^2m8XO?6rxO@5rFzA33fs`{a LF|!D<>&kwgnNLhfd<&=O9H5Y7iEBiObAE1aYF-J0b5UwyNotBh zd1gt5g1e`0KzJjcI8YJ4r;B3<$MxitgoGdG8+aJ_#26XR&Dh)tlw$C7^>bP0l+XkK D$`C0q literal 0 HcmV?d00001 diff --git a/test/resources/black_white.gif b/test/resources/black_white.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a044a671f59c3c4a5f859eb6a09a71d809be683 GIT binary patch literal 65 zcmZ?wbhEHbWMW`q_`t{j1poj4SNzEWVlgQG6Lc<0O)N=GQ7F$W$xv|j^bKIp0m*=r NGcYl;2(dC)0{}064pjgE literal 0 HcmV?d00001 diff --git a/test/resources/black_white.jpg b/test/resources/black_white.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6539cece59e6c5ce41bf50f1ced121096ead5aed GIT binary patch literal 175 zcmex=>ukC3pCfH06P@c#e52OWR?s5zX<@pgBjQW literal 0 HcmV?d00001 diff --git a/test/resources/black_white.png b/test/resources/black_white.png new file mode 100644 index 0000000000000000000000000000000000000000..33ba331fa1e8642247b0cd1e48e03e7bb24a624b GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^Od!kwBL7~QRScv!3p^r$G`BDaGcwGYBLNg-FY)ws zWxvnN$08&+x9_71P)M@GHKN2hKQ}iuuY|$5C^fMpHASI3vm`^o-P1Q9ypc~Fs7TDy o#W95AdU65;{Q3Xio|&1MfuD`(|Am>%Vn7uPp00i_>zopr06?22>i_@% literal 0 HcmV?d00001 diff --git a/test/resources/canvas_black.gif b/test/resources/canvas_black.gif new file mode 100644 index 0000000000000000000000000000000000000000..49b19dbc179f5995e4ff5338e7039d3a47b4a888 GIT binary patch literal 72 zcmZ?wbhEHbWMp7u_`t{j1poj4SNzEWVlgQG=l0A^Oi%SqOwUZt=1ot`%}um5&@(YL UF*Rk-0jU6KV_;%(VPvod0K2{rbpQYW literal 0 HcmV?d00001 diff --git a/test/resources/canvas_black.jpg b/test/resources/canvas_black.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d059f431b267596da878937214280915c89f51ee GIT binary patch literal 160 zcmZ9GK@P$|3!+y-7X048hz>mPHCTnRfR>gOhxmUMTR+ao=IO?3^7bqx(d49u-eEv*bK owG9oe3=Ayq=dVT4kei>9nO2EggGbYKL7)Z(Pgg&ebxsLQ05fwqK>z>% literal 0 HcmV?d00001 diff --git a/test/resources/canvas_white.gif b/test/resources/canvas_white.gif new file mode 100644 index 0000000000000000000000000000000000000000..7881ce629acf8d6918eb5ac07169bc67b99a904b GIT binary patch literal 72 zcmZ?wbhEHbWMp7u_`t{j1poj4SNzEWVlgQG=l0A^Oi%SqOwUZt=1ot`%}um5&@(YL UF*Rk-0jU6KV_;(PVPvod0K480eEnh z;e>7%gm;ZT^#P}}Pr|CgB3h=RdCVe19mdJ5nwEK)ZE4y3hRm@?RN;3VV)+M&*8W@g M3*4UIi0j**UsypDH2?qr literal 0 HcmV?d00001 diff --git a/test/resources/canvas_white.png b/test/resources/canvas_white.png new file mode 100644 index 0000000000000000000000000000000000000000..4231a4bb5400c5554d146b0f8b32b717c109c1bf GIT binary patch literal 239 zcmeAS@N?(olHy`uVBq!ia0vp^j35jm7|ip2ssJgLbVpxD28NCO++aH!(Ecb`_ +:organization: `python-escpos `_ +:copyright: Copyright (c) 2016 `Michael Billington `_ +:license: GNU GPL v3 +""" + +import escpos.printer as printer +from PIL import Image + +# Raster format print +def test_bit_image_black(): + """ + Test printing solid black bit image (raster) + """ + instance = printer.Dummy() + instance.image('test/resources/canvas_black.png', impl="bitImageRaster") + assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\x80') + # Same thing w/ object created on the fly, rather than a filename + instance = printer.Dummy() + im = Image.new("RGB", (1, 1), (0, 0, 0)) + instance.image(im, impl="bitImageRaster") + assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\x80') + +def test_bit_image_white(): + """ + Test printing solid white bit image (raster) + """ + instance = printer.Dummy() + instance.image('test/resources/canvas_white.png', impl="bitImageRaster") + assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\x00') + +def test_bit_image_both(): + """ + Test printing black/white bit image (raster) + """ + instance = printer.Dummy() + instance.image('test/resources/black_white.png', impl="bitImageRaster") + assert(instance.output == b'\x1dv0\x00\x01\x00\x02\x00\xc0\x00') + +def test_bit_image_transparent(): + """ + Test printing black/transparent bit image (raster) + """ + instance = printer.Dummy() + instance.image('test/resources/black_transparent.png', impl="bitImageRaster") + assert(instance.output == b'\x1dv0\x00\x01\x00\x02\x00\xc0\x00') + +# Column format print +def test_bit_image_colfmt_black(): + """ + Test printing solid black bit image (column format) + """ + instance = printer.Dummy() + instance.image('test/resources/canvas_black.png', impl="bitImageColumn") + assert(instance.output == b'\x1b3\x10\x1b*!\x01\x00\x80\x00\x00\x0a\x1b2') + +def test_bit_image_colfmt_white(): + """ + Test printing solid white bit image (column format) + """ + instance = printer.Dummy() + instance.image('test/resources/canvas_white.png', impl="bitImageColumn") + assert(instance.output == b'\x1b3\x10\x1b*!\x01\x00\x00\x00\x00\x0a\x1b2') + +def test_bit_image_colfmt_both(): + """ + Test printing black/white bit image (column format) + """ + instance = printer.Dummy() + instance.image('test/resources/black_white.png', impl="bitImageColumn") + assert(instance.output == b'\x1b3\x10\x1b*!\x02\x00\x80\x00\x00\x80\x00\x00\x0a\x1b2') + +def test_bit_image_colfmt_transparent(): + """ + Test printing black/transparent bit image (column format) + """ + instance = printer.Dummy() + instance.image('test/resources/black_transparent.png', impl="bitImageColumn") + assert(instance.output == b'\x1b3\x10\x1b*!\x02\x00\x80\x00\x00\x80\x00\x00\x0a\x1b2') + +# Graphics print +def test_graphics_black(): + """ + Test printing solid black graphics + """ + instance = printer.Dummy() + instance.image('test/resources/canvas_black.png', impl="graphics") + assert(instance.output == b'\x1d(L\x0b\x000p0\x01\x011\x01\x00\x01\x00\x80\x1d(L\x02\x0002') + +def test_graphics_white(): + """ + Test printing solid white graphics + """ + instance = printer.Dummy() + instance.image('test/resources/canvas_white.png', impl="graphics") + assert(instance.output == b'\x1d(L\x0b\x000p0\x01\x011\x01\x00\x01\x00\x00\x1d(L\x02\x0002') + +def test_graphics_both(): + """ + Test printing black/white graphics + """ + instance = printer.Dummy() + instance.image('test/resources/black_white.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_graphics_transparent(): + """ + Test printing black/transparent graphics + """ + 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') diff --git a/test/test_function_qr_native.py b/test/test_function_qr_native.py index 9a07dc5..8636e5e 100644 --- a/test/test_function_qr_native.py +++ b/test/test_function_qr_native.py @@ -12,70 +12,78 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -from nose.tools import with_setup - +from nose.tools import raises import escpos.printer as printer -import os from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1 -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_qr_defaults(): - """test QR code with defaults""" - instance = printer.File(devfile=devfile) +def test_defaults(): + """Test QR code with defaults""" + instance = printer.Dummy() instance.qr("1234", native=True) - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d(k\x07\x001P01234\x1d(k\x03\x001Q0') + expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d' \ + b'(k\x07\x001P01234\x1d(k\x03\x001Q0' + assert(instance.output == expected) -@with_setup(setup_testfile, teardown_testfile) -def test_function_qr_empty(): - """test QR printing blank code""" - instance = printer.File(devfile=devfile) +def test_empty(): + """Test QR printing blank code""" + instance = printer.Dummy() instance.qr("", native=True) - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'') + assert(instance.output == b'') -@with_setup(setup_testfile, teardown_testfile) -def test_function_qr_ec(): - """test QR error correction setting""" - instance = printer.File(devfile=devfile) +def test_ec(): + """Test QR error correction setting""" + instance = printer.Dummy() instance.qr("1234", native=True, ec=QR_ECLEVEL_H) - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E3\x1d(k\x07\x001P01234\x1d(k\x03\x001Q0') + expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E3\x1d' \ + b'(k\x07\x001P01234\x1d(k\x03\x001Q0' + assert(instance.output == expected) -@with_setup(setup_testfile, teardown_testfile) -def test_function_qr_size(): - """test QR box size""" - instance = printer.File(devfile=devfile) +def test_size(): + """Test QR box size""" + instance = printer.Dummy() instance.qr("1234", native=True, size=7) - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x07\x1d(k\x03\x001E0\x1d(k\x07\x001P01234\x1d(k\x03\x001Q0') + expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x07\x1d(k\x03\x001E0\x1d' \ + b'(k\x07\x001P01234\x1d(k\x03\x001Q0' + assert(instance.output == expected) -@with_setup(setup_testfile, teardown_testfile) -def test_function_qr_model(): - """test QR model""" - instance = printer.File(devfile=devfile) +def test_model(): + """Test QR model""" + instance = printer.Dummy() instance.qr("1234", native=True, model=QR_MODEL_1) - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'\x1d(k\x04\x001A1\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d(k\x07\x001P01234\x1d(k\x03\x001Q0') + expected = b'\x1d(k\x04\x001A1\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d' \ + b'(k\x07\x001P01234\x1d(k\x03\x001Q0' + assert(instance.output == expected) + +@raises(ValueError) +def test_invalid_ec(): + """Test invalid QR error correction""" + instance = printer.Dummy() + instance.qr("1234", native=True, ec=-1) + +@raises(ValueError) +def test_invalid_size(): + """Test invalid QR size""" + instance = printer.Dummy() + instance.qr("1234", native=True, size=0) + +@raises(ValueError) +def test_invalid_model(): + """Test invalid QR model""" + instance = printer.Dummy() + instance.qr("1234", native=True, model="Hello") + +def test_image(): + """Test QR as image""" + instance = printer.Dummy() + instance.qr("1", native=False, size=1) + expected = b'\x1d(LO\x000p0\x01\x011\x17\x00\x17\x00\x00\x00\x00\x7f]\xfcA' \ + b'\x19\x04]it]et]ItA=\x04\x7fU\xfc\x00\x0c\x00y~t4\x7f =\xa84j\xd9\xf0' \ + b'\x05\xd4\x90\x00i(\x7f<\xa8A \xd8]\'\xc4]y\xf8]E\x80Ar\x94\x7fR@\x00\x00' \ + b'\x00\x1d(L\x02\x0002' + assert(instance.output == expected) + +@raises(ValueError) +def test_image_invalid_model(): + """Test unsupported QR model as image""" + instance = printer.Dummy() + instance.qr("1234", native=False, model=QR_MODEL_1) diff --git a/test/test_image.py b/test/test_image.py new file mode 100644 index 0000000..6612703 --- /dev/null +++ b/test/test_image.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +""" Image tests- Check that images from different source formats are correctly +converted to ESC/POS column & raster formats. + +:author: `Michael Billington `_ +:organization: `python-escpos `_ +:copyright: Copyright (c) 2016 `Michael Billington `_ +:license: GNU GPL v3 +""" + +from escpos.image import EscposImage + +def test_image_black(): + """ + Test rendering solid black image + """ + for img_format in ['png', 'jpg', 'gif']: + _load_and_check_img('canvas_black.' + img_format, 1, 1, b'\x80', [b'\x80']) + +def test_image_black_transparent(): + """ + Test rendering black/transparent image + """ + for img_format in ['png', 'gif']: + _load_and_check_img('black_transparent.' + img_format, 2, 2, b'\xc0\x00', [b'\x80\x80']) + +def test_image_black_white(): + """ + Test rendering black/white image + """ + for img_format in ['png', 'jpg', 'gif']: + _load_and_check_img('black_white.' + img_format, 2, 2, b'\xc0\x00', [b'\x80\x80']) + +def test_image_white(): + """ + Test rendering solid white image + """ + for img_format in ['png', 'jpg', 'gif']: + _load_and_check_img('canvas_white.' + img_format, 1, 1, b'\x00', [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. + """ + im = EscposImage('test/resources/' + filename) + assert(im.width == width_expected) + assert(im.height == height_expected) + assert(im.to_raster_format() == raster_format_expected) + i = 0 + for row in im.to_column_format(False): + assert(row == column_format_expected[i]) + i = i + 1 From ba03538c5046b7b914eff639e3fcc0bda8578f8d Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 3 Apr 2016 22:15:43 +1000 Subject: [PATCH 3/8] remove pypy3 from allowed failures --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 316cd67..3c2f563 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,6 @@ matrix: - python: pypy3 env: TOXENV=pypy3 allow_failures: - - python: pypy3 - python: 3.5-dev - python: nightly before_install: From 6b445b3fb1f5ec375890bf9037a3a3593218f33b Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Wed, 6 Apr 2016 21:05:32 +1000 Subject: [PATCH 4/8] update docstrings per QuantifiedCode suggestions --- escpos/image.py | 10 +++++++--- escpos/printer.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/escpos/image.py b/escpos/image.py index af150ba..8dd46d4 100644 --- a/escpos/image.py +++ b/escpos/image.py @@ -2,9 +2,6 @@ This module contains the image format handler :py:class:`EscposImage`. -The class is designed to efficiently delegate image processing to -PIL, rather than spend CPU cycles looping over pixels. - :author: `Michael Billington `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 Michael Billington @@ -14,6 +11,13 @@ PIL, rather than spend CPU cycles looping over pixels. from PIL import Image, ImageOps class EscposImage(object): + """ + Load images in, and output ESC/POS formats. + + The class is designed to efficiently delegate image processing to + PIL, rather than spend CPU cycles looping over pixels. + """ + def __init__(self, img_source): """ Load in an image diff --git a/escpos/printer.py b/escpos/printer.py index 086f8ff..ee9e227 100644 --- a/escpos/printer.py +++ b/escpos/printer.py @@ -292,4 +292,5 @@ class Dummy(Escpos): @property def output(self): + """ Get the data that was sent to this printer """ return b''.join(self._output_list) From 4584e3138a6961956110a062496ca0f67f7b537d Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Wed, 6 Apr 2016 21:39:46 +1000 Subject: [PATCH 5/8] switch default image format to bitImageRaster Printers which don't have native QR rendering are less likely to support the newer GS ( L graphics command. --- escpos/escpos.py | 2 +- test/test_function_qr_native.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/escpos/escpos.py b/escpos/escpos.py index 6ff75bf..088cfe2 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -49,7 +49,7 @@ class Escpos(object): """ pass - def image(self, img_source, high_density_vertical = True, high_density_horizontal = True, impl = "graphics"): + def image(self, img_source, high_density_vertical = True, high_density_horizontal = True, impl = "bitImageRaster"): """ Print an image :param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp` diff --git a/test/test_function_qr_native.py b/test/test_function_qr_native.py index 8636e5e..7f24e42 100644 --- a/test/test_function_qr_native.py +++ b/test/test_function_qr_native.py @@ -76,10 +76,10 @@ def test_image(): """Test QR as image""" instance = printer.Dummy() instance.qr("1", native=False, size=1) - expected = b'\x1d(LO\x000p0\x01\x011\x17\x00\x17\x00\x00\x00\x00\x7f]\xfcA' \ - b'\x19\x04]it]et]ItA=\x04\x7fU\xfc\x00\x0c\x00y~t4\x7f =\xa84j\xd9\xf0' \ - b'\x05\xd4\x90\x00i(\x7f<\xa8A \xd8]\'\xc4]y\xf8]E\x80Ar\x94\x7fR@\x00\x00' \ - b'\x00\x1d(L\x02\x0002' + print(instance.output) + expected = 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' assert(instance.output == expected) @raises(ValueError) From 44c79eaf1126de96a9439b80b0d9c0f5b96f4dbe Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Thu, 7 Apr 2016 22:06:14 +1000 Subject: [PATCH 6/8] Remove trailing semicolons sed -i 's/;$//' escpos/*.py --- escpos/constants.py | 14 +++++++------- escpos/escpos.py | 34 +++++++++++++++++----------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/escpos/constants.py b/escpos/constants.py index 1a36f9e..e268edb 100644 --- a/escpos/constants.py +++ b/escpos/constants.py @@ -184,15 +184,15 @@ BARCODE_TYPES = { } ## QRCode error correction levels -QR_ECLEVEL_L = 0; -QR_ECLEVEL_M = 1; -QR_ECLEVEL_Q = 2; -QR_ECLEVEL_H = 3; +QR_ECLEVEL_L = 0 +QR_ECLEVEL_M = 1 +QR_ECLEVEL_Q = 2 +QR_ECLEVEL_H = 3 ## QRcode models -QR_MODEL_1 = 1; -QR_MODEL_2 = 2; -QR_MICRO = 3; +QR_MODEL_1 = 1 +QR_MODEL_2 = 2 +QR_MICRO = 3 # Image format # NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image" diff --git a/escpos/escpos.py b/escpos/escpos.py index 088cfe2..6bcfa40 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -60,25 +60,25 @@ class Escpos(object): if impl == "bitImageRaster": # GS v 0, raster format bit image density_byte = (0 if high_density_vertical else 1) + (0 if high_density_horizontal else 2) - header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) + self._int_low_high(im.height, 2); + header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) + self._int_low_high(im.height, 2) self._raw(header + im.to_raster_format()) if impl == "graphics": # GS ( L raster format graphics - img_header = self._int_low_high(im.width, 2) + self._int_low_high(im.height, 2); - tone = b'0'; - colors = b'1'; + img_header = self._int_low_high(im.width, 2) + self._int_low_high(im.height, 2) + tone = b'0' + colors = b'1' ym = six.int2byte(1 if high_density_vertical else 2) xm = six.int2byte(1 if high_density_horizontal else 2) header = tone + xm + ym + colors + img_header raster_data = im.to_raster_format() - self._image_send_graphics_data(b'0', b'p', header + raster_data); - self._image_send_graphics_data(b'0', b'2', b''); + self._image_send_graphics_data(b'0', b'p', header + raster_data) + self._image_send_graphics_data(b'0', b'2', b'') if impl == "bitImageColumn": # ESC *, column format bit image - density_byte = (1 if high_density_horizontal else 0) + (32 if high_density_vertical else 0); - header = ESC + b"*" + six.int2byte(density_byte) + self._int_low_high( im.width, 2 ); + density_byte = (1 if high_density_horizontal else 0) + (32 if high_density_vertical else 0) + header = ESC + b"*" + six.int2byte(density_byte) + self._int_low_high( im.width, 2 ) outp = [] outp.append(ESC + b"3" + six.int2byte(16)) # Adjust line-feed size for blob in im.to_column_format(high_density_vertical): @@ -94,7 +94,7 @@ class Escpos(object): :param fn: Function number to use, as byte :param data: Data to send """ - header = self._int_low_high(len(data) + 2, 2); + header = self._int_low_high(len(data) + 2, 2) self._raw(GS + b'(L' + header + m + fn + data) def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False): @@ -138,14 +138,14 @@ class Escpos(object): # Native 2D code printing cn = b'1' # Code type for QR code # Select model: 1, 2 or micro. - self._send_2d_code_data(six.int2byte(65), cn, six.int2byte(48 + model) + six.int2byte(0)); + self._send_2d_code_data(six.int2byte(65), cn, six.int2byte(48 + model) + six.int2byte(0)) # Set dot size. - self._send_2d_code_data(six.int2byte(67), cn, six.int2byte(size)); + self._send_2d_code_data(six.int2byte(67), cn, six.int2byte(size)) # Set error correction level: L, M, Q, or H - self._send_2d_code_data(six.int2byte(69), cn, six.int2byte(48 + ec)); + self._send_2d_code_data(six.int2byte(69), cn, six.int2byte(48 + ec)) # Send content & print - self._send_2d_code_data(six.int2byte(80), cn, content.encode('utf-8'), b'0'); - self._send_2d_code_data(six.int2byte(81), cn, b'', b'0'); + self._send_2d_code_data(six.int2byte(80), cn, content.encode('utf-8'), b'0') + self._send_2d_code_data(six.int2byte(81), cn, b'', b'0') def _send_2d_code_data(self, fn, cn, data, m=b''): """ Wrapper for GS ( k, to calculate and send correct data length. @@ -157,7 +157,7 @@ class Escpos(object): """ if len(m) > 1 or len(cn) != 1 or len(fn) != 1: raise ValueError("cn and fn must be one byte each.") - header = self._int_low_high(len(data) + len(m) + 2, 2); + header = self._int_low_high(len(data) + len(m) + 2, 2) self._raw(GS + b'(k' + header + cn + fn + m + data) @staticmethod @@ -167,12 +167,12 @@ class Escpos(object): :param inp_number: Input number :param out_bytes: The number of bytes to output (1 - 4). """ - max_input = (256 << (out_bytes * 8) - 1); + max_input = (256 << (out_bytes * 8) - 1) if not 1 <= out_bytes <= 4: raise ValueError("Can only output 1-4 byes") 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)) - outp = b''; + outp = b'' for _ in range(0, out_bytes): outp += six.int2byte(inp_number % 256) inp_number = inp_number // 256 From a0d8689141d1f95abf2af2c49b869bd8c701a4fd Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Wed, 13 Apr 2016 21:27:51 +1000 Subject: [PATCH 7/8] apply fixes, mainly to whitespace ( patch by @patkan in #128 ) --- escpos/escpos.py | 11 +++++------ escpos/image.py | 3 ++- test/test_function_image.py | 17 +++++++++++++++++ test/test_function_qr_native.py | 10 ++++++++++ test/test_image.py | 7 ++++++- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/escpos/escpos.py b/escpos/escpos.py index 6bcfa40..639de99 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -49,7 +49,7 @@ 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"): """ Print an image :param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp` @@ -78,12 +78,11 @@ class Escpos(object): if impl == "bitImageColumn": # ESC *, column format bit image density_byte = (1 if high_density_horizontal else 0) + (32 if high_density_vertical else 0) - header = ESC + b"*" + six.int2byte(density_byte) + self._int_low_high( im.width, 2 ) - outp = [] - outp.append(ESC + b"3" + six.int2byte(16)) # Adjust line-feed size + header = ESC + b"*" + six.int2byte(density_byte) + self._int_low_high(im.width, 2) + outp = [ESC + b"3" + six.int2byte(16)] # Adjust line-feed size for blob in im.to_column_format(high_density_vertical): outp.append(header + blob + b"\n") - outp.append(ESC + b"2"); # Reset line-feed size + outp.append(ESC + b"2") # Reset line-feed size self._raw(b''.join(outp)) def _image_send_graphics_data(self, m, fn, data): @@ -136,7 +135,7 @@ class Escpos(object): self.image(im) return # Native 2D code printing - cn = b'1' # Code type for QR code + cn = b'1' # Code type for QR code # Select model: 1, 2 or micro. self._send_2d_code_data(six.int2byte(65), cn, six.int2byte(48 + model) + six.int2byte(0)) # Set dot size. diff --git a/escpos/image.py b/escpos/image.py index 8dd46d4..1180614 100644 --- a/escpos/image.py +++ b/escpos/image.py @@ -10,6 +10,7 @@ This module contains the image format handler :py:class:`EscposImage`. from PIL import Image, ImageOps + class EscposImage(object): """ Load images in, and output ESC/POS formats. @@ -64,7 +65,7 @@ class EscposImage(object): _, height_pixels = self._im.size return height_pixels - def to_column_format(self, high_density_vertical = True): + def to_column_format(self, high_density_vertical=True): """ Extract slices of an image as equal-sized blobs of column-format data. diff --git a/test/test_function_image.py b/test/test_function_image.py index 17844a0..f60aa76 100644 --- a/test/test_function_image.py +++ b/test/test_function_image.py @@ -7,9 +7,15 @@ :license: GNU GPL v3 """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + import escpos.printer as printer from PIL import Image + # Raster format print def test_bit_image_black(): """ @@ -24,6 +30,7 @@ def test_bit_image_black(): instance.image(im, impl="bitImageRaster") assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\x80') + def test_bit_image_white(): """ Test printing solid white bit image (raster) @@ -32,6 +39,7 @@ def test_bit_image_white(): instance.image('test/resources/canvas_white.png', impl="bitImageRaster") assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\x00') + def test_bit_image_both(): """ Test printing black/white bit image (raster) @@ -40,6 +48,7 @@ def test_bit_image_both(): instance.image('test/resources/black_white.png', impl="bitImageRaster") assert(instance.output == b'\x1dv0\x00\x01\x00\x02\x00\xc0\x00') + def test_bit_image_transparent(): """ Test printing black/transparent bit image (raster) @@ -48,6 +57,7 @@ def test_bit_image_transparent(): instance.image('test/resources/black_transparent.png', impl="bitImageRaster") assert(instance.output == b'\x1dv0\x00\x01\x00\x02\x00\xc0\x00') + # Column format print def test_bit_image_colfmt_black(): """ @@ -57,6 +67,7 @@ def test_bit_image_colfmt_black(): instance.image('test/resources/canvas_black.png', impl="bitImageColumn") assert(instance.output == b'\x1b3\x10\x1b*!\x01\x00\x80\x00\x00\x0a\x1b2') + def test_bit_image_colfmt_white(): """ Test printing solid white bit image (column format) @@ -65,6 +76,7 @@ def test_bit_image_colfmt_white(): instance.image('test/resources/canvas_white.png', impl="bitImageColumn") assert(instance.output == b'\x1b3\x10\x1b*!\x01\x00\x00\x00\x00\x0a\x1b2') + def test_bit_image_colfmt_both(): """ Test printing black/white bit image (column format) @@ -73,6 +85,7 @@ def test_bit_image_colfmt_both(): instance.image('test/resources/black_white.png', impl="bitImageColumn") assert(instance.output == b'\x1b3\x10\x1b*!\x02\x00\x80\x00\x00\x80\x00\x00\x0a\x1b2') + def test_bit_image_colfmt_transparent(): """ Test printing black/transparent bit image (column format) @@ -81,6 +94,7 @@ def test_bit_image_colfmt_transparent(): instance.image('test/resources/black_transparent.png', impl="bitImageColumn") assert(instance.output == b'\x1b3\x10\x1b*!\x02\x00\x80\x00\x00\x80\x00\x00\x0a\x1b2') + # Graphics print def test_graphics_black(): """ @@ -90,6 +104,7 @@ def test_graphics_black(): instance.image('test/resources/canvas_black.png', impl="graphics") assert(instance.output == b'\x1d(L\x0b\x000p0\x01\x011\x01\x00\x01\x00\x80\x1d(L\x02\x0002') + def test_graphics_white(): """ Test printing solid white graphics @@ -98,6 +113,7 @@ def test_graphics_white(): instance.image('test/resources/canvas_white.png', impl="graphics") assert(instance.output == b'\x1d(L\x0b\x000p0\x01\x011\x01\x00\x01\x00\x00\x1d(L\x02\x0002') + def test_graphics_both(): """ Test printing black/white graphics @@ -106,6 +122,7 @@ def test_graphics_both(): instance.image('test/resources/black_white.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_graphics_transparent(): """ Test printing black/transparent graphics diff --git a/test/test_function_qr_native.py b/test/test_function_qr_native.py index 7f24e42..a3355ca 100644 --- a/test/test_function_qr_native.py +++ b/test/test_function_qr_native.py @@ -16,6 +16,7 @@ from nose.tools import raises import escpos.printer as printer from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1 + def test_defaults(): """Test QR code with defaults""" instance = printer.Dummy() @@ -24,12 +25,14 @@ def test_defaults(): b'(k\x07\x001P01234\x1d(k\x03\x001Q0' assert(instance.output == expected) + def test_empty(): """Test QR printing blank code""" instance = printer.Dummy() instance.qr("", native=True) assert(instance.output == b'') + def test_ec(): """Test QR error correction setting""" instance = printer.Dummy() @@ -38,6 +41,7 @@ def test_ec(): b'(k\x07\x001P01234\x1d(k\x03\x001Q0' assert(instance.output == expected) + def test_size(): """Test QR box size""" instance = printer.Dummy() @@ -46,6 +50,7 @@ def test_size(): b'(k\x07\x001P01234\x1d(k\x03\x001Q0' assert(instance.output == expected) + def test_model(): """Test QR model""" instance = printer.Dummy() @@ -54,24 +59,28 @@ def test_model(): b'(k\x07\x001P01234\x1d(k\x03\x001Q0' assert(instance.output == expected) + @raises(ValueError) def test_invalid_ec(): """Test invalid QR error correction""" instance = printer.Dummy() instance.qr("1234", native=True, ec=-1) + @raises(ValueError) def test_invalid_size(): """Test invalid QR size""" instance = printer.Dummy() instance.qr("1234", native=True, size=0) + @raises(ValueError) def test_invalid_model(): """Test invalid QR model""" instance = printer.Dummy() instance.qr("1234", native=True, model="Hello") + def test_image(): """Test QR as image""" instance = printer.Dummy() @@ -82,6 +91,7 @@ def test_image(): b'i(\x7f<\xa8A \xd8]\'\xc4]y\xf8]E\x80Ar\x94\x7fR@\x00\x00\x00' assert(instance.output == expected) + @raises(ValueError) def test_image_invalid_model(): """Test unsupported QR model as image""" diff --git a/test/test_image.py b/test/test_image.py index 6612703..fd49797 100644 --- a/test/test_image.py +++ b/test/test_image.py @@ -10,6 +10,7 @@ converted to ESC/POS column & raster formats. from escpos.image import EscposImage + def test_image_black(): """ Test rendering solid black image @@ -17,6 +18,7 @@ def test_image_black(): for img_format in ['png', 'jpg', 'gif']: _load_and_check_img('canvas_black.' + img_format, 1, 1, b'\x80', [b'\x80']) + def test_image_black_transparent(): """ Test rendering black/transparent image @@ -24,6 +26,7 @@ def test_image_black_transparent(): for img_format in ['png', 'gif']: _load_and_check_img('black_transparent.' + img_format, 2, 2, b'\xc0\x00', [b'\x80\x80']) + def test_image_black_white(): """ Test rendering black/white image @@ -31,6 +34,7 @@ def test_image_black_white(): for img_format in ['png', 'jpg', 'gif']: _load_and_check_img('black_white.' + img_format, 2, 2, b'\xc0\x00', [b'\x80\x80']) + def test_image_white(): """ Test rendering solid white image @@ -38,11 +42,12 @@ def test_image_white(): for img_format in ['png', 'jpg', 'gif']: _load_and_check_img('canvas_white.' + img_format, 1, 1, b'\x00', [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. """ - im = EscposImage('test/resources/' + filename) + im = EscposImage('test/resources/' + filename) assert(im.width == width_expected) assert(im.height == height_expected) assert(im.to_raster_format() == raster_format_expected) From f903af6730a6891e75e1833ede61b2ef976085a4 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Wed, 13 Apr 2016 22:19:50 +1000 Subject: [PATCH 8/8] fix: horizontal/vertical density settings were backwards for bitImageRaster --- escpos/escpos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/escpos/escpos.py b/escpos/escpos.py index 639de99..b9dcdf8 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -59,7 +59,7 @@ class Escpos(object): if impl == "bitImageRaster": # GS v 0, raster format bit image - density_byte = (0 if high_density_vertical else 1) + (0 if high_density_horizontal else 2) + density_byte = (0 if high_density_horizontal else 1) + (0 if high_density_vertical else 2) header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) + self._int_low_high(im.height, 2) self._raw(header + im.to_raster_format())