New high level barcode method. Closes #245, #244. (#527)

* Merge software and hardware barcodes to one method

* Fix wrong sw barcode heigh/width

* Add missing param to _sw_barcode call

* Make barcode() smarter, improvements and clean up

* Use param font_size in sw_barcode()

* Update docstrings

* Update barcode examples and docs

* Add --force_software option to CLI

* Attempt to match the sw and hw barcode sizes

* Better approximation to native font size

* Fix docs build

* Update tests at test_function_softbarcode

* Fix exception

* Move image dpi setting to writter_options

* Fix _sw_barcode() docstring param

* Fix wrong default param in docstring

* improve linkage in documentation

---------

Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
Co-authored-by: Patrick Kanzler <dev@pkanzler.de>
This commit is contained in:
Benito López 2023-07-12 20:45:41 +02:00 committed by GitHub
parent 676d2840de
commit 3c11c1b9ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 265 additions and 63 deletions

View File

@ -1,17 +1,18 @@
Printing Barcodes
-----------------
:Last Reviewed: 2016-07-31
:Last Reviewed: 2023-05-16
Most ESC/POS-printers implement barcode-printing.
The barcode-commandset is implemented in the barcode-method.
For a list of compatible barcodes you should check the manual of your printer.
As a rule of thumb: even older Epson-models support most 1D-barcodes.
To be sure just try some implementations and have a look at the notices below.
Many printers implement barcode printing natively.
This hardware renderered barcodes are fast but the supported formats are limited by the printer itself and different between models.
However, almost all printers support printing images, so barcode renderization can be performed externally by software and then sent to the printer as an image.
As a drawback, this operation is much slower and the user needs to know and choose the image implementation method supported by the printer's commandset.
barcode-method
~~~~~~~~~~~~~~
The barcode-method is rather low-level and orients itself on the implementation of ESC/POS.
In the future this class could be supplemented by a high-level class that helps the user generating the payload.
Since version 3.0, the ``barcode`` method unifies the previous ``barcode`` (hardware) and ``soft_barcode`` (software) methods.
It is able to choose automatically the best printer implementation for barcode printing based on the capabilities of the printer and the type of barcode desired.
To achieve this, it relies on the information contained in the escpos-printer-db profiles.
The chosen profile needs to match the capabilities of the printer as closely as possible.
.. py:currentmodule:: escpos.escpos

View File

@ -2,10 +2,10 @@ from escpos.printer import Usb
# Adapt to your needs
p = Usb(0x0416, 0x5011, profile="POS-5890")
p = Usb(0x0416, 0x5011, profile="TM-T88II")
# Print software and then hardware barcode with the same content
p.soft_barcode("code39", "123456")
p.barcode("123456", "CODE39", width=2, force_software=True)
p.text("\n")
p.text("\n")
p.barcode("123456", "CODE39")

View File

@ -5,5 +5,5 @@ from escpos.printer import Usb
p = Usb(0x0416, 0x5011, profile="POS-5890")
# Some software barcodes
p.soft_barcode("code128", "Hello")
p.soft_barcode("code39", "1234")
p.barcode("Hello", "code128", width=2, force_software="bitImageRaster")
p.barcode("1234", "code39", width=2, force_software=True)

View File

