mirror of
				https://github.com/python-escpos/python-escpos
				synced 2025-10-23 09:30:00 +00:00 
			
		
		
		
	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
This commit is contained in:
		@@ -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,
 | 
			
		||||
            },      
 | 
			
		||||
                      
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										85
									
								
								escpos/image.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								escpos/image.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <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):
 | 
			
		||||
    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()
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user