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: 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/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 435b95e..b9dcdf8 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -14,20 +14,14 @@ 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 * from abc import ABCMeta, abstractmethod # abstract base class support - +from escpos.image import EscposImage class Escpos(object): """ ESC/POS Printer object @@ -55,213 +49,52 @@ class Escpos(object): """ pass - @staticmethod - def _check_image_size(size): - """ Check and fix the size of the image to 32 bits + def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster"): + """ Print an image - :param size: size of the image - :returns: tuple of image borders - :rtype: (int, int) + :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_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()) + + 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 = [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)) + + def _image_send_graphics_data(self, m, fn, data): """ - 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: + 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 """ - 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 - - Prints an image. The image is automatically adjusted in size in order to print it. - - .. 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` - """ - 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') + 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 @@ -286,7 +119,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, @@ -299,19 +132,19 @@ 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 + 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. @@ -323,7 +156,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 @@ -333,12 +166,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 diff --git a/escpos/image.py b/escpos/image.py new file mode 100644 index 0000000..1180614 --- /dev/null +++ b/escpos/image.py @@ -0,0 +1,90 @@ +""" Image format handling class + +This module contains the image format handler :py:class:`EscposImage`. + +:author: `Michael Billington `_ +:organization: `python-escpos `_ +:copyright: Copyright (c) 2016 Michael Billington +:license: GNU GPL v3 +""" + +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 + + :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..ee9e227 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,38 @@ 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): + """ Get the data that was sent to this printer """ + return b''.join(self._output_list) diff --git a/test/255x255.png b/test/255x255.png deleted file mode 100644 index a99fb5f..0000000 Binary files a/test/255x255.png and /dev/null differ diff --git a/test/400x400.png b/test/400x400.png deleted file mode 100644 index cab3db6..0000000 Binary files a/test/400x400.png and /dev/null differ diff --git a/test/50x50.png b/test/50x50.png deleted file mode 100644 index 67b483b..0000000 Binary files a/test/50x50.png and /dev/null differ diff --git a/test/resources/black_transparent.gif b/test/resources/black_transparent.gif new file mode 100644 index 0000000..6c54bad Binary files /dev/null and b/test/resources/black_transparent.gif differ diff --git a/test/resources/black_transparent.png b/test/resources/black_transparent.png new file mode 100644 index 0000000..b43bbb8 Binary files /dev/null and b/test/resources/black_transparent.png differ diff --git a/test/resources/black_white.gif b/test/resources/black_white.gif new file mode 100644 index 0000000..0a044a6 Binary files /dev/null and b/test/resources/black_white.gif differ diff --git a/test/resources/black_white.jpg b/test/resources/black_white.jpg new file mode 100644 index 0000000..6539cec Binary files /dev/null and b/test/resources/black_white.jpg differ diff --git a/test/resources/black_white.png b/test/resources/black_white.png new file mode 100644 index 0000000..33ba331 Binary files /dev/null and b/test/resources/black_white.png differ diff --git a/test/resources/canvas_black.gif b/test/resources/canvas_black.gif new file mode 100644 index 0000000..49b19db Binary files /dev/null and b/test/resources/canvas_black.gif differ diff --git a/test/resources/canvas_black.jpg b/test/resources/canvas_black.jpg new file mode 100644 index 0000000..d059f43 Binary files /dev/null and b/test/resources/canvas_black.jpg differ diff --git a/test/resources/canvas_black.png b/test/resources/canvas_black.png new file mode 100644 index 0000000..52e6ba9 Binary files /dev/null and b/test/resources/canvas_black.png differ diff --git a/test/resources/canvas_white.gif b/test/resources/canvas_white.gif new file mode 100644 index 0000000..7881ce6 Binary files /dev/null and b/test/resources/canvas_white.gif differ diff --git a/test/resources/canvas_white.jpg b/test/resources/canvas_white.jpg new file mode 100644 index 0000000..516a965 Binary files /dev/null and b/test/resources/canvas_white.jpg differ diff --git a/test/resources/canvas_white.png b/test/resources/canvas_white.png new file mode 100644 index 0000000..4231a4b Binary files /dev/null and b/test/resources/canvas_white.png differ diff --git a/test/test_function_image.py b/test/test_function_image.py index 89fcade..f60aa76 100644 --- a/test/test_function_image.py +++ b/test/test_function_image.py @@ -1,9 +1,9 @@ -#!/usr/bin/python -"""tests for the image printing function +#!/usr/bin/env python +""" Image function tests- Check that image print commands are sent correctly. -:author: `Patrick Kanzler `_ +:author: `Michael Billington `_ :organization: `python-escpos `_ -:copyright: Copyright (c) 2016 `python-escpos `_ +:copyright: Copyright (c) 2016 `Michael Billington `_ :license: GNU GPL v3 """ @@ -12,39 +12,121 @@ 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 +from PIL import Image -devfile = 'testfile' -def setup_testfile(): - """create a testfile as devfile""" - fhandle = open(devfile, 'a') - try: - os.utime(devfile, None) - finally: - fhandle.close() +# 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 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") +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') -@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") +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..a3355ca 100644 --- a/test/test_function_qr_native.py +++ b/test/test_function_qr_native.py @@ -12,70 +12,88 @@ 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) + 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) +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..fd49797 --- /dev/null +++ b/test/test_image.py @@ -0,0 +1,57 @@ +#!/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