2010-02-26 08:54:46 +00:00
|
|
|
#!/usr/bin/python
|
2016-03-28 13:54:43 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2016-01-05 16:30:40 +00:00
|
|
|
""" Main class
|
|
|
|
|
|
|
|
This module contains the abstract base class :py:class:`Escpos`.
|
|
|
|
|
|
|
|
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
|
|
|
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
2017-01-29 23:39:43 +00:00
|
|
|
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
|
|
|
|
:license: MIT
|
2014-05-21 05:15:54 +00:00
|
|
|
"""
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2016-03-03 03:21:35 +00:00
|
|
|
|
2013-03-14 08:31:07 +00:00
|
|
|
import qrcode
|
2015-06-04 11:57:59 +00:00
|
|
|
import textwrap
|
2017-01-30 01:15:40 +00:00
|
|
|
import six
|
2017-07-24 11:57:02 +00:00
|
|
|
import time
|
2017-10-08 18:05:18 +00:00
|
|
|
from re import match as re_match
|
2017-01-30 01:15:40 +00:00
|
|
|
|
2017-05-16 18:55:30 +00:00
|
|
|
import barcode
|
|
|
|
from barcode.writer import ImageWriter
|
|
|
|
|
2018-05-14 23:03:07 +00:00
|
|
|
import os
|
|
|
|
|
2017-01-30 01:15:40 +00:00
|
|
|
from .constants import ESC, GS, NUL, QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q
|
|
|
|
from .constants import QR_MODEL_1, QR_MODEL_2, QR_MICRO, BARCODE_TYPES, BARCODE_HEIGHT, BARCODE_WIDTH
|
2017-10-08 18:05:18 +00:00
|
|
|
from .constants import BARCODE_FONT_A, BARCODE_FONT_B, BARCODE_FORMATS
|
2017-01-30 01:15:40 +00:00
|
|
|
from .constants import BARCODE_TXT_OFF, BARCODE_TXT_BTH, BARCODE_TXT_ABV, BARCODE_TXT_BLW
|
2017-05-14 08:38:21 +00:00
|
|
|
from .constants import TXT_SIZE, TXT_NORMAL
|
|
|
|
from .constants import SET_FONT
|
|
|
|
from .constants import LINESPACING_FUNCS, LINESPACING_RESET
|
2016-11-15 15:24:44 +00:00
|
|
|
from .constants import LINE_DISPLAY_OPEN, LINE_DISPLAY_CLEAR, LINE_DISPLAY_CLOSE
|
2017-01-30 01:15:40 +00:00
|
|
|
from .constants import CD_KICK_DEC_SEQUENCE, CD_KICK_5, CD_KICK_2, PAPER_FULL_CUT, PAPER_PART_CUT
|
|
|
|
from .constants import HW_RESET, HW_SELECT, HW_INIT
|
2017-07-26 08:01:08 +00:00
|
|
|
from .constants import CTL_VT, CTL_CR, CTL_FF, CTL_LF, CTL_SET_HT, PANEL_BUTTON_OFF, PANEL_BUTTON_ON
|
2017-05-14 08:38:21 +00:00
|
|
|
from .constants import TXT_STYLE
|
2017-07-24 11:57:02 +00:00
|
|
|
from .constants import RT_STATUS_ONLINE, RT_MASK_ONLINE
|
2017-07-27 21:05:50 +00:00
|
|
|
from .constants import RT_STATUS_PAPER, RT_MASK_PAPER, RT_MASK_LOWPAPER, RT_MASK_NOPAPER
|
2017-01-30 01:15:40 +00:00
|
|
|
|
|
|
|
from .exceptions import BarcodeTypeError, BarcodeSizeError, TabPosError
|
|
|
|
from .exceptions import CashDrawerError, SetVariableError, BarcodeCodeError
|
2017-05-23 13:13:28 +00:00
|
|
|
from .exceptions import ImageWidthError
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2016-07-23 20:16:11 +00:00
|
|
|
from .magicencode import MagicEncode
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2015-11-27 22:10:20 +00:00
|
|
|
from abc import ABCMeta, abstractmethod # abstract base class support
|
2016-04-03 07:36:54 +00:00
|
|
|
from escpos.image import EscposImage
|
2016-08-30 10:53:31 +00:00
|
|
|
from escpos.capabilities import get_profile, BARCODE_B
|
2016-01-08 02:34:14 +00:00
|
|
|
|
2016-06-19 10:25:40 +00:00
|
|
|
|
2016-04-03 20:16:03 +00:00
|
|
|
@six.add_metaclass(ABCMeta)
|
2015-11-27 20:24:47 +00:00
|
|
|
class Escpos(object):
|
2016-01-05 16:30:40 +00:00
|
|
|
""" ESC/POS Printer object
|
|
|
|
|
|
|
|
This class is the abstract base class for an esc/pos-printer. The printer implementations are children of this
|
|
|
|
class.
|
|
|
|
"""
|
2015-11-27 20:20:12 +00:00
|
|
|
device = None
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2016-08-27 09:09:08 +00:00
|
|
|
def __init__(self, profile=None, magic_encode_args=None, **kwargs):
|
2015-12-12 16:19:30 +00:00
|
|
|
""" Initialize ESCPOS Printer
|
|
|
|
|
2016-08-25 15:30:20 +00:00
|
|
|
:param profile: Printer profile"""
|
|
|
|
self.profile = get_profile(profile)
|
2016-08-27 09:09:08 +00:00
|
|
|
self.magic = MagicEncode(self, **(magic_encode_args or {}))
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2016-03-28 13:54:43 +00:00
|
|
|
def __del__(self):
|
|
|
|
""" call self.close upon deletion """
|
|
|
|
self.close()
|
|
|
|
|
2015-11-27 22:10:20 +00:00
|
|
|
@abstractmethod
|
|
|
|
def _raw(self, msg):
|
|
|
|
""" Sends raw data to the printer
|
|
|
|
|
|
|
|
This function has to be individually implemented by the implementations.
|
2016-01-05 16:30:40 +00:00
|
|
|
|
2015-11-27 22:10:20 +00:00
|
|
|
:param msg: message string to be sent to the printer
|
2016-03-03 02:37:43 +00:00
|
|
|
:type msg: bytes
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
2017-08-10 19:51:51 +00:00
|
|
|
def _read(self):
|
2017-07-24 11:57:02 +00:00
|
|
|
""" Returns a NotImplementedError if the instance of the class doesn't override this method.
|
|
|
|
:raises NotImplementedError
|
|
|
|
"""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
2016-08-01 12:39:44 +00:00
|
|
|
def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster",
|
2017-08-31 07:25:35 +00:00
|
|
|
fragment_height=960, center=False):
|
2016-04-03 07:36:54 +00:00
|
|
|
""" Print an image
|
|
|
|
|
2016-04-28 19:57:57 +00:00
|
|
|
You can select whether the printer should print in high density or not. The default value is high density.
|
|
|
|
When printing in low density, the image will be stretched.
|
|
|
|
|
|
|
|
Esc/Pos supplies several commands for printing. This function supports three of them. Please try to vary the
|
|
|
|
implementations if you have any problems. For example the printer `IT80-002` will have trouble aligning
|
|
|
|
images that are not printed in Column-mode.
|
|
|
|
|
|
|
|
The available printing implementations are:
|
|
|
|
|
|
|
|
* `bitImageRaster`: prints with the `GS v 0`-command
|
|
|
|
* `graphics`: prints with the `GS ( L`-command
|
|
|
|
* `bitImageColumn`: prints with the `ESC *`-command
|
|
|
|
|
2020-03-11 14:51:16 +00:00
|
|
|
When trying to center an image make sure you have initialized the printer with a valid profile, that
|
|
|
|
contains a media width pixel field. Otherwise the centering will have no effect.
|
|
|
|
|
2016-04-03 07:36:54 +00:00
|
|
|
:param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp`
|
2016-04-28 19:57:57 +00:00
|
|
|
:param high_density_vertical: print in high density in vertical direction *default:* True
|
|
|
|
:param high_density_horizontal: print in high density in horizontal direction *default:* True
|
|
|
|
:param impl: choose image printing mode between `bitImageRaster`, `graphics` or `bitImageColumn`
|
2017-01-25 22:56:06 +00:00
|
|
|
:param fragment_height: Images larger than this will be split into multiple fragments *default:* 960
|
2017-08-31 07:25:35 +00:00
|
|
|
:param center: Center image horizontally *default:* False
|
2016-04-28 19:57:57 +00:00
|
|
|
|
2017-01-29 23:10:14 +00:00
|
|
|
"""
|
2016-04-03 07:36:54 +00:00
|
|
|
im = EscposImage(img_source)
|
2016-08-01 12:39:44 +00:00
|
|
|
|
2017-05-23 13:13:28 +00:00
|
|
|
try:
|
2020-03-11 14:51:16 +00:00
|
|
|
if self.profile.profile_data['media']['width']['pixels'] == "Unknown":
|
|
|
|
print("The media.width.pixel field of the printer profile is not set. " +
|
|
|
|
"The center flag will have no effect.")
|
|
|
|
|
2017-05-23 13:13:28 +00:00
|
|
|
max_width = int(self.profile.profile_data['media']['width']['pixels'])
|
2017-08-31 07:25:35 +00:00
|
|
|
|
2017-05-23 13:13:28 +00:00
|
|
|
if im.width > max_width:
|
|
|
|
raise ImageWidthError('{} > {}'.format(im.width, max_width))
|
2017-08-31 07:25:35 +00:00
|
|
|
|
|
|
|
if center:
|
|
|
|
im.center(max_width)
|
2017-05-23 13:13:28 +00:00
|
|
|
except KeyError:
|
|
|
|
# If the printer's pixel width is not known, print anyways...
|
|
|
|
pass
|
|
|
|
except ValueError:
|
|
|
|
# If the max_width cannot be converted to an int, print anyways...
|
|
|
|
pass
|
|
|
|
|
2016-08-01 12:39:44 +00:00
|
|
|
if im.height > fragment_height:
|
|
|
|
fragments = im.split(fragment_height)
|
|
|
|
for fragment in fragments:
|
|
|
|
self.image(fragment,
|
|
|
|
high_density_vertical=high_density_vertical,
|
|
|
|
high_density_horizontal=high_density_horizontal,
|
|
|
|
impl=impl,
|
|
|
|
fragment_height=fragment_height)
|
|
|
|
return
|
2017-01-29 23:10:14 +00:00
|
|
|
|
2016-04-03 07:36:54 +00:00
|
|
|
if impl == "bitImageRaster":
|
|
|
|
# GS v 0, raster format bit image
|
2016-04-13 12:19:50 +00:00
|
|
|
density_byte = (0 if high_density_horizontal else 1) + (0 if high_density_vertical else 2)
|
2017-01-29 23:10:14 +00:00
|
|
|
header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) +\
|
2017-01-30 00:57:36 +00:00
|
|
|
self._int_low_high(im.height, 2)
|
2016-04-03 07:36:54 +00:00
|
|
|
self._raw(header + im.to_raster_format())
|
2017-01-29 23:10:14 +00:00
|
|
|
|
2016-04-03 07:36:54 +00:00
|
|
|
if impl == "graphics":
|
|
|
|
# GS ( L raster format graphics
|
2016-04-07 12:06:14 +00:00
|
|
|
img_header = self._int_low_high(im.width, 2) + self._int_low_high(im.height, 2)
|
|
|
|
tone = b'0'
|
|
|
|
colors = b'1'
|
2016-04-03 07:36:54 +00:00
|
|
|
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()
|
2016-04-07 12:06:14 +00:00
|
|
|
self._image_send_graphics_data(b'0', b'p', header + raster_data)
|
|
|
|
self._image_send_graphics_data(b'0', b'2', b'')
|
2017-01-29 23:10:14 +00:00
|
|
|
|
2016-04-03 07:36:54 +00:00
|
|
|
if impl == "bitImageColumn":
|
|
|
|
# ESC *, column format bit image
|
2016-04-07 12:06:14 +00:00
|
|
|
density_byte = (1 if high_density_horizontal else 0) + (32 if high_density_vertical else 0)
|
2016-04-13 11:27:51 +00:00
|
|
|
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
|
2016-04-03 07:36:54 +00:00
|
|
|
for blob in im.to_column_format(high_density_vertical):
|
|
|
|
outp.append(header + blob + b"\n")
|
2016-04-13 11:27:51 +00:00
|
|
|
outp.append(ESC + b"2") # Reset line-feed size
|
2016-04-03 07:36:54 +00:00
|
|
|
self._raw(b''.join(outp))
|
|
|
|
|
|
|
|
def _image_send_graphics_data(self, m, fn, data):
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2016-04-03 07:36:54 +00:00
|
|
|
Wrapper for GS ( L, to calculate and send correct data length.
|
2017-01-30 00:50:27 +00:00
|
|
|
|
2016-04-03 07:36:54 +00:00
|
|
|
:param m: Modifier//variant for function. Usually '0'
|
|
|
|
:param fn: Function number to use, as byte
|
|
|
|
:param data: Data to send
|
|
|
|
"""
|
2016-04-07 12:06:14 +00:00
|
|
|
header = self._int_low_high(len(data) + 2, 2)
|
2016-04-03 07:36:54 +00:00
|
|
|
self._raw(GS + b'(L' + header + m + fn + data)
|
2015-06-10 01:21:52 +00:00
|
|
|
|
2017-08-31 07:25:35 +00:00
|
|
|
def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2,
|
2018-05-13 16:42:41 +00:00
|
|
|
native=False, center=False, impl="bitImageRaster"):
|
2015-11-27 22:10:20 +00:00
|
|
|
""" Print QR Code for the provided string
|
2013-03-14 08:31:07 +00:00
|
|
|
|
2016-03-19 13:48:29 +00:00
|
|
|
:param content: The content of the code. Numeric data will be more efficiently compacted.
|
2016-06-19 10:25:40 +00:00
|
|
|
:param ec: Error-correction level to use. One of QR_ECLEVEL_L (default), QR_ECLEVEL_M, QR_ECLEVEL_Q or
|
|
|
|
QR_ECLEVEL_H.
|
2016-03-19 13:48:29 +00:00
|
|
|
Higher error correction results in a less compact code.
|
|
|
|
:param size: Pixel size to use. Must be 1-16 (default 3)
|
2016-06-19 10:25:40 +00:00
|
|
|
:param model: QR code model to use. Must be one of QR_MODEL_1, QR_MODEL_2 (default) or QR_MICRO (not supported
|
|
|
|
by all printers).
|
|
|
|
:param native: True to render the code on the printer, False to render the code as an image and send it to the
|
|
|
|
printer (Default)
|
2017-08-31 07:25:35 +00:00
|
|
|
:param center: Centers the code *default:* False
|
2019-06-15 23:53:39 +00:00
|
|
|
:param impl: Image-printing-implementation, refer to :meth:`.image()` for details
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2016-03-19 13:48:29 +00:00
|
|
|
# Basic validation
|
|
|
|
if ec not in [QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q]:
|
|
|
|
raise ValueError("Invalid error correction level")
|
|
|
|
if not 1 <= size <= 16:
|
|
|
|
raise ValueError("Invalid block size (must be 1-16)")
|
|
|
|
if model not in [QR_MODEL_1, QR_MODEL_2, QR_MICRO]:
|
|
|
|
raise ValueError("Invalid QR model (must be one of QR_MODEL_1, QR_MODEL_2, QR_MICRO)")
|
|
|
|
if content == "":
|
|
|
|
# Handle edge case by printing nothing.
|
|
|
|
return
|
|
|
|
if not native:
|
|
|
|
# Map ESC/POS error correction levels to python 'qrcode' library constant and render to an image
|
|
|
|
if model != QR_MODEL_2:
|
2016-08-15 21:24:06 +00:00
|
|
|
raise ValueError("Invalid QR model for qrlib rendering (must be QR_MODEL_2)")
|
2016-03-19 13:48:29 +00:00
|
|
|
python_qr_ec = {
|
2016-06-19 10:25:40 +00:00
|
|
|
QR_ECLEVEL_H: qrcode.constants.ERROR_CORRECT_H,
|
|
|
|
QR_ECLEVEL_L: qrcode.constants.ERROR_CORRECT_L,
|
|
|
|
QR_ECLEVEL_M: qrcode.constants.ERROR_CORRECT_M,
|
|
|
|
QR_ECLEVEL_Q: qrcode.constants.ERROR_CORRECT_Q
|
|
|
|
}
|
2016-03-19 13:48:29 +00:00
|
|
|
qr_code = qrcode.QRCode(version=None, box_size=size, border=1, error_correction=python_qr_ec[ec])
|
|
|
|
qr_code.add_data(content)
|
|
|
|
qr_code.make(fit=True)
|
|
|
|
qr_img = qr_code.make_image()
|
2016-08-15 21:24:06 +00:00
|
|
|
im = qr_img._img.convert("RGB")
|
2017-08-31 07:25:35 +00:00
|
|
|
|
2016-08-15 21:24:06 +00:00
|
|
|
# Convert the RGB image in printable image
|
2017-07-24 13:04:54 +00:00
|
|
|
self.text('\n')
|
2018-05-13 16:42:41 +00:00
|
|
|
self.image(im, center=center, impl=impl)
|
2017-07-24 13:04:54 +00:00
|
|
|
self.text('\n')
|
|
|
|
self.text('\n')
|
2016-03-19 13:48:29 +00:00
|
|
|
return
|
2017-08-31 07:25:35 +00:00
|
|
|
|
|
|
|
if center:
|
|
|
|
raise NotImplementedError("Centering not implemented for native QR rendering")
|
|
|
|
|
2016-03-19 13:48:29 +00:00
|
|
|
# Native 2D code printing
|
2016-04-13 11:27:51 +00:00
|
|
|
cn = b'1' # Code type for QR code
|
2016-03-19 13:48:29 +00:00
|
|
|
# Select model: 1, 2 or micro.
|
2016-04-07 12:06:14 +00:00
|
|
|
self._send_2d_code_data(six.int2byte(65), cn, six.int2byte(48 + model) + six.int2byte(0))
|
2016-03-19 13:48:29 +00:00
|
|
|
# Set dot size.
|
2016-04-07 12:06:14 +00:00
|
|
|
self._send_2d_code_data(six.int2byte(67), cn, six.int2byte(size))
|
2016-03-19 13:48:29 +00:00
|
|
|
# Set error correction level: L, M, Q, or H
|
2016-04-07 12:06:14 +00:00
|
|
|
self._send_2d_code_data(six.int2byte(69), cn, six.int2byte(48 + ec))
|
2016-03-19 13:48:29 +00:00
|
|
|
# Send content & print
|
2016-04-07 12:06:14 +00:00
|
|
|
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')
|
2016-03-19 13:48:29 +00:00
|
|
|
|
|
|
|
def _send_2d_code_data(self, fn, cn, data, m=b''):
|
|
|
|
""" Wrapper for GS ( k, to calculate and send correct data length.
|
|
|
|
|
|
|
|
:param fn: Function to use.
|
|
|
|
:param cn: Output code type. Affects available data.
|
|
|
|
:param data: Data to send.
|
|
|
|
:param m: Modifier/variant for function. Often '0' where used.
|
|
|
|
"""
|
|
|
|
if len(m) > 1 or len(cn) != 1 or len(fn) != 1:
|
|
|
|
raise ValueError("cn and fn must be one byte each.")
|
2016-04-07 12:06:14 +00:00
|
|
|
header = self._int_low_high(len(data) + len(m) + 2, 2)
|
2016-03-19 13:48:29 +00:00
|
|
|
self._raw(GS + b'(k' + header + cn + fn + m + data)
|
2017-01-29 23:10:14 +00:00
|
|
|
|
2016-03-19 13:48:29 +00:00
|
|
|
@staticmethod
|
|
|
|
def _int_low_high(inp_number, out_bytes):
|
|
|
|
""" Generate multiple bytes for a number: In lower and higher parts, or more parts as needed.
|
2017-01-30 00:50:27 +00:00
|
|
|
|
2016-03-19 13:48:29 +00:00
|
|
|
:param inp_number: Input number
|
|
|
|
:param out_bytes: The number of bytes to output (1 - 4).
|
|
|
|
"""
|
2016-04-07 12:06:14 +00:00
|
|
|
max_input = (256 << (out_bytes * 8) - 1)
|
2016-03-19 13:48:29 +00:00
|
|
|
if not 1 <= out_bytes <= 4:
|
2017-07-27 21:06:59 +00:00
|
|
|
raise ValueError("Can only output 1-4 bytes")
|
2016-03-19 13:48:29 +00:00
|
|
|
if not 0 <= inp_number <= max_input:
|
2017-07-27 21:06:59 +00:00
|
|
|
raise ValueError("Number too large. Can only output up to {0} in {1} bytes".format(max_input, out_bytes))
|
2016-04-07 12:06:14 +00:00
|
|
|
outp = b''
|
2016-03-19 13:48:29 +00:00
|
|
|
for _ in range(0, out_bytes):
|
|
|
|
outp += six.int2byte(inp_number % 256)
|
2016-06-19 09:51:52 +00:00
|
|
|
inp_number //= 256
|
2016-03-19 13:48:29 +00:00
|
|
|
return outp
|
2013-03-14 08:31:07 +00:00
|
|
|
|
2016-07-23 20:16:11 +00:00
|
|
|
def charcode(self, code="AUTO"):
|
2015-11-27 22:10:20 +00:00
|
|
|
""" Set Character Code Table
|
|
|
|
|
2016-07-23 20:16:11 +00:00
|
|
|
Sets the control sequence from ``CHARCODE`` in :py:mod:`escpos.constants` as active. It will be sent with
|
|
|
|
the next text sequence. If you set the variable code to ``AUTO`` it will try to automatically guess the
|
|
|
|
right codepage. (This is the standard behaviour.)
|
2013-03-14 08:31:07 +00:00
|
|
|
|
2015-11-27 22:10:20 +00:00
|
|
|
:param code: Name of CharCode
|
2016-01-05 16:30:40 +00:00
|
|
|
:raises: :py:exc:`~escpos.exceptions.CharCodeError`
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2016-07-23 20:16:11 +00:00
|
|
|
if code.upper() == "AUTO":
|
2016-08-27 09:09:08 +00:00
|
|
|
self.magic.force_encoding(False)
|
2014-05-21 05:15:54 +00:00
|
|
|
else:
|
2016-08-27 09:09:08 +00:00
|
|
|
self.magic.force_encoding(code)
|
2014-05-21 05:15:54 +00:00
|
|
|
|
2017-10-08 18:05:18 +00:00
|
|
|
@staticmethod
|
|
|
|
def check_barcode(bc, code):
|
|
|
|
"""
|
|
|
|
This method checks if the barcode is in the proper format.
|
|
|
|
The validation concerns the barcode length and the set of characters, but won't compute/validate any checksum.
|
|
|
|
The full set of requirement for each barcode type is available in the ESC/POS documentation.
|
|
|
|
|
|
|
|
As an example, using EAN13, the barcode `12345678901` will be correct, because it can be rendered by the
|
|
|
|
printer. But it does not suit the EAN13 standard, because the checksum digit is missing. Adding a wrong
|
|
|
|
checksum in the end will also be considered correct, but adding a letter won't (EAN13 is numeric only).
|
|
|
|
|
|
|
|
.. todo:: Add a method to compute the checksum for the different standards
|
|
|
|
|
|
|
|
.. todo:: For fixed-length standards with mandatory checksum (EAN, UPC),
|
|
|
|
compute and add the checksum automatically if missing.
|
|
|
|
|
2019-06-15 23:53:39 +00:00
|
|
|
:param bc: barcode format, see :py:meth:`.barcode()`
|
|
|
|
:param code: alphanumeric data to be printed as bar code, see :py:meth:`.barcode()`
|
2017-10-08 18:05:18 +00:00
|
|
|
:return: bool
|
|
|
|
"""
|
|
|
|
if bc not in BARCODE_FORMATS:
|
|
|
|
return False
|
|
|
|
|
|
|
|
bounds, regex = BARCODE_FORMATS[bc]
|
|
|
|
return any(bound[0] <= len(code) <= bound[1] for bound in bounds) and re_match(regex, code)
|
|
|
|
|
2016-08-30 10:53:31 +00:00
|
|
|
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A",
|
2017-10-08 18:05:18 +00:00
|
|
|
align_ct=True, function_type=None, check=True):
|
2015-11-27 22:10:20 +00:00
|
|
|
""" Print Barcode
|
|
|
|
|
2016-01-14 14:50:58 +00:00
|
|
|
This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to
|
2017-10-08 18:05:18 +00:00
|
|
|
be supported by the unit. By default, this method will check whether your barcode text is correct, that is
|
|
|
|
the characters and lengths are supported by ESCPOS. Call the method with `check=False` to disable the check, but
|
|
|
|
note that uncorrect barcodes may lead to unexpected printer behaviour.
|
|
|
|
There are two forms of the barcode function. Type A is default but has fewer barcodes,
|
|
|
|
while type B has some more to choose from.
|
2016-01-14 14:50:58 +00:00
|
|
|
|
|
|
|
Use the parameters `height` and `width` for adjusting of the barcode size. Please take notice that the barcode
|
|
|
|
will not be printed if it is outside of the printable area. (Which should be impossible with this method, so
|
|
|
|
this information is probably more useful for debugging purposes.)
|
|
|
|
|
2016-01-14 00:00:53 +00:00
|
|
|
.. todo:: On TM-T88II width from 1 to 6 is accepted. Try to acquire command reference and correct the code.
|
2016-01-14 13:04:35 +00:00
|
|
|
.. todo:: Supplying pos does not have an effect for every barcode type. Check and document for which types this
|
|
|
|
is true.
|
2016-01-14 00:00:53 +00:00
|
|
|
|
2016-01-14 14:50:58 +00:00
|
|
|
If you do not want to center the barcode you can call the method with `align_ct=False`, which will disable
|
|
|
|
automatic centering. Please note that when you use center alignment, then the alignment of text will be changed
|
|
|
|
automatically to centered. You have to manually restore the alignment if necessary.
|
|
|
|
|
|
|
|
.. todo:: If further barcode-types are needed they could be rendered transparently as an image. (This could also
|
|
|
|
be of help if the printer does not support types that others do.)
|
2017-01-30 00:50:27 +00:00
|
|
|
|
2016-01-05 16:30:40 +00:00
|
|
|
:param code: alphanumeric data to be printed as bar code
|
2016-03-07 04:06:05 +00:00
|
|
|
:param bc: barcode format, possible values are for type A are:
|
2016-01-05 16:30:40 +00:00
|
|
|
|
|
|
|
* UPC-A
|
|
|
|
* UPC-E
|
|
|
|
* EAN13
|
|
|
|
* EAN8
|
|
|
|
* CODE39
|
|
|
|
* ITF
|
|
|
|
* NW7
|
|
|
|
|
2016-03-07 04:06:05 +00:00
|
|
|
Possible values for type B:
|
|
|
|
|
|
|
|
* All types from function type A
|
|
|
|
* CODE93
|
|
|
|
* CODE128
|
|
|
|
* GS1-128
|
|
|
|
* GS1 DataBar Omnidirectional
|
|
|
|
* GS1 DataBar Truncated
|
|
|
|
* GS1 DataBar Limited
|
|
|
|
* GS1 DataBar Expanded
|
|
|
|
|
2016-01-05 16:30:40 +00:00
|
|
|
If none is specified, the method raises :py:exc:`~escpos.exceptions.BarcodeTypeError`.
|
2016-01-14 00:00:53 +00:00
|
|
|
:param height: barcode height, has to be between 1 and 255
|
2014-05-22 04:45:57 +00:00
|
|
|
*default*: 64
|
2016-01-14 13:51:26 +00:00
|
|
|
:type height: int
|
2016-01-14 00:00:53 +00:00
|
|
|
:param width: barcode width, has to be between 2 and 6
|
|
|
|
*default*: 3
|
2016-01-14 13:51:26 +00:00
|
|
|
:type width: int
|
|
|
|
:param pos: where to place the text relative to the barcode, *default*: BELOW
|
2016-01-05 16:30:40 +00:00
|
|
|
|
|
|
|
* ABOVE
|
|
|
|
* BELOW
|
|
|
|
* BOTH
|
|
|
|
* OFF
|
|
|
|
|
|
|
|
:param font: select font (see ESC/POS-documentation, the device often has two fonts), *default*: A
|
|
|
|
|
|
|
|
* A
|
|
|
|
* B
|
|
|
|
|
2016-01-14 13:51:26 +00:00
|
|
|
:param align_ct: If this parameter is True the barcode will be centered. Otherwise no alignment command will be
|
|
|
|
issued.
|
|
|
|
:type align_ct: bool
|
|
|
|
|
2016-08-30 10:53:31 +00:00
|
|
|
:param function_type: Choose between ESCPOS function type A or B,
|
|
|
|
depending on printer support and desired barcode. If not given,
|
|
|
|
the printer will attempt to automatically choose the correct
|
|
|
|
function based on the current profile.
|
2016-03-07 04:06:05 +00:00
|
|
|
*default*: A
|
|
|
|
|
2017-10-08 18:05:18 +00:00
|
|
|
:param check: If this parameter is True, the barcode format will be checked to ensure it meets the bc
|
2020-05-11 21:39:56 +00:00
|
|
|
requirements as definged in the ESC/POS documentation. See :py:meth:`.check_barcode()`
|
2017-10-08 18:05:18 +00:00
|
|
|
for more information. *default*: True.
|
|
|
|
|
2016-01-05 16:30:40 +00:00
|
|
|
:raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`,
|
|
|
|
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
|
|
|
|
:py:exc:`~escpos.exceptions.BarcodeCodeError`
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2016-08-30 10:53:31 +00:00
|
|
|
if function_type is None:
|
|
|
|
# Choose the function type automatically.
|
|
|
|
if bc in BARCODE_TYPES['A']:
|
|
|
|
function_type = 'A'
|
|
|
|
else:
|
|
|
|
if bc in BARCODE_TYPES['B']:
|
|
|
|
if not self.profile.supports(BARCODE_B):
|
|
|
|
raise BarcodeTypeError((
|
|
|
|
"Barcode type '{bc} not supported for "
|
|
|
|
"the current printer profile").format(bc=bc))
|
|
|
|
function_type = 'B'
|
|
|
|
else:
|
|
|
|
raise BarcodeTypeError((
|
2017-01-30 00:57:36 +00:00
|
|
|
"Barcode type '{bc} is not valid").format(bc=bc))
|
2016-08-30 10:53:31 +00:00
|
|
|
|
|
|
|
bc_types = BARCODE_TYPES[function_type.upper()]
|
|
|
|
if bc.upper() not in bc_types.keys():
|
|
|
|
raise BarcodeTypeError((
|
2017-10-08 18:05:18 +00:00
|
|
|
"Barcode '{bc}' not valid for barcode function type "
|
2016-08-30 10:53:31 +00:00
|
|
|
"{function_type}").format(
|
|
|
|
bc=bc,
|
|
|
|
function_type=function_type,
|
|
|
|
))
|
|
|
|
|
2017-10-08 18:05:18 +00:00
|
|
|
if check and not self.check_barcode(bc, code):
|
|
|
|
raise BarcodeCodeError((
|
|
|
|
"Barcode '{code}' not in a valid format for type '{bc}'").format(
|
|
|
|
code=code,
|
|
|
|
bc=bc,
|
|
|
|
))
|
|
|
|
|
2010-02-26 08:54:46 +00:00
|
|
|
# Align Bar Code()
|
2016-01-14 13:51:26 +00:00
|
|
|
if align_ct:
|
2017-05-14 08:38:21 +00:00
|
|
|
self._raw(TXT_STYLE['align']['center'])
|
2010-02-26 08:54:46 +00:00
|
|
|
# Height
|
2014-05-22 04:45:57 +00:00
|
|
|
if 1 <= height <= 255:
|
2016-03-03 03:38:05 +00:00
|
|
|
self._raw(BARCODE_HEIGHT + six.int2byte(height))
|
2010-02-26 08:54:46 +00:00
|
|
|
else:
|
2016-01-08 02:43:33 +00:00
|
|
|
raise BarcodeSizeError("height = {height}".format(height=height))
|
2010-02-26 08:54:46 +00:00
|
|
|
# Width
|
2014-05-22 04:45:57 +00:00
|
|
|
if 2 <= width <= 6:
|
2016-03-03 03:38:05 +00:00
|
|
|
self._raw(BARCODE_WIDTH + six.int2byte(width))
|
2010-02-26 08:54:46 +00:00
|
|
|
else:
|
2016-01-08 02:43:33 +00:00
|
|
|
raise BarcodeSizeError("width = {width}".format(width=width))
|
2010-02-26 08:54:46 +00:00
|
|
|
# Font
|
|
|
|
if font.upper() == "B":
|
|
|
|
self._raw(BARCODE_FONT_B)
|
2015-11-27 20:20:12 +00:00
|
|
|
else: # DEFAULT FONT: A
|
2010-02-26 08:54:46 +00:00
|
|
|
self._raw(BARCODE_FONT_A)
|
|
|
|
# Position
|
|
|
|
if pos.upper() == "OFF":
|
|
|
|
self._raw(BARCODE_TXT_OFF)
|
|
|
|
elif pos.upper() == "BOTH":
|
|
|
|
self._raw(BARCODE_TXT_BTH)
|
|
|
|
elif pos.upper() == "ABOVE":
|
|
|
|
self._raw(BARCODE_TXT_ABV)
|
2015-06-04 11:57:59 +00:00
|
|
|
else: # DEFAULT POSITION: BELOW
|
2010-02-26 08:54:46 +00:00
|
|
|
self._raw(BARCODE_TXT_BLW)
|
2016-03-07 04:06:05 +00:00
|
|
|
|
|
|
|
self._raw(bc_types[bc.upper()])
|
|
|
|
|
|
|
|
if function_type.upper() == "B":
|
2016-03-16 23:26:13 +00:00
|
|
|
self._raw(six.int2byte(len(code)))
|
2016-03-07 04:06:05 +00:00
|
|
|
|
2010-02-26 08:54:46 +00:00
|
|
|
# Print Code
|
|
|
|
if code:
|
2016-03-03 03:38:05 +00:00
|
|
|
self._raw(code.encode())
|
2010-02-26 08:54:46 +00:00
|
|
|
else:
|
2015-11-27 20:20:12 +00:00
|
|
|
raise BarcodeCodeError()
|
2015-06-04 11:57:59 +00:00
|
|
|
|
2016-03-08 00:20:04 +00:00
|
|
|
if function_type.upper() == "A":
|
2016-03-16 22:23:42 +00:00
|
|
|
self._raw(NUL)
|
2016-03-08 00:20:04 +00:00
|
|
|
|
2017-05-22 18:21:35 +00:00
|
|
|
def soft_barcode(self, barcode_type, data, impl='bitImageColumn',
|
2020-05-10 12:06:00 +00:00
|
|
|
module_height=5, module_width=0.2, text_distance=1,
|
|
|
|
center=True):
|
2017-05-22 18:21:35 +00:00
|
|
|
|
2017-05-16 18:55:30 +00:00
|
|
|
image_writer = ImageWriter()
|
|
|
|
|
2017-05-22 18:21:35 +00:00
|
|
|
# Check if barcode type exists
|
2017-05-16 18:55:30 +00:00
|
|
|
if barcode_type not in barcode.PROVIDED_BARCODES:
|
|
|
|
raise BarcodeTypeError(
|
|
|
|
'Barcode type {} not supported by software barcode renderer'
|
|
|
|
.format(barcode_type))
|
|
|
|
|
2017-05-22 18:21:35 +00:00
|
|
|
# Render the barcode to a fake file
|
2017-05-16 18:55:30 +00:00
|
|
|
barcode_class = barcode.get_barcode_class(barcode_type)
|
|
|
|
my_code = barcode_class(data, writer=image_writer)
|
|
|
|
|
2018-05-14 23:03:07 +00:00
|
|
|
with open(os.devnull, "wb") as nullfile:
|
|
|
|
my_code.write(nullfile, {
|
|
|
|
'module_height': module_height,
|
|
|
|
'module_width': module_width,
|
|
|
|
'text_distance': text_distance
|
|
|
|
})
|
2017-05-16 18:55:30 +00:00
|
|
|
|
2017-05-22 18:21:35 +00:00
|
|
|
# Retrieve the Pillow image and print it
|
2017-05-16 18:55:30 +00:00
|
|
|
image = my_code.writer._image
|
2020-05-10 12:06:00 +00:00
|
|
|
self.image(image, impl=impl, center=center)
|
2017-05-16 18:55:30 +00:00
|
|
|
|
2010-02-26 08:54:46 +00:00
|
|
|
def text(self, txt):
|
2015-11-27 22:10:20 +00:00
|
|
|
""" Print alpha-numeric text
|
|
|
|
|
|
|
|
The text has to be encoded in the currently selected codepage.
|
2016-06-17 21:04:10 +00:00
|
|
|
The input text has to be encoded in unicode.
|
2016-03-03 03:38:05 +00:00
|
|
|
|
2015-11-27 22:10:20 +00:00
|
|
|
:param txt: text to be printed
|
2016-01-05 16:30:40 +00:00
|
|
|
:raises: :py:exc:`~escpos.exceptions.TextError`
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2016-07-23 20:16:11 +00:00
|
|
|
txt = six.text_type(txt)
|
2016-08-27 09:09:08 +00:00
|
|
|
self.magic.write(txt)
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2017-08-01 15:09:24 +00:00
|
|
|
def textln(self, txt=''):
|
|
|
|
"""Print alpha-numeric text with a newline
|
|
|
|
|
|
|
|
The text has to be encoded in the currently selected codepage.
|
|
|
|
The input text has to be encoded in unicode.
|
|
|
|
|
|
|
|
:param txt: text to be printed with a newline
|
|
|
|
:raises: :py:exc:`~escpos.exceptions.TextError`
|
|
|
|
"""
|
|
|
|
self.text('{}\n'.format(txt))
|
|
|
|
|
|
|
|
def ln(self, count=1):
|
|
|
|
"""Print a newline or more
|
|
|
|
|
|
|
|
:param count: number of newlines to print
|
|
|
|
:raises: :py:exc:`ValueError` if count < 0
|
|
|
|
"""
|
|
|
|
if count < 0:
|
|
|
|
raise ValueError('Count cannot be lesser than 0')
|
|
|
|
if count > 0:
|
|
|
|
self.text('\n' * count)
|
|
|
|
|
2016-08-25 15:30:20 +00:00
|
|
|
def block_text(self, txt, font=None, columns=None):
|
2016-01-05 16:30:40 +00:00
|
|
|
""" Text is printed wrapped to specified columns
|
|
|
|
|
2016-06-17 21:04:10 +00:00
|
|
|
Text has to be encoded in unicode.
|
|
|
|
|
2016-01-05 16:30:40 +00:00
|
|
|
:param txt: text to be printed
|
2017-05-13 16:42:05 +00:00
|
|
|
:param font: font to be used, can be :code:`a` or :code:`b`
|
2016-01-05 16:30:40 +00:00
|
|
|
:param columns: amount of columns
|
|
|
|
:return: None
|
|
|
|
"""
|
2016-08-25 15:30:20 +00:00
|
|
|
col_count = self.profile.get_columns(font) if columns is None else columns
|
2016-04-02 13:26:00 +00:00
|
|
|
self.text(textwrap.fill(txt, col_count))
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2017-05-14 08:38:21 +00:00
|
|
|
def set(self, align='left', font='a', bold=False, underline=0, width=1,
|
|
|
|
height=1, density=9, invert=False, smooth=False, flip=False,
|
2017-05-15 16:54:54 +00:00
|
|
|
double_width=False, double_height=False, custom_size=False):
|
2015-11-27 22:10:20 +00:00
|
|
|
""" Set text properties by sending them to the printer
|
|
|
|
|
2016-01-05 16:30:40 +00:00
|
|
|
:param align: horizontal position for text, possible values are:
|
|
|
|
|
2017-05-14 12:41:56 +00:00
|
|
|
* 'center'
|
|
|
|
* 'left'
|
|
|
|
* 'right'
|
2016-01-05 16:30:40 +00:00
|
|
|
|
2017-05-14 12:41:56 +00:00
|
|
|
*default*: 'left'
|
|
|
|
|
2016-08-30 10:26:09 +00:00
|
|
|
:param font: font given as an index, a name, or one of the
|
2017-05-14 12:41:56 +00:00
|
|
|
special values 'a' or 'b', referring to fonts 0 and 1.
|
|
|
|
:param bold: text in bold, *default*: False
|
|
|
|
:param underline: underline mode for text, decimal range 0-2, *default*: 0
|
2017-05-15 16:54:54 +00:00
|
|
|
:param double_height: doubles the height of the text
|
|
|
|
:param double_width: doubles the width of the text
|
|
|
|
:param custom_size: uses custom size specified by width and height
|
|
|
|
parameters. Cannot be used with double_width or double_height.
|
|
|
|
:param width: text width multiplier when custom_size is used, decimal range 1-8, *default*: 1
|
|
|
|
:param height: text height multiplier when custom_size is used, decimal range 1-8, *default*: 1
|
2016-01-05 16:30:40 +00:00
|
|
|
:param density: print density, value from 0-8, if something else is supplied the density remains unchanged
|
2016-02-29 18:04:00 +00:00
|
|
|
:param invert: True enables white on black printing, *default*: False
|
2016-03-01 18:05:52 +00:00
|
|
|
:param smooth: True enables text smoothing. Effective on 4x4 size text and larger, *default*: False
|
|
|
|
:param flip: True enables upside-down printing, *default*: False
|
2017-05-14 12:41:56 +00:00
|
|
|
|
2017-05-15 16:54:54 +00:00
|
|
|
:type font: str
|
2016-02-29 18:04:00 +00:00
|
|
|
:type invert: bool
|
2017-05-14 12:41:56 +00:00
|
|
|
:type bold: bool
|
|
|
|
:type underline: bool
|
|
|
|
:type smooth: bool
|
|
|
|
:type flip: bool
|
2017-05-15 16:54:54 +00:00
|
|
|
:type custom_size: bool
|
|
|
|
:type double_width: bool
|
|
|
|
:type double_height: bool
|
2017-05-14 12:41:56 +00:00
|
|
|
:type align: str
|
|
|
|
:type width: int
|
|
|
|
:type height: int
|
|
|
|
:type density: int
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2017-05-14 08:38:21 +00:00
|
|
|
|
2017-05-15 16:54:54 +00:00
|
|
|
if custom_size:
|
2017-05-14 08:38:21 +00:00
|
|
|
if 1 <= width <= 8 and 1 <= height <= 8 and isinstance(width, int) and\
|
|
|
|
isinstance(height, int):
|
|
|
|
size_byte = TXT_STYLE['width'][width] + TXT_STYLE['height'][height]
|
|
|
|
self._raw(TXT_SIZE + six.int2byte(size_byte))
|
|
|
|
else:
|
|
|
|
raise SetVariableError()
|
2017-05-15 16:54:54 +00:00
|
|
|
else:
|
|
|
|
self._raw(TXT_NORMAL)
|
|
|
|
if double_width and double_height:
|
|
|
|
self._raw(TXT_STYLE['size']['2x'])
|
|
|
|
elif double_width:
|
|
|
|
self._raw(TXT_STYLE['size']['2w'])
|
|
|
|
elif double_height:
|
|
|
|
self._raw(TXT_STYLE['size']['2h'])
|
|
|
|
else:
|
|
|
|
self._raw(TXT_STYLE['size']['normal'])
|
2017-05-14 08:38:21 +00:00
|
|
|
|
|
|
|
self._raw(TXT_STYLE['flip'][flip])
|
|
|
|
self._raw(TXT_STYLE['smooth'][smooth])
|
|
|
|
self._raw(TXT_STYLE['bold'][bold])
|
|
|
|
self._raw(TXT_STYLE['underline'][underline])
|
2016-08-30 10:26:09 +00:00
|
|
|
self._raw(SET_FONT(six.int2byte(self.profile.get_font(font))))
|
2017-05-14 08:38:21 +00:00
|
|
|
self._raw(TXT_STYLE['align'][align])
|
2016-08-30 10:26:09 +00:00
|
|
|
|
2017-05-14 08:38:21 +00:00
|
|
|
if density != 9:
|
|
|
|
self._raw(TXT_STYLE['density'][density])
|
|
|
|
|
|
|
|
self._raw(TXT_STYLE['invert'][invert])
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2016-08-26 08:38:36 +00:00
|
|
|
def line_spacing(self, spacing=None, divisor=180):
|
2016-08-25 13:56:32 +00:00
|
|
|
""" Set line character spacing.
|
|
|
|
|
2016-08-26 08:38:36 +00:00
|
|
|
If no spacing is given, we reset it to the default.
|
|
|
|
|
2016-08-25 13:56:32 +00:00
|
|
|
There are different commands for setting the line spacing, using
|
|
|
|
a different denominator:
|
|
|
|
|
2016-08-26 09:59:40 +00:00
|
|
|
'+'' line_spacing/360 of an inch, 0 <= line_spacing <= 255
|
|
|
|
'3' line_spacing/180 of an inch, 0 <= line_spacing <= 255
|
|
|
|
'A' line_spacing/60 of an inch, 0 <= line_spacing <= 85
|
2016-08-25 13:56:32 +00:00
|
|
|
|
|
|
|
Some printers may not support all of them. The most commonly
|
|
|
|
available command (using a divisor of 180) is chosen.
|
|
|
|
"""
|
2016-08-26 08:38:36 +00:00
|
|
|
if spacing is None:
|
|
|
|
self._raw(LINESPACING_RESET)
|
|
|
|
return
|
|
|
|
|
2016-08-25 13:56:32 +00:00
|
|
|
if divisor not in LINESPACING_FUNCS:
|
|
|
|
raise ValueError("divisor must be either 360, 180 or 60")
|
2017-01-29 23:10:14 +00:00
|
|
|
if (divisor in [360, 180]
|
2016-08-25 13:56:32 +00:00
|
|
|
and (not(0 <= spacing <= 255))):
|
|
|
|
raise ValueError("spacing must be a int between 0 and 255 when divisor is 360 or 180")
|
|
|
|
if divisor == 60 and (not(0 <= spacing <= 85)):
|
|
|
|
raise ValueError("spacing must be a int between 0 and 85 when divisor is 60")
|
|
|
|
|
|
|
|
self._raw(LINESPACING_FUNCS[divisor] + six.int2byte(spacing))
|
|
|
|
|
2017-07-18 16:50:38 +00:00
|
|
|
def cut(self, mode='FULL', feed=True):
|
2016-01-13 15:06:18 +00:00
|
|
|
""" Cut paper.
|
|
|
|
|
|
|
|
Without any arguments the paper will be cut completely. With 'mode=PART' a partial cut will
|
|
|
|
be attempted. Note however, that not all models can do a partial cut. See the documentation of
|
|
|
|
your printer for details.
|
2017-01-30 00:50:27 +00:00
|
|
|
|
2017-02-06 14:48:15 +00:00
|
|
|
:param mode: set to 'PART' for a partial cut. default: 'FULL'
|
2017-07-18 16:50:38 +00:00
|
|
|
:param feed: print and feed before cutting. default: true
|
2017-02-06 14:48:15 +00:00
|
|
|
:raises ValueError: if mode not in ('FULL', 'PART')
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2017-07-23 09:11:22 +00:00
|
|
|
|
|
|
|
if not feed:
|
|
|
|
self._raw(GS + b'V' + six.int2byte(66) + b'\x00')
|
|
|
|
return
|
|
|
|
|
|
|
|
self.print_and_feed(6)
|
2017-02-06 14:48:15 +00:00
|
|
|
|
|
|
|
mode = mode.upper()
|
|
|
|
if mode not in ('FULL', 'PART'):
|
|
|
|
raise ValueError("Mode must be one of ('FULL', 'PART')")
|
|
|
|
|
|
|
|
if mode == "PART":
|
|
|
|
if self.profile.supports('paperPartCut'):
|
|
|
|
self._raw(PAPER_PART_CUT)
|
|
|
|
elif self.profile.supports('paperFullCut'):
|
|
|
|
self._raw(PAPER_FULL_CUT)
|
|
|
|
elif mode == "FULL":
|
|
|
|
if self.profile.supports('paperFullCut'):
|
|
|
|
self._raw(PAPER_FULL_CUT)
|
|
|
|
elif self.profile.supports('paperPartCut'):
|
|
|
|
self._raw(PAPER_PART_CUT)
|
2010-02-26 08:54:46 +00:00
|
|
|
|
|
|
|
def cashdraw(self, pin):
|
2015-11-27 22:10:20 +00:00
|
|
|
""" Send pulse to kick the cash drawer
|
|
|
|
|
2016-09-15 20:54:35 +00:00
|
|
|
Kick cash drawer on pin 2 or pin 5 according to default parameter.
|
|
|
|
For non default parameter send a decimal sequence i.e. [27,112,48] or [27,112,0,25,255]
|
2016-01-05 16:30:40 +00:00
|
|
|
|
2016-09-15 20:54:35 +00:00
|
|
|
:param pin: pin number, 2 or 5 or list of decimals
|
2016-01-05 16:30:40 +00:00
|
|
|
:raises: :py:exc:`~escpos.exceptions.CashDrawerError`
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2010-02-26 08:54:46 +00:00
|
|
|
if pin == 2:
|
|
|
|
self._raw(CD_KICK_2)
|
|
|
|
elif pin == 5:
|
|
|
|
self._raw(CD_KICK_5)
|
|
|
|
else:
|
2016-09-15 20:54:35 +00:00
|
|
|
try:
|
|
|
|
self._raw(CD_KICK_DEC_SEQUENCE(*pin))
|
2017-12-03 22:21:29 +00:00
|
|
|
except TypeError as err:
|
|
|
|
raise CashDrawerError(err)
|
2010-02-26 08:54:46 +00:00
|
|
|
|
2016-11-15 15:24:44 +00:00
|
|
|
def linedisplay_select(self, select_display=False):
|
|
|
|
""" Selects the line display or the printer
|
|
|
|
|
|
|
|
This method is used for line displays that are daisy-chained between your computer and printer.
|
|
|
|
If you set `select_display` to true, only the display is selected and if you set it to false,
|
|
|
|
only the printer is selected.
|
|
|
|
|
|
|
|
:param select_display: whether the display should be selected or the printer
|
|
|
|
:type select_display: bool
|
|
|
|
"""
|
|
|
|
if select_display:
|
|
|
|
self._raw(LINE_DISPLAY_OPEN)
|
|
|
|
else:
|
|
|
|
self._raw(LINE_DISPLAY_CLOSE)
|
|
|
|
|
|
|
|
def linedisplay_clear(self):
|
|
|
|
""" Clears the line display and resets the cursor
|
|
|
|
|
|
|
|
This method is used for line displays that are daisy-chained between your computer and printer.
|
|
|
|
"""
|
|
|
|
self._raw(LINE_DISPLAY_CLEAR)
|
|
|
|
|
|
|
|
def linedisplay(self, text):
|
|
|
|
"""
|
|
|
|
Display text on a line display connected to your printer
|
|
|
|
|
|
|
|
You should connect a line display to your printer. You can do this by daisy-chaining
|
|
|
|
the display between your computer and printer.
|
2018-02-07 21:17:18 +00:00
|
|
|
|
2016-11-15 15:24:44 +00:00
|
|
|
:param text: Text to display
|
|
|
|
"""
|
|
|
|
self.linedisplay_select(select_display=True)
|
|
|
|
self.linedisplay_clear()
|
|
|
|
self.text(text)
|
|
|
|
self.linedisplay_select(select_display=False)
|
|
|
|
|
2010-02-26 08:54:46 +00:00
|
|
|
def hw(self, hw):
|
2015-11-27 22:10:20 +00:00
|
|
|
""" Hardware operations
|
|
|
|
|
2016-01-05 16:30:40 +00:00
|
|
|
:param hw: hardware action, may be:
|
|
|
|
|
|
|
|
* INIT
|
|
|
|
* SELECT
|
|
|
|
* RESET
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2010-02-26 08:54:46 +00:00
|
|
|
if hw.upper() == "INIT":
|
|
|
|
self._raw(HW_INIT)
|
|
|
|
elif hw.upper() == "SELECT":
|
|
|
|
self._raw(HW_SELECT)
|
|
|
|
elif hw.upper() == "RESET":
|
|
|
|
self._raw(HW_RESET)
|
2015-11-27 20:20:12 +00:00
|
|
|
else: # DEFAULT: DOES NOTHING
|
2010-02-26 08:54:46 +00:00
|
|
|
pass
|
|
|
|
|
2017-02-09 14:11:19 +00:00
|
|
|
def print_and_feed(self, n=1):
|
2017-02-06 14:48:15 +00:00
|
|
|
""" Print data in print buffer and feed *n* lines
|
|
|
|
|
|
|
|
if n not in range (0, 255) then ValueError will be raised
|
|
|
|
|
2017-02-09 14:11:19 +00:00
|
|
|
:param n: number of n to feed. 0 <= n <= 255. default: 1
|
2017-02-06 14:48:15 +00:00
|
|
|
:raises ValueError: if not 0 <= n <= 255
|
|
|
|
"""
|
|
|
|
if 0 <= n <= 255:
|
|
|
|
# ESC d n
|
|
|
|
self._raw(ESC + b"d" + six.int2byte(n))
|
|
|
|
else:
|
|
|
|
raise ValueError("n must be betwen 0 and 255")
|
|
|
|
|
2017-07-26 08:01:08 +00:00
|
|
|
def control(self, ctl, count=5, tab_size=8):
|
2015-11-27 22:10:20 +00:00
|
|
|
""" Feed control sequences
|
|
|
|
|
2016-01-05 16:30:40 +00:00
|
|
|
:param ctl: string for the following control sequences:
|
|
|
|
|
|
|
|
* LF *for Line Feed*
|
|
|
|
* FF *for Form Feed*
|
|
|
|
* CR *for Carriage Return*
|
|
|
|
* HT *for Horizontal Tab*
|
|
|
|
* VT *for Vertical Tab*
|
|
|
|
|
2017-07-26 08:01:08 +00:00
|
|
|
:param count: integer between 1 and 32, controls the horizontal tab count. Defaults to 5.
|
|
|
|
:param tab_size: integer between 1 and 255, controls the horizontal tab size in characters. Defaults to 8
|
2016-01-05 16:30:40 +00:00
|
|
|
:raises: :py:exc:`~escpos.exceptions.TabPosError`
|
2015-11-27 22:10:20 +00:00
|
|
|
"""
|
2014-05-21 05:15:54 +00:00
|
|
|
# Set position
|
2010-02-26 08:54:46 +00:00
|
|
|
if ctl.upper() == "LF":
|
|
|
|
self._raw(CTL_LF)
|
|
|
|
elif ctl.upper() == "FF":
|
|
|
|
self._raw(CTL_FF)
|
|
|
|
elif ctl.upper() == "CR":
|
|
|
|
self._raw(CTL_CR)
|
|
|
|
elif ctl.upper() == "HT":
|
2017-07-26 08:01:08 +00:00
|
|
|
if not (0 <= count <= 32 and
|
|
|
|
1 <= tab_size <= 255 and
|
|
|
|
count * tab_size < 256):
|
2017-02-09 14:11:19 +00:00
|
|
|
raise TabPosError()
|
|
|
|
else:
|
|
|
|
# Set tab positions
|
2017-07-26 08:01:08 +00:00
|
|
|
self._raw(CTL_SET_HT)
|
|
|
|
for iterator in range(1, count):
|
|
|
|
self._raw(six.int2byte(iterator * tab_size))
|
|
|
|
self._raw(NUL)
|
2010-02-26 08:54:46 +00:00
|
|
|
elif ctl.upper() == "VT":
|
2012-11-15 17:20:16 +00:00
|
|
|
self._raw(CTL_VT)
|
2016-03-10 15:01:35 +00:00
|
|
|
|
|
|
|
def panel_buttons(self, enable=True):
|
|
|
|
""" Controls the panel buttons on the printer (e.g. FEED)
|
|
|
|
|
|
|
|
When enable is set to False the panel buttons on the printer will be disabled. Calling the method with
|
|
|
|
enable=True or without argument will enable the panel buttons.
|
|
|
|
|
|
|
|
If panel buttons are enabled, the function of the panel button, such as feeding, will be executed upon pressing
|
|
|
|
the button. If the panel buttons are disabled, pressing them will not have any effect.
|
|
|
|
|
|
|
|
This command is effective until the printer is initialized, reset or power-cycled. The default is enabled panel
|
|
|
|
buttons.
|
|
|
|
|
|
|
|
Some panel buttons will always work, especially when printer is opened. See for more information the manual
|
|
|
|
of your printer and the escpos-command-reference.
|
|
|
|
|
|
|
|
:param enable: controls the panel buttons
|
|
|
|
:rtype: None
|
|
|
|
"""
|
|
|
|
if enable:
|
|
|
|
self._raw(PANEL_BUTTON_ON)
|
|
|
|
else:
|
|
|
|
self._raw(PANEL_BUTTON_OFF)
|
2016-03-28 13:54:43 +00:00
|
|
|
|
2017-07-27 21:05:50 +00:00
|
|
|
def query_status(self, mode):
|
2018-05-02 07:25:04 +00:00
|
|
|
"""
|
|
|
|
Queries the printer for its status, and returns an array of integers containing it.
|
|
|
|
|
2017-07-27 21:05:50 +00:00
|
|
|
:param mode: Integer that sets the status mode queried to the printer.
|
2018-05-02 07:25:04 +00:00
|
|
|
- RT_STATUS_ONLINE: Printer status.
|
|
|
|
- RT_STATUS_PAPER: Paper sensor.
|
|
|
|
:rtype: array(integer)
|
|
|
|
"""
|
2017-07-27 21:05:50 +00:00
|
|
|
self._raw(mode)
|
2017-07-24 11:57:02 +00:00
|
|
|
time.sleep(1)
|
|
|
|
status = self._read()
|
2017-07-27 21:05:50 +00:00
|
|
|
return status
|
2017-07-24 11:57:02 +00:00
|
|
|
|
|
|
|
def is_online(self):
|
2018-05-02 07:25:04 +00:00
|
|
|
"""
|
|
|
|
Queries the online status of the printer.
|
|
|
|
|
|
|
|
:returns: When online, returns ``True``; ``False`` otherwise.
|
|
|
|
:rtype: bool
|
|
|
|
"""
|
2017-07-27 21:05:50 +00:00
|
|
|
status = self.query_status(RT_STATUS_ONLINE)
|
|
|
|
if len(status) == 0:
|
|
|
|
return False
|
2018-05-02 07:25:04 +00:00
|
|
|
return not (status[0] & RT_MASK_ONLINE)
|
2017-07-27 21:05:50 +00:00
|
|
|
|
|
|
|
def paper_status(self):
|
2018-05-02 07:25:04 +00:00
|
|
|
"""
|
|
|
|
Queries the paper status of the printer.
|
|
|
|
|
2017-07-27 21:05:50 +00:00
|
|
|
Returns 2 if there is plenty of paper, 1 if the paper has arrived to
|
|
|
|
the near-end sensor and 0 if there is no paper.
|
2018-05-02 07:25:04 +00:00
|
|
|
|
|
|
|
:returns: 2: Paper is adequate. 1: Paper ending. 0: No paper.
|
|
|
|
:rtype: int
|
|
|
|
"""
|
2017-07-27 21:05:50 +00:00
|
|
|
status = self.query_status(RT_STATUS_PAPER)
|
|
|
|
if len(status) == 0:
|
|
|
|
return 2
|
|
|
|
if (status[0] & RT_MASK_NOPAPER == RT_MASK_NOPAPER):
|
|
|
|
return 0
|
|
|
|
if (status[0] & RT_MASK_LOWPAPER == RT_MASK_LOWPAPER):
|
|
|
|
return 1
|
|
|
|
if (status[0] & RT_MASK_PAPER == RT_MASK_PAPER):
|
|
|
|
return 2
|
2017-07-24 11:57:02 +00:00
|
|
|
|
2016-03-28 13:54:43 +00:00
|
|
|
|
|
|
|
class EscposIO(object):
|
|
|
|
"""ESC/POS Printer IO object
|
|
|
|
|
|
|
|
Allows the class to be used together with the `with`-statement. You have to define a printer instance
|
2018-05-02 07:25:04 +00:00
|
|
|
and assign it to the EscposIO class.
|
2016-03-28 13:54:43 +00:00
|
|
|
This example explains the usage:
|
|
|
|
|
|
|
|
.. code-block:: Python
|
|
|
|
|
|
|
|
with EscposIO(printer.Serial('/dev/ttyUSB0')) as p:
|
|
|
|
p.set(font='a', height=2, align='center', text_type='bold')
|
|
|
|
p.printer.set(align='left')
|
|
|
|
p.printer.image('logo.gif')
|
|
|
|
p.writelines('Big line\\n', font='b')
|
|
|
|
p.writelines('Привет')
|
|
|
|
p.writelines('BIG TEXT', width=2)
|
|
|
|
|
|
|
|
After the `with`-statement the printer automatically cuts the paper if `autocut` is `True`.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, printer, autocut=True, autoclose=True, **kwargs):
|
|
|
|
"""
|
|
|
|
:param printer: An EscPos-printer object
|
|
|
|
:type printer: escpos.Escpos
|
|
|
|
:param autocut: If True, paper is automatically cut after the `with`-statement *default*: True
|
|
|
|
:param kwargs: These arguments will be passed to :py:meth:`escpos.Escpos.set()`
|
|
|
|
"""
|
|
|
|
self.printer = printer
|
|
|
|
self.params = kwargs
|
|
|
|
self.autocut = autocut
|
|
|
|
self.autoclose = autoclose
|
|
|
|
|
|
|
|
def set(self, **kwargs):
|
|
|
|
""" Set the printer-parameters
|
|
|
|
|
|
|
|
Controls which parameters will be passed to :py:meth:`Escpos.set() <escpos.escpos.Escpos.set()>`.
|
|
|
|
For more information on the parameters see the :py:meth:`set() <escpos.escpos.Escpos.set()>`-methods
|
|
|
|
documentation. These parameters can also be passed with this class' constructor or the
|
|
|
|
:py:meth:`~escpos.escpos.EscposIO.writelines()`-method.
|
|
|
|
|
|
|
|
:param kwargs: keyword-parameters that will be passed to :py:meth:`Escpos.set() <escpos.escpos.Escpos.set()>`
|
|
|
|
"""
|
|
|
|
self.params.update(kwargs)
|
|
|
|
|
|
|
|
def writelines(self, text, **kwargs):
|
|
|
|
params = dict(self.params)
|
|
|
|
params.update(kwargs)
|
|
|
|
|
|
|
|
if isinstance(text, six.text_type):
|
|
|
|
lines = text.split('\n')
|
|
|
|
elif isinstance(text, list) or isinstance(text, tuple):
|
|
|
|
lines = text
|
|
|
|
else:
|
|
|
|
lines = ["{0}".format(text), ]
|
|
|
|
|
|
|
|
# TODO check unicode handling
|
|
|
|
# TODO flush? or on print? (this should prob rather be handled by the _raw-method)
|
|
|
|
for line in lines:
|
|
|
|
self.printer.set(**params)
|
|
|
|
if isinstance(text, six.text_type):
|
|
|
|
self.printer.text(u"{0}\n".format(line))
|
|
|
|
else:
|
|
|
|
self.printer.text("{0}\n".format(line))
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
""" called upon closing the `with`-statement
|
|
|
|
"""
|
|
|
|
self.printer.close()
|
|
|
|
|
|
|
|
def __enter__(self, **kwargs):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
|
|
"""
|
|
|
|
|
|
|
|
If :py:attr:`autocut <escpos.escpos.EscposIO.autocut>` is `True` (set by this class' constructor),
|
|
|
|
then :py:meth:`printer.cut() <escpos.escpos.Escpos.cut()>` will be called here.
|
|
|
|
"""
|
|
|
|
if not (type is not None and issubclass(type, Exception)):
|
|
|
|
if self.autocut:
|
|
|
|
self.printer.cut()
|
|
|
|
|
|
|
|
if self.autoclose:
|
|
|
|
self.close()
|