@ -161,6 +161,11 @@ ESCPOS_COMMANDS = [
"help": "ESCPOS function type",
"choices": ["A", "B"],
},
{
"option_strings": ("--force_software",),
"help": "Force render and print barcode as an image",
"choices": ["graphics", "bitImageColumn", "bitImageRaster"],
},
],
},
{

View File

@ -27,12 +27,12 @@ FS = b"\x1c"
GS = b"\x1d"
# Feed control sequences
CTL_LF = b"\n" # Print and line feed
CTL_FF = b"\f" # Form feed
CTL_CR = b"\r" # Carriage return
CTL_HT = b"\t" # Horizontal tab
CTL_SET_HT = ESC + b"\x44" # Set horizontal tab positions
CTL_VT = b"\v" # Vertical tab
CTL_LF = b"\n" #: Print and line feed
CTL_FF = b"\f" #: Form feed
CTL_CR = b"\r" #: Carriage return
CTL_HT = b"\t" #: Horizontal tab
CTL_SET_HT = ESC + b"\x44" #: Set horizontal tab positions
CTL_VT = b"\v" #: Vertical tab
# Printer hardware
HW_INIT = ESC + b"@" # Clear data in buffer and reset modes
@ -57,8 +57,8 @@ CD_KICK_5 = _CASH_DRAWER(b"\x01", 50, 50) # Sends a pulse to pin 5 []
# Paper Cutter
_CUT_PAPER = lambda m: GS + b"V" + m
PAPER_FULL_CUT = _CUT_PAPER(b"\x00") # Full cut paper
PAPER_PART_CUT = _CUT_PAPER(b"\x01") # Partial cut paper
PAPER_FULL_CUT = _CUT_PAPER(b"\x00") #: Full cut paper
PAPER_PART_CUT = _CUT_PAPER(b"\x01") #: Partial cut paper
# Beep (please note that the actual beep sequence may differ between devices)
BEEP = b"\x07"
@ -168,8 +168,8 @@ TXT_STYLE = {
# Fonts
SET_FONT = lambda n: ESC + b"\x4d" + n
TXT_FONT_A = SET_FONT(b"\x00") # Font type A
TXT_FONT_B = SET_FONT(b"\x01") # Font type B
TXT_FONT_A = SET_FONT(b"\x00") #: Font type A
TXT_FONT_B = SET_FONT(b"\x01") #: Font type B
# Spacing
LINESPACING_RESET = ESC + b"2"
@ -179,23 +179,23 @@ LINESPACING_FUNCS = {
180: ESC + b"3", # line_spacing/180 of an inch, 0 <= line_spacing <= 255
}
# Prefix to change the codepage. You need to attach a byte to indicate
# the codepage to use. We use escpos-printer-db as the data source.
#: Prefix to change the codepage. You need to attach a byte to indicate
#: the codepage to use. We use escpos-printer-db as the data source.
CODEPAGE_CHANGE = ESC + b"\x74"
# Barcode format
_SET_BARCODE_TXT_POS = lambda n: GS + b"H" + n
BARCODE_TXT_OFF = _SET_BARCODE_TXT_POS(b"\x00") # HRI barcode chars OFF
BARCODE_TXT_ABV = _SET_BARCODE_TXT_POS(b"\x01") # HRI barcode chars above
BARCODE_TXT_BLW = _SET_BARCODE_TXT_POS(b"\x02") # HRI barcode chars below
BARCODE_TXT_BTH = _SET_BARCODE_TXT_POS(b"\x03") # HRI both above and below
BARCODE_TXT_OFF = _SET_BARCODE_TXT_POS(b"\x00") #: HRI barcode chars OFF
BARCODE_TXT_ABV = _SET_BARCODE_TXT_POS(b"\x01") #: HRI barcode chars above
BARCODE_TXT_BLW = _SET_BARCODE_TXT_POS(b"\x02") #: HRI barcode chars below
BARCODE_TXT_BTH = _SET_BARCODE_TXT_POS(b"\x03") #: HRI both above and below
_SET_HRI_FONT = lambda n: GS + b"f" + n
BARCODE_FONT_A = _SET_HRI_FONT(b"\x00") # Font type A for HRI barcode chars
BARCODE_FONT_B = _SET_HRI_FONT(b"\x01") # Font type B for HRI barcode chars
BARCODE_FONT_A = _SET_HRI_FONT(b"\x00") #: Font type A for HRI barcode chars
BARCODE_FONT_B = _SET_HRI_FONT(b"\x01") #: Font type B for HRI barcode chars
BARCODE_HEIGHT = GS + b"h" # Barcode Height [1-255]
BARCODE_WIDTH = GS + b"w" # Barcode Width [2-6]
BARCODE_HEIGHT = GS + b"h" #: Barcode Height [1-255]
BARCODE_WIDTH = GS + b"w" #: Barcode Width [2-6]
# NOTE: This isn't actually an ESC/POS command. It's the common prefix to the
# two "print bar code" commands:
@ -204,7 +204,7 @@ BARCODE_WIDTH = GS + b"w" # Barcode Width [2-6]
# The latter command supports more barcode types
_SET_BARCODE_TYPE = lambda m: GS + b"k" + six.int2byte(m)
# Barcodes for printing function type A
#: Barcodes for printing function type A
BARCODE_TYPE_A = {
"UPC-A": _SET_BARCODE_TYPE(0),
"UPC-E": _SET_BARCODE_TYPE(1),
@ -216,8 +216,8 @@ BARCODE_TYPE_A = {
"CODABAR": _SET_BARCODE_TYPE(6), # Same as NW7
}
# Barcodes for printing function type B
# The first 8 are the same barcodes as type A
#: Barcodes for printing function type B
#: The first 8 are the same barcodes as type A
BARCODE_TYPE_B = {
"UPC-A": _SET_BARCODE_TYPE(65),
"UPC-E": _SET_BARCODE_TYPE(66),
@ -236,6 +236,7 @@ BARCODE_TYPE_B = {
"GS1 DATABAR EXPANDED": _SET_BARCODE_TYPE(78),
}
#: supported barcode formats
BARCODE_FORMATS = {
"UPC-A": ([(11, 12)], "^[0-9]{11,12}$"),
"UPC-E": ([(7, 8), (11, 12)], "^([0-9]{7,8}|[0-9]{11,12})$"),

View File

@ -87,6 +87,19 @@ from escpos.image import EscposImage
from escpos.capabilities import get_profile, BARCODE_B
# Remove special characters and whitespaces of the supported barcode names,
# convert to uppercase and map them to their original names.
HW_BARCODE_NAMES = {
"".join([char for char in name.upper() if char.isalnum()]): name
for bc_type in BARCODE_TYPES.values()
for name in bc_type
}
SW_BARCODE_NAMES = {
"".join([char for char in name.upper() if char.isalnum()]): name
for name in barcode.PROVIDED_BARCODES
}
@six.add_metaclass(ABCMeta)
class Escpos(object):
"""ESC/POS Printer object
@ -411,6 +424,24 @@ class Escpos(object):
regex, code
)
def _dpi(self) -> int:
"""Printer's DPI resolution."""
try:
dpi = int(self.profile.profile_data["media"]["dpi"])
except (KeyError, TypeError):
# Calculate the printer's DPI from the width info of the profile.
try:
px = self.profile.profile_data["media"]["width"]["pixels"]
mm = self.profile.profile_data["media"]["width"]["mm"]
mm -= 10 # paper width minus margin =~ printable area
dpi = int(px / (mm / 25.4))
except (KeyError, TypeError, ZeroDivisionError):
# Value on error.
dpi = 180
print(f"No printer's DPI info was found: Defaulting to {dpi}.")
self.profile.profile_data["media"]["dpi"] = dpi
return dpi
def barcode(
self,
code,
@ -422,6 +453,128 @@ class Escpos(object):
align_ct=True,
function_type=None,
check=True,
force_software=False,
):
"""Print barcode.
Automatic hardware|software barcode renderer according to the printer capabilities.
Defaults to hardware barcode and its format types if supported.
Automatically switches to software barcode renderer if hardware does not
support a barcode type that is supported by software. (e.g. JAN, ISSN, etc.).
Set force_software=True to force the software renderer according to the profile.
Set force_software=graphics|bitImageColumn|bitImageRaster to specify a renderer.
Ignores caps, special chars and whitespaces in barcode type names.
So "EAN13", "ean-13", "Ean_13", "EAN 13" are all accepted.
:param code: alphanumeric data to be printed as bar code (payload).
:param bc: barcode format type (EAN13, CODE128, JAN, etc.).
:param height: barcode module height (in printer dots), has to be between 1 and 255.
*default*: 64
:type height: int
:param width: barcode module width (in printer dots), has to be between 2 and 6.
*default*: 3
:type width: int
:param pos: text position (ABOVE, BELOW, BOTH, OFF) relative to the barcode
(ignored in software renderer).
*default*: BELOW
:param font: select font A or B (ignored in software renderer).
*default*: A
:param align_ct: If *True*, center the barcode.
*default*: True
:type align_ct: bool
:param function_type: ESCPOS function type A or B. None to guess it from profile
(ignored in software renderer).
*default*: None
:param check: If *True*, checks that the code meets the requirements of the barcode type.
*default*: True
:type check: bool
:param force_software: If *True*, force the use of software barcode renderer from profile.
If *"graphics", "bitImageColumn" or "bitImageRaster"*, force the use of specific renderer.
:type force_software: bool | str
:raises: :py:exc:`~escpos.exceptions.BarcodeCodeError`,
:py:exc:`~escpos.exceptions.BarcodeTypeError`
.. note::
Get all supported formats at:
- Hardware: :py:const:`~escpos.constants.BARCODE_FORMATS`
- Software: `Python barcode documentation <https://python-barcode.readthedocs.io/en/stable/supported-formats.html>`_
"""
hw_modes = ["barcodeA", "barcodeB"]
sw_modes = ["graphics", "bitImageColumn", "bitImageRaster"]
capable = {
"hw": [mode for mode in hw_modes if self.profile.supports(mode)] or None,
"sw": [mode for mode in sw_modes if self.profile.supports(mode)] or None,
}
if (not capable["hw"] and not capable["sw"]) or (
not capable["sw"] and force_software
):
raise BarcodeTypeError(
f"""Profile {
self.profile.profile_data['name']
} - hw barcode: {capable['hw']}, sw barcode: {capable['sw']}"""
)
bc_alnum = "".join([char for char in bc.upper() if char.isalnum()])
capable_bc = {
"hw": HW_BARCODE_NAMES.get(bc_alnum),
"sw": SW_BARCODE_NAMES.get(bc_alnum),
}
if not any([*capable_bc.values()]):
raise BarcodeTypeError(f"Not supported or wrong barcode name {bc}.")
if force_software or not capable["hw"] or not capable_bc["hw"]:
# Select the best possible capable render mode
impl = capable["sw"][0]
if force_software in capable["sw"]:
# Force to a specific mode
impl = force_software
print(f"Using {impl} software barcode renderer")
# Set barcode type
bc = capable_bc["sw"] or bc
# Get mm per point of the printer
mmxpt = 25.4 / self._dpi()
self._sw_barcode(
bc,
code,
impl=impl,
module_height=height * mmxpt,
module_width=width * mmxpt,
text_distance=3, # TODO: _hw_barcode() size equivalence
font_size=9, # TODO: _hw_barcode() size equivalence
center=align_ct,
)
return
print("Using hardware barcode renderer")
bc = capable_bc["hw"] or bc
self._hw_barcode(
code, bc, height, width, pos, font, align_ct, function_type, check
)
def _hw_barcode(
self,
code,
bc,
height=64,
width=3,
pos="BELOW",
font="A",
align_ct=True,
function_type=None,
check=True,
):
"""Print Barcode
@ -444,9 +597,6 @@ class Escpos(object):
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.)
:param code: alphanumeric data to be printed as bar code
:param bc: barcode format, possible values are for type A are:
@ -506,27 +656,12 @@ class Escpos(object):
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
:py:exc:`~escpos.exceptions.BarcodeCodeError`
"""
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(
("Barcode type '{bc} is not valid").format(bc=bc)
)
# If function_type is specified, otherwise use guessing.
ft_guess = [ft for ft in ["A", "B"] if bc in BARCODE_TYPES.get(ft)]
ft_guess = ft_guess or [None]
function_type = function_type or ft_guess[0]
bc_types = BARCODE_TYPES[function_type.upper()]
if bc.upper() not in bc_types.keys():
if not function_type or not BARCODE_TYPES.get(function_type.upper()):
raise BarcodeTypeError(
(
"Barcode '{bc}' not valid for barcode function type "
@ -536,6 +671,7 @@ class Escpos(object):
function_type=function_type,
)
)
bc_types = BARCODE_TYPES[function_type.upper()]
if check and not self.check_barcode(bc, code):
raise BarcodeCodeError(
@ -587,16 +723,71 @@ class Escpos(object):
if function_type.upper() == "A":
self._raw(NUL)
def soft_barcode(
def _sw_barcode(
self,
barcode_type,
data,
impl="bitImageColumn",
module_height=5,
module_width=0.2,
text_distance=1,
text_distance=5,
font_size=10,
center=True,
):
"""Print Barcode
This method allows to print barcodes. The rendering of the barcode is done by
the `barcode` library and sent to the printer as image through one of the
printer's supported implementations: graphics, bitImageColumn or bitImageRaster.
:param barcode_type: barcode format, possible values are:
* ean8
* ean8-guard
* ean13
* ean13-guard
* ean
* gtin
* ean14
* jan
* upc
* upca
* isbn
* isbn13
* gs1
* isbn10
* issn
* code39
* pzn
* code128
* itf
* gs1_128
* codabar
* nw-7
:type data: str
:param data: alphanumeric data to be printed as bar code (payload).
:type data: str
:param impl: image printing mode:
* graphics
* bitImageColumn
* bitImageRaster
:param module_height: barcode module height (in mm).
:type module_height: int | float
:param module_width: barcode module width (in mm).
:type module_width: int | float
:param text_distance: distance from the barcode to the code text (in mm).
:type text_distance: int | float
:param font_size: font size of the code text (in dots).
:type font_size: int
:param center: center the barcode.
:type center: bool
"""
image_writer = ImageWriter()
# Check if barcode type exists
@ -610,11 +801,15 @@ class Escpos(object):
# Render the barcode
barcode_class = barcode.get_barcode_class(barcode_type)
my_code = barcode_class(data, writer=image_writer)
my_code.render(
writer_options={
"module_height": module_height,
"module_width": module_width,
"quiet_zone": 0, # horizontal padding
"text_distance": text_distance,
"font_size": font_size,
"dpi": self._dpi(), # Image dpi has to match the printer's dpi
}
)

View File

@ -13,13 +13,13 @@ def instance():
def test_soft_barcode_ean8_invalid(instance):
"""test with an invalid barcode"""
with pytest.raises(barcode.errors.BarcodeError):
instance.soft_barcode("ean8", "1234")
instance.barcode("1234", "ean8", force_software=True)
def test_soft_barcode_ean8(instance):
"""test with a valid ean8 barcode"""
instance.soft_barcode("ean8", "1234567")
instance.barcode("1234567", "ean8", force_software=True)
def test_soft_barcode_ean8_nocenter(instance):
instance.soft_barcode("ean8", "1234567", center=False)
instance.barcode("1234567", "ean8", align_ct=False, force_software=True)