Merge pull request #128 from mike42/feature/rewrite-image-handling

Rewrite image handling
This commit is contained in:
Patrick Kanzler 2016-04-14 00:00:52 +02:00
commit c6cc28254e
23 changed files with 451 additions and 322 deletions

View File

@ -20,7 +20,6 @@ matrix:
- python: pypy3 - python: pypy3
env: TOXENV=pypy3 env: TOXENV=pypy3
allow_failures: allow_failures:
- python: pypy3
- python: 3.5-dev - python: 3.5-dev
- python: nightly - python: nightly
before_install: before_install:

View File

@ -37,8 +37,8 @@ DEMO_FUNCTIONS = {
{'txt': 'Hello, World!\n',} {'txt': 'Hello, World!\n',}
], ],
'qr': [ 'qr': [
{'text': 'This tests a QR code'}, {'content': 'This tests a QR code'},
{'text': 'https://en.wikipedia.org/'} {'content': 'https://en.wikipedia.org/'}
], ],
'barcodes_a': [ 'barcodes_a': [
{'bc': 'UPC-A', 'code': '13243546576'}, {'bc': 'UPC-A', 'code': '13243546576'},
@ -87,7 +87,7 @@ ESCPOS_COMMANDS = [
}, },
'arguments': [ 'arguments': [
{ {
'option_strings': ('--text',), 'option_strings': ('--content',),
'help': 'Text to print as a qr code', 'help': 'Text to print as a qr code',
'required': True, 'required': True,
} }
@ -223,10 +223,26 @@ ESCPOS_COMMANDS = [
}, },
'arguments': [ 'arguments': [
{ {
'option_strings': ('--path_img',), 'option_strings': ('--img_source',),
'help': 'Path to image', 'help': 'Path to image',
'required': True, '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,
},
], ],
}, },
{ {

View File

@ -184,15 +184,15 @@ BARCODE_TYPES = {
} }
## QRCode error correction levels ## QRCode error correction levels
QR_ECLEVEL_L = 0; QR_ECLEVEL_L = 0
QR_ECLEVEL_M = 1; QR_ECLEVEL_M = 1
QR_ECLEVEL_Q = 2; QR_ECLEVEL_Q = 2
QR_ECLEVEL_H = 3; QR_ECLEVEL_H = 3
## QRcode models ## QRcode models
QR_MODEL_1 = 1; QR_MODEL_1 = 1
QR_MODEL_2 = 2; QR_MODEL_2 = 2
QR_MICRO = 3; QR_MICRO = 3
# Image format # Image format
# NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image" # NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image"

View File

@ -14,20 +14,14 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
import six
from PIL import Image
import qrcode import qrcode
import textwrap import textwrap
import binascii
import operator
from .constants import * from .constants import *
from .exceptions import * from .exceptions import *
from abc import ABCMeta, abstractmethod # abstract base class support from abc import ABCMeta, abstractmethod # abstract base class support
from escpos.image import EscposImage
class Escpos(object): class Escpos(object):
""" ESC/POS Printer object """ ESC/POS Printer object
@ -55,213 +49,52 @@ class Escpos(object):
""" """
pass pass
@staticmethod def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster"):
def _check_image_size(size): """ Print an image
""" Check and fix the size of the image to 32 bits
:param size: size of the image :param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp`
:returns: tuple of image borders
:rtype: (int, int) """
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: Wrapper for GS ( L, to calculate and send correct data length.
return 0, 0
else: :param m: Modifier//variant for function. Usually '0'
image_border = 32 - (size % 32) :param fn: Function number to use, as byte
if (image_border % 2) == 0: :param data: Data to send
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 header = self._int_low_high(len(data) + 2, 2)
cont = 0 self._raw(GS + b'(L' + header + m + fn + data)
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')
def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False): def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False):
""" Print QR Code for the provided string """ Print QR Code for the provided string
@ -286,7 +119,7 @@ class Escpos(object):
if not native: if not native:
# Map ESC/POS error correction levels to python 'qrcode' library constant and render to an image # Map ESC/POS error correction levels to python 'qrcode' library constant and render to an image
if model != QR_MODEL_2: 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 = { python_qr_ec = {
QR_ECLEVEL_H: qrcode.constants.ERROR_CORRECT_H, QR_ECLEVEL_H: qrcode.constants.ERROR_CORRECT_H,
QR_ECLEVEL_L: qrcode.constants.ERROR_CORRECT_L, QR_ECLEVEL_L: qrcode.constants.ERROR_CORRECT_L,
@ -299,19 +132,19 @@ class Escpos(object):
qr_img = qr_code.make_image() qr_img = qr_code.make_image()
im = qr_img._img.convert("RGB") im = qr_img._img.convert("RGB")
# Convert the RGB image in printable image # Convert the RGB image in printable image
self._convert_image(im) self.image(im)
return return
# Native 2D code printing # 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. # 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. # 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 # 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 # 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(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(81), cn, b'', b'0')
def _send_2d_code_data(self, fn, cn, data, m=b''): def _send_2d_code_data(self, fn, cn, data, m=b''):
""" Wrapper for GS ( k, to calculate and send correct data length. """ 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: if len(m) > 1 or len(cn) != 1 or len(fn) != 1:
raise ValueError("cn and fn must be one byte each.") 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) self._raw(GS + b'(k' + header + cn + fn + m + data)
@staticmethod @staticmethod
@ -333,12 +166,12 @@ class Escpos(object):
:param inp_number: Input number :param inp_number: Input number
:param out_bytes: The number of bytes to output (1 - 4). :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: if not 1 <= out_bytes <= 4:
raise ValueError("Can only output 1-4 byes") raise ValueError("Can only output 1-4 byes")
if not 0 <= inp_number <= max_input: 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} byes".format(max_input, out_bytes))
outp = b''; outp = b''
for _ in range(0, out_bytes): for _ in range(0, out_bytes):
outp += six.int2byte(inp_number % 256) outp += six.int2byte(inp_number % 256)
inp_number = inp_number // 256 inp_number = inp_number // 256

90
escpos/image.py Normal file
View File

@ -0,0 +1,90 @@
""" Image format handling class
This module contains the image format handler :py:class:`EscposImage`.
:author: `Michael Billington <michael.billington@gmail.com>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 Michael Billington <michael.billington@gmail.com>
: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()

View File

@ -20,7 +20,6 @@ import socket
from .escpos import Escpos from .escpos import Escpos
from .exceptions import * from .exceptions import *
class Usb(Escpos): class Usb(Escpos):
""" USB printer """ USB printer
@ -260,3 +259,38 @@ class File(Escpos):
""" Close system file """ """ Close system file """
self.device.flush() self.device.flush()
self.device.close() 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

View File

@ -1,9 +1,9 @@
#!/usr/bin/python #!/usr/bin/env python
"""tests for the image printing function """ Image function tests- Check that image print commands are sent correctly.
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_ :author: `Michael Billington <michael.billington@gmail.com>`_
:organization: `python-escpos <https://github.com/python-escpos>`_ :organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_ :copyright: Copyright (c) 2016 `Michael Billington <michael.billington@gmail.com>`_
:license: GNU GPL v3 :license: GNU GPL v3
""" """
@ -12,39 +12,121 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
from nose.tools import with_setup
import escpos.printer as printer import escpos.printer as printer
import os from PIL import Image
devfile = 'testfile'
def setup_testfile(): # Raster format print
"""create a testfile as devfile""" def test_bit_image_black():
fhandle = open(devfile, 'a') """
try: Test printing solid black bit image (raster)
os.utime(devfile, None) """
finally: instance = printer.Dummy()
fhandle.close() 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_bit_image_white():
def test_function_image_with_50x50_png(): """
"""test the image function with 50x50.png (grayscale png)""" Test printing solid white bit image (raster)
instance = printer.File(devfile=devfile) """
instance.image("test/50x50.png") 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_bit_image_both():
def test_function_image_with_400x400_png(): """
"""test the image function with 400x400.png (grayscale png)""" Test printing black/white bit image (raster)
instance = printer.File(devfile=devfile) """
instance.image("test/400x400.png") 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')

View File

@ -12,70 +12,88 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
from nose.tools import with_setup from nose.tools import raises
import escpos.printer as printer import escpos.printer as printer
import os
from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1 from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1
devfile = 'testfile'
def test_defaults():
def setup_testfile(): """Test QR code with defaults"""
"""create a testfile as devfile""" instance = printer.Dummy()
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)
instance.qr("1234", native=True) instance.qr("1234", native=True)
instance.flush() expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d' \
with open(devfile, "rb") as f: b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
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') assert(instance.output == expected)
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_empty(): def test_empty():
"""test QR printing blank code""" """Test QR printing blank code"""
instance = printer.File(devfile=devfile) instance = printer.Dummy()
instance.qr("", native=True) instance.qr("", native=True)
instance.flush() assert(instance.output == b'')
with open(devfile, "rb") as f:
assert(f.read() == b'')
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_ec(): def test_ec():
"""test QR error correction setting""" """Test QR error correction setting"""
instance = printer.File(devfile=devfile) instance = printer.Dummy()
instance.qr("1234", native=True, ec=QR_ECLEVEL_H) instance.qr("1234", native=True, ec=QR_ECLEVEL_H)
instance.flush() expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E3\x1d' \
with open(devfile, "rb") as f: b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
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') assert(instance.output == expected)
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_size(): def test_size():
"""test QR box size""" """Test QR box size"""
instance = printer.File(devfile=devfile) instance = printer.Dummy()
instance.qr("1234", native=True, size=7) instance.qr("1234", native=True, size=7)
instance.flush() expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x07\x1d(k\x03\x001E0\x1d' \
with open(devfile, "rb") as f: b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
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') assert(instance.output == expected)
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_model(): def test_model():
"""test QR model""" """Test QR model"""
instance = printer.File(devfile=devfile) instance = printer.Dummy()
instance.qr("1234", native=True, model=QR_MODEL_1) instance.qr("1234", native=True, model=QR_MODEL_1)
instance.flush() expected = b'\x1d(k\x04\x001A1\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d' \
with open(devfile, "rb") as f: b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
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') 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)

57
test/test_image.py Normal file
View File

@ -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 <michael.billington@gmail.com>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `Michael Billington <michael.billington@gmail.com>`_
: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