Primer commit
This commit is contained in:
15
venv/lib/python3.11/site-packages/escpos/__init__.py
Normal file
15
venv/lib/python3.11/site-packages/escpos/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
python-escpos enables you to manipulate escpos-printers
|
||||
"""
|
||||
|
||||
__all__ = ["constants", "escpos", "exceptions", "printer"]
|
||||
|
||||
try:
|
||||
from .version import version as __version__ # noqa
|
||||
except ImportError: # pragma: no cover
|
||||
raise ImportError(
|
||||
'Failed to find (autogenerated) version.py. '
|
||||
'This might be because you are installing from GitHub\'s tarballs, '
|
||||
'use the PyPI ones.'
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2136
venv/lib/python3.11/site-packages/escpos/capabilities.json
Normal file
2136
venv/lib/python3.11/site-packages/escpos/capabilities.json
Normal file
File diff suppressed because it is too large
Load Diff
146
venv/lib/python3.11/site-packages/escpos/capabilities.py
Normal file
146
venv/lib/python3.11/site-packages/escpos/capabilities.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import re
|
||||
from os import environ, path
|
||||
import pkg_resources
|
||||
import pickle
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
import six
|
||||
import yaml
|
||||
from tempfile import gettempdir
|
||||
import platform
|
||||
|
||||
pickle_dir = environ.get('ESCPOS_CAPABILITIES_PICKLE_DIR', gettempdir())
|
||||
pickle_path = path.join(pickle_dir, '{v}.capabilities.pickle'.format(v=platform.python_version()))
|
||||
# get a temporary file from pkg_resources if no file is specified in env
|
||||
capabilities_path = environ.get('ESCPOS_CAPABILITIES_FILE',
|
||||
pkg_resources.resource_filename(__name__, 'capabilities.json'))
|
||||
|
||||
# Load external printer database
|
||||
t0 = time.time()
|
||||
if path.exists(pickle_path):
|
||||
if path.getmtime(capabilities_path) > path.getmtime(pickle_path):
|
||||
full_load = True
|
||||
else:
|
||||
full_load = False
|
||||
with open(pickle_path, 'rb') as cf:
|
||||
CAPABILITIES = pickle.load(cf)
|
||||
else:
|
||||
full_load = True
|
||||
|
||||
if full_load:
|
||||
with open(capabilities_path) as cp, open(pickle_path, 'wb') as pp:
|
||||
CAPABILITIES = yaml.safe_load(cp)
|
||||
pickle.dump(CAPABILITIES, pp, protocol=2)
|
||||
|
||||
|
||||
if os.name == 'nt':
|
||||
file_ = os.path.join(os.path.normpath(os.path.dirname(__file__)), 'capabilities_win.json')
|
||||
with open(file_) as f:
|
||||
CAPABILITIES = json.load(f)
|
||||
|
||||
PROFILES = CAPABILITIES['profiles']
|
||||
|
||||
|
||||
class NotSupported(Exception):
|
||||
"""Raised if a requested feature is not supported by the
|
||||
printer profile.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
BARCODE_B = 'barcodeB'
|
||||
|
||||
|
||||
class BaseProfile(object):
|
||||
"""This represents a printer profile.
|
||||
|
||||
A printer profile knows about the number of columns, supported
|
||||
features, colors and more.
|
||||
"""
|
||||
|
||||
profile_data = {}
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self.profile_data[name]
|
||||
|
||||
def get_font(self, font):
|
||||
"""Return the escpos index for `font`. Makes sure that
|
||||
the requested `font` is valid.
|
||||
"""
|
||||
font = {'a': 0, 'b': 1}.get(font, font)
|
||||
if not six.text_type(font) in self.fonts:
|
||||
raise NotSupported(
|
||||
'"{}" is not a valid font in the current profile'.format(font))
|
||||
return font
|
||||
|
||||
def get_columns(self, font):
|
||||
""" Return the number of columns for the given font.
|
||||
"""
|
||||
font = self.get_font(font)
|
||||
return self.fonts[six.text_type(font)]['columns']
|
||||
|
||||
def supports(self, feature):
|
||||
"""Return true/false for the given feature.
|
||||
"""
|
||||
return self.features.get(feature)
|
||||
|
||||
def get_code_pages(self):
|
||||
"""Return the support code pages as a ``{name: index}`` dict.
|
||||
"""
|
||||
return {v: k for k, v in self.codePages.items()}
|
||||
|
||||
|
||||
def get_profile(name=None, **kwargs):
|
||||
"""Get the profile by name; if no name is given, return the
|
||||
default profile.
|
||||
"""
|
||||
if isinstance(name, Profile):
|
||||
return name
|
||||
|
||||
clazz = get_profile_class(name or 'default')
|
||||
return clazz(**kwargs)
|
||||
|
||||
|
||||
CLASS_CACHE = {}
|
||||
|
||||
|
||||
def get_profile_class(name):
|
||||
"""For the given profile name, load the data from the external
|
||||
database, then generate dynamically a class.
|
||||
"""
|
||||
if name not in CLASS_CACHE:
|
||||
profile_data = PROFILES[name]
|
||||
profile_name = clean(name)
|
||||
class_name = '{}{}Profile'.format(
|
||||
profile_name[0].upper(), profile_name[1:])
|
||||
new_class = type(class_name, (BaseProfile,), {'profile_data': profile_data})
|
||||
CLASS_CACHE[name] = new_class
|
||||
|
||||
return CLASS_CACHE[name]
|
||||
|
||||
|
||||
def clean(s):
|
||||
# Remove invalid characters
|
||||
s = re.sub('[^0-9a-zA-Z_]', '', s)
|
||||
# Remove leading characters until we find a letter or underscore
|
||||
s = re.sub('^[^a-zA-Z_]+', '', s)
|
||||
return str(s)
|
||||
|
||||
|
||||
class Profile(get_profile_class('default')):
|
||||
"""
|
||||
For users, who want to provide their profile
|
||||
"""
|
||||
|
||||
def __init__(self, columns=None, features=None):
|
||||
super(Profile, self).__init__()
|
||||
|
||||
self.columns = columns
|
||||
self.features = features or {}
|
||||
|
||||
def get_columns(self, font):
|
||||
if self.columns is not None:
|
||||
return self.columns
|
||||
|
||||
return super(Profile, self).get_columns(font)
|
||||
2136
venv/lib/python3.11/site-packages/escpos/capabilities_win.json
Normal file
2136
venv/lib/python3.11/site-packages/escpos/capabilities_win.json
Normal file
File diff suppressed because it is too large
Load Diff
577
venv/lib/python3.11/site-packages/escpos/cli.py
Normal file
577
venv/lib/python3.11/site-packages/escpos/cli.py
Normal file
@@ -0,0 +1,577 @@
|
||||
#!/usr/bin/env python
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
""" CLI
|
||||
|
||||
This module acts as a command line interface for python-escpos. It mirrors
|
||||
closely the available ESCPOS commands while adding a couple extra ones for convenience.
|
||||
|
||||
It requires you to have a configuration file. See documentation for details.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import argparse
|
||||
try:
|
||||
import argcomplete
|
||||
except ImportError:
|
||||
# this CLI works nevertheless without argcomplete
|
||||
pass # noqa
|
||||
import sys
|
||||
import six
|
||||
from . import config
|
||||
from . import version
|
||||
|
||||
|
||||
# Must be defined before it's used in DEMO_FUNCTIONS
|
||||
def str_to_bool(string):
|
||||
""" Used as a type in argparse so that we get back a proper
|
||||
bool instead of always True
|
||||
"""
|
||||
return string.lower() in ('y', 'yes', '1', 'true')
|
||||
|
||||
|
||||
# A list of functions that work better with a newline to be sent after them.
|
||||
REQUIRES_NEWLINE = ('qr', 'barcode', 'text', 'block_text')
|
||||
|
||||
|
||||
# Used in demo method
|
||||
# Key: The name of escpos function and the argument passed on the CLI. Some
|
||||
# manual translation is done in the case of barcodes_a -> barcode.
|
||||
# Value: A list of dictionaries to pass to the escpos function as arguments.
|
||||
DEMO_FUNCTIONS = {
|
||||
'text': [
|
||||
{'txt': 'Hello, World!\n', }
|
||||
],
|
||||
'qr': [
|
||||
{'content': 'This tests a QR code'},
|
||||
{'content': 'https://en.wikipedia.org/'}
|
||||
],
|
||||
'barcodes_a': [
|
||||
{'bc': 'UPC-A', 'code': '13243546576'},
|
||||
{'bc': 'UPC-E', 'code': '132435'},
|
||||
{'bc': 'EAN13', 'code': '1324354657687'},
|
||||
{'bc': 'EAN8', 'code': '1324354'},
|
||||
{'bc': 'CODE39', 'code': 'TEST'},
|
||||
{'bc': 'ITF', 'code': '55867492279103'},
|
||||
{'bc': 'NW7', 'code': 'A00000000A'},
|
||||
],
|
||||
'barcodes_b': [
|
||||
{'bc': 'UPC-A', 'code': '13243546576', 'function_type': 'B'},
|
||||
{'bc': 'UPC-E', 'code': '132435', 'function_type': 'B'},
|
||||
{'bc': 'EAN13', 'code': '1324354657687', 'function_type': 'B'},
|
||||
{'bc': 'EAN8', 'code': '1324354', 'function_type': 'B'},
|
||||
{'bc': 'CODE39', 'code': 'TEST', 'function_type': 'B'},
|
||||
{'bc': 'ITF', 'code': '55867492279103', 'function_type': 'B'},
|
||||
{'bc': 'NW7', 'code': 'A00000000A', 'function_type': 'B'},
|
||||
{'bc': 'CODE93', 'code': 'A00000000A', 'function_type': 'B'},
|
||||
{'bc': 'CODE93', 'code': '1324354657687', 'function_type': 'B'},
|
||||
{'bc': 'CODE128A', 'code': 'TEST', 'function_type': 'B'},
|
||||
{'bc': 'CODE128B', 'code': 'TEST', 'function_type': 'B'},
|
||||
{'bc': 'CODE128C', 'code': 'TEST', 'function_type': 'B'},
|
||||
{'bc': 'GS1-128', 'code': '00123456780000000001', 'function_type': 'B'},
|
||||
{'bc': 'GS1 DataBar Omnidirectional', 'code': '0000000000000', 'function_type': 'B'},
|
||||
{'bc': 'GS1 DataBar Truncated', 'code': '0000000000000', 'function_type': 'B'},
|
||||
{'bc': 'GS1 DataBar Limited', 'code': '0000000000000', 'function_type': 'B'},
|
||||
{'bc': 'GS1 DataBar Expanded', 'code': '00AAAAAAA', 'function_type': 'B'},
|
||||
],
|
||||
}
|
||||
|
||||
# Used to build the CLI
|
||||
# A list of dictionaries. Each dict is a CLI argument.
|
||||
# Keys:
|
||||
# parser: A dict of args for command_parsers.add_parser
|
||||
# defaults: A dict of args for subparser.set_defaults
|
||||
# arguments: A list of dicts of args for subparser.add_argument
|
||||
ESCPOS_COMMANDS = [
|
||||
{
|
||||
'parser': {
|
||||
'name': 'qr',
|
||||
'help': 'Print a QR code',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'qr',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--content',),
|
||||
'help': 'Text to print as a qr code',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--size',),
|
||||
'help': 'QR code size (1-16) [default:3]',
|
||||
'required': False,
|
||||
'type': int,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'barcode',
|
||||
'help': 'Print a barcode',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'barcode',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--code',),
|
||||
'help': 'Barcode data to print',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--bc',),
|
||||
'help': 'Barcode format',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--height',),
|
||||
'help': 'Barcode height in px',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--width',),
|
||||
'help': 'Barcode width',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--pos',),
|
||||
'help': 'Label position',
|
||||
'choices': ['BELOW', 'ABOVE', 'BOTH', 'OFF'],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--font',),
|
||||
'help': 'Label font',
|
||||
'choices': ['A', 'B'],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--align_ct',),
|
||||
'help': 'Align barcode center',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--function_type',),
|
||||
'help': 'ESCPOS function type',
|
||||
'choices': ['A', 'B'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'text',
|
||||
'help': 'Print plain text',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'text',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--txt',),
|
||||
'help': 'Plain text to print',
|
||||
'required': True,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'block_text',
|
||||
'help': 'Print wrapped text',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'block_text',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--txt',),
|
||||
'help': 'block_text to print',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--columns',),
|
||||
'help': 'Number of columns',
|
||||
'type': int,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'cut',
|
||||
'help': 'Cut the paper',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'cut',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--mode',),
|
||||
'help': 'Type of cut',
|
||||
'choices': ['FULL', 'PART'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'cashdraw',
|
||||
'help': 'Kick the cash drawer',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'cashdraw',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--pin',),
|
||||
'help': 'Which PIN to kick',
|
||||
'choices': [2, 5],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'image',
|
||||
'help': 'Print an image',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'image',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'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,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'fullimage',
|
||||
'help': 'Print a fullimage',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'fullimage',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--img',),
|
||||
'help': 'Path to img',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--max_height',),
|
||||
'help': 'Max height of image in px',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--width',),
|
||||
'help': 'Max width of image in px',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--histeq',),
|
||||
'help': 'Equalize the histrogram',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--bandsize',),
|
||||
'help': 'Size of bands to divide into when printing',
|
||||
'type': int,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'charcode',
|
||||
'help': 'Set character code table',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'charcode',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--code',),
|
||||
'help': 'Character code',
|
||||
'required': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'set',
|
||||
'help': 'Set text properties',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'set',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--align',),
|
||||
'help': 'Horizontal alignment',
|
||||
'choices': ['left', 'center', 'right'],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--font',),
|
||||
'help': 'Font choice',
|
||||
'choices': ['left', 'center', 'right'],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--text_type',),
|
||||
'help': 'Text properties',
|
||||
'choices': ['B', 'U', 'U2', 'BU', 'BU2', 'NORMAL'],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--width',),
|
||||
'help': 'Width multiplier',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--height',),
|
||||
'help': 'Height multiplier',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--density',),
|
||||
'help': 'Print density',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--invert',),
|
||||
'help': 'White on black printing',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--smooth',),
|
||||
'help': 'Text smoothing. Effective on >: 4x4 text',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--flip',),
|
||||
'help': 'Text smoothing. Effective on >: 4x4 text',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'hw',
|
||||
'help': 'Hardware operations',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'hw',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--hw',),
|
||||
'help': 'Operation',
|
||||
'choices': ['INIT', 'SELECT', 'RESET'],
|
||||
'required': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'control',
|
||||
'help': 'Control sequences',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'control',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--ctl',),
|
||||
'help': 'Control sequence',
|
||||
'choices': ['LF', 'FF', 'CR', 'HT', 'VT'],
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--pos',),
|
||||
'help': 'Horizontal tab position (1-4)',
|
||||
'type': int,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'panel_buttons',
|
||||
'help': 'Controls panel buttons',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'panel_buttons',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--enable',),
|
||||
'help': 'Feed button enabled',
|
||||
'type': str_to_bool,
|
||||
'required': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'raw',
|
||||
'help': 'Raw data',
|
||||
},
|
||||
'defaults': {
|
||||
'func': '_raw',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--msg',),
|
||||
'help': 'Raw data to send',
|
||||
'required': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
|
||||
Handles loading of configuration and creating and processing of command
|
||||
line arguments. Called when run from a CLI.
|
||||
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='CLI for python-escpos',
|
||||
epilog='Printer configuration is defined in the python-escpos config'
|
||||
'file. See documentation for details.',
|
||||
)
|
||||
|
||||
parser.register('type', 'bool', str_to_bool)
|
||||
|
||||
# Allow config file location to be passed
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
help='Alternate path to the configuration file',
|
||||
)
|
||||
|
||||
# Everything interesting runs off of a subparser so we can use the format
|
||||
# cli [subparser] -args
|
||||
command_subparsers = parser.add_subparsers(
|
||||
title='ESCPOS Command',
|
||||
dest='parser',
|
||||
)
|
||||
# fix inconsistencies in the behaviour of some versions of argparse
|
||||
command_subparsers.required = False # force 'required' testing
|
||||
|
||||
# Build the ESCPOS command arguments
|
||||
for command in ESCPOS_COMMANDS:
|
||||
parser_command = command_subparsers.add_parser(**command['parser'])
|
||||
parser_command.set_defaults(**command['defaults'])
|
||||
for argument in command['arguments']:
|
||||
option_strings = argument.pop('option_strings')
|
||||
parser_command.add_argument(*option_strings, **argument)
|
||||
|
||||
# Build any custom arguments
|
||||
parser_command_demo = command_subparsers.add_parser('demo',
|
||||
help='Demonstrates various functions')
|
||||
parser_command_demo.set_defaults(func='demo')
|
||||
demo_group = parser_command_demo.add_mutually_exclusive_group()
|
||||
demo_group.add_argument(
|
||||
'--barcodes-a',
|
||||
help='Print demo barcodes for function type A',
|
||||
action='store_true',
|
||||
)
|
||||
demo_group.add_argument(
|
||||
'--barcodes-b',
|
||||
help='Print demo barcodes for function type B',
|
||||
action='store_true',
|
||||
)
|
||||
demo_group.add_argument(
|
||||
'--qr',
|
||||
help='Print some demo QR codes',
|
||||
action='store_true',
|
||||
)
|
||||
demo_group.add_argument(
|
||||
'--text',
|
||||
help='Print some demo text',
|
||||
action='store_true',
|
||||
)
|
||||
|
||||
parser_command_version = command_subparsers.add_parser('version',
|
||||
help='Print the version of python-escpos')
|
||||
parser_command_version.set_defaults(version=True)
|
||||
|
||||
# hook in argcomplete
|
||||
if 'argcomplete' in globals():
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
# Get only arguments actually passed
|
||||
args_dict = vars(parser.parse_args())
|
||||
if not args_dict:
|
||||
parser.print_help()
|
||||
sys.exit()
|
||||
command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v is not None)
|
||||
|
||||
# If version should be printed, do this, then exit
|
||||
print_version = command_arguments.pop('version', None)
|
||||
if print_version:
|
||||
print(version.version)
|
||||
sys.exit()
|
||||
|
||||
# If there was a config path passed, grab it
|
||||
config_path = command_arguments.pop('config', None)
|
||||
|
||||
# Load the configuration and defined printer
|
||||
saved_config = config.Config()
|
||||
saved_config.load(config_path)
|
||||
printer = saved_config.printer()
|
||||
|
||||
if not printer:
|
||||
raise Exception('No printers loaded from config')
|
||||
|
||||
target_command = command_arguments.pop('func')
|
||||
|
||||
# remove helper-argument 'parser' from dict
|
||||
command_arguments.pop('parser', None)
|
||||
|
||||
if hasattr(printer, target_command):
|
||||
# print command with args
|
||||
getattr(printer, target_command)(**command_arguments)
|
||||
if target_command in REQUIRES_NEWLINE:
|
||||
printer.text("\n")
|
||||
else:
|
||||
command_arguments['printer'] = printer
|
||||
globals()[target_command](**command_arguments)
|
||||
|
||||
|
||||
def demo(printer, **kwargs):
|
||||
"""
|
||||
Prints demos. Called when CLI is passed `demo`. This function
|
||||
uses the DEMO_FUNCTIONS dictionary.
|
||||
|
||||
:param printer: A printer from escpos.printer
|
||||
:param kwargs: A dict with a key for each function you want to test. It's
|
||||
in this format since it usually comes from argparse.
|
||||
"""
|
||||
for demo_choice in kwargs.keys():
|
||||
command = getattr(
|
||||
printer,
|
||||
demo_choice
|
||||
.replace('barcodes_a', 'barcode')
|
||||
.replace('barcodes_b', 'barcode')
|
||||
)
|
||||
for params in DEMO_FUNCTIONS[demo_choice]:
|
||||
command(**params)
|
||||
printer.cut()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
47
venv/lib/python3.11/site-packages/escpos/codepages.py
Normal file
47
venv/lib/python3.11/site-packages/escpos/codepages.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# from .capabilities import CAPABILITIES
|
||||
|
||||
code_pages = {
|
||||
'CP437': '0',
|
||||
'CP932': '1',
|
||||
'CP850': '2',
|
||||
'CP860': '3',
|
||||
'CP863': '4',
|
||||
'CP865': '5',
|
||||
'Unknown': '255',
|
||||
'CP851': '11',
|
||||
'CP853': '12',
|
||||
'CP857': '13',
|
||||
'CP737': '14',
|
||||
'ISO_8859-7': '15',
|
||||
'CP1252': '16',
|
||||
'CP866': '17',
|
||||
'CP852': '18',
|
||||
'CP858': '19',
|
||||
'CP874': '21',
|
||||
'TCVN-3-1': '30',
|
||||
'TCVN-3-2': '31',
|
||||
'CP720': '32',
|
||||
'CP775': '33',
|
||||
'CP855': '34',
|
||||
'CP861': '35',
|
||||
'CP862': '36',
|
||||
'CP864': '37',
|
||||
'CP869': '38',
|
||||
'ISO_8859-2': '39',
|
||||
'ISO_8859-15': '40',
|
||||
'CP1098': '41',
|
||||
'CP774': '42',
|
||||
'CP772': '43',
|
||||
'CP1125': '44',
|
||||
'CP1250': '45',
|
||||
'CP1251': '46',
|
||||
'CP1253': '47',
|
||||
'CP1254': '48',
|
||||
'CP1255': '49',
|
||||
'CP1256': '50',
|
||||
'CP1257': '51',
|
||||
'CP1258': '52',
|
||||
'RK1048': '53'
|
||||
}
|
||||
|
||||
CODEPAGE = {'iconv': 'CP437', 'name': 'CP437', 'python_encode': 'cp437'}
|
||||
114
venv/lib/python3.11/site-packages/escpos/config.py
Normal file
114
venv/lib/python3.11/site-packages/escpos/config.py
Normal file
@@ -0,0 +1,114 @@
|
||||
""" ESC/POS configuration manager.
|
||||
|
||||
This module contains the implementations of abstract base class :py:class:`Config`.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import appdirs
|
||||
import yaml
|
||||
|
||||
from . import printer
|
||||
from . import exceptions
|
||||
|
||||
|
||||
class Config(object):
|
||||
""" Configuration handler class.
|
||||
|
||||
This class loads configuration from a default or specificed directory. It
|
||||
can create your defined printer and return it to you.
|
||||
"""
|
||||
_app_name = 'python-escpos'
|
||||
_config_file = 'config.yaml'
|
||||
|
||||
def __init__(self):
|
||||
""" Initialize configuration.
|
||||
|
||||
Remember to add anything that needs to be reset between configurations
|
||||
to self._reset_config
|
||||
"""
|
||||
self._has_loaded = False
|
||||
self._printer = None
|
||||
|
||||
self._printer_name = None
|
||||
self._printer_config = None
|
||||
|
||||
def _reset_config(self):
|
||||
""" Clear the loaded configuration.
|
||||
|
||||
If we are loading a changed config, we don't want to have leftover
|
||||
data.
|
||||
"""
|
||||
self._has_loaded = False
|
||||
self._printer = None
|
||||
|
||||
self._printer_name = None
|
||||
self._printer_config = None
|
||||
|
||||
def load(self, config_path=None):
|
||||
""" Load and parse the configuration file using pyyaml
|
||||
|
||||
:param config_path: An optional file path, file handle, or byte string
|
||||
for the configuration file.
|
||||
|
||||
"""
|
||||
|
||||
self._reset_config()
|
||||
|
||||
if not config_path:
|
||||
config_path = os.path.join(
|
||||
appdirs.user_config_dir(self._app_name),
|
||||
self._config_file
|
||||
)
|
||||
|
||||
try:
|
||||
# First check if it's file like. If it is, pyyaml can load it.
|
||||
# I'm checking type instead of catching exceptions to keep the
|
||||
# exception handling simple
|
||||
if hasattr(config_path, 'read'):
|
||||
config = yaml.safe_load(config_path)
|
||||
else:
|
||||
# If it isn't, it's a path. We have to open it first, otherwise
|
||||
# pyyaml will try to read it as yaml
|
||||
with open(config_path, 'rb') as config_file:
|
||||
config = yaml.safe_load(config_file)
|
||||
except EnvironmentError:
|
||||
raise exceptions.ConfigNotFoundError('Couldn\'t read config at {config_path}'.format(
|
||||
config_path=str(config_path),
|
||||
))
|
||||
except yaml.YAMLError:
|
||||
raise exceptions.ConfigSyntaxError('Error parsing YAML')
|
||||
|
||||
if 'printer' in config:
|
||||
self._printer_config = config['printer']
|
||||
self._printer_name = self._printer_config.pop('type').title()
|
||||
|
||||
if not self._printer_name or not hasattr(printer, self._printer_name):
|
||||
raise exceptions.ConfigSyntaxError(
|
||||
'Printer type "{printer_name}" is invalid'.format(
|
||||
printer_name=self._printer_name,
|
||||
)
|
||||
)
|
||||
|
||||
self._has_loaded = True
|
||||
|
||||
def printer(self):
|
||||
""" Returns a printer that was defined in the config, or throws an
|
||||
exception.
|
||||
|
||||
This method loads the default config if one hasn't beeen already loaded.
|
||||
|
||||
"""
|
||||
if not self._has_loaded:
|
||||
self.load()
|
||||
|
||||
if not self._printer_name:
|
||||
raise exceptions.ConfigSectionMissingError('printer')
|
||||
|
||||
if not self._printer:
|
||||
# We could catch init errors and make them a ConfigSyntaxError,
|
||||
# but I'll just let them pass
|
||||
self._printer = getattr(printer, self._printer_name)(**self._printer_config)
|
||||
|
||||
return self._printer
|
||||
274
venv/lib/python3.11/site-packages/escpos/constants.py
Normal file
274
venv/lib/python3.11/site-packages/escpos/constants.py
Normal file
@@ -0,0 +1,274 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Set of ESC/POS Commands (Constants)
|
||||
|
||||
This module contains constants that are described in the esc/pos-documentation.
|
||||
Since there is no definitive and unified specification for all esc/pos-like printers the constants could later be
|
||||
moved to `capabilities` as in `escpos-php by @mike42 <https://github.com/mike42/escpos-php>`_.
|
||||
|
||||
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
|
||||
import six
|
||||
|
||||
# Control characters
|
||||
# as labelled in https://www.novopos.ch/client/EPSON/TM-T20/TM-T20_eng_qr.pdf
|
||||
NUL = b'\x00'
|
||||
EOT = b'\x04'
|
||||
ENQ = b'\x05'
|
||||
DLE = b'\x10'
|
||||
DC4 = b'\x14'
|
||||
CAN = b'\x18'
|
||||
ESC = b'\x1b'
|
||||
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
|
||||
|
||||
# Printer hardware
|
||||
HW_INIT = ESC + b'@' # Clear data in buffer and reset modes
|
||||
HW_SELECT = ESC + b'=\x01' # Printer select
|
||||
|
||||
HW_RESET = ESC + b'\x3f\x0a\x00' # Reset printer hardware
|
||||
# (TODO: Where is this specified?)
|
||||
|
||||
# Cash Drawer (ESC p <pin> <on time: 2*ms> <off time: 2*ms>)
|
||||
_CASH_DRAWER = lambda m, t1='', t2='': ESC + b'p' + m + six.int2byte(t1) + six.int2byte(t2)
|
||||
CD_KICK_DEC_SEQUENCE = lambda esc, p, m, t1=50, t2=50: six.int2byte(esc) + six.int2byte(p) + six.int2byte(m) + six.int2byte(t1) + six.int2byte(t2)
|
||||
CD_KICK_2 = _CASH_DRAWER(b'\x00', 50, 50) # Sends a pulse to pin 2 []
|
||||
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
|
||||
|
||||
# Beep
|
||||
BEEP = b'\x07'
|
||||
|
||||
# Panel buttons (e.g. the FEED button)
|
||||
_PANEL_BUTTON = lambda n: ESC + b'c5' + six.int2byte(n)
|
||||
PANEL_BUTTON_ON = _PANEL_BUTTON(0) # enable all panel buttons
|
||||
PANEL_BUTTON_OFF = _PANEL_BUTTON(1) # disable all panel buttons
|
||||
|
||||
# Line display printing
|
||||
LINE_DISPLAY_OPEN = ESC + b'\x3d\x02'
|
||||
LINE_DISPLAY_CLEAR = ESC + b'\x40'
|
||||
LINE_DISPLAY_CLOSE = ESC + b'\x3d\x01'
|
||||
|
||||
# Sheet modes
|
||||
SHEET_SLIP_MODE = ESC + b'\x63\x30\x04' # slip paper
|
||||
SHEET_ROLL_MODE = ESC + b'\x63\x30\x01' # paper roll
|
||||
|
||||
# Text format
|
||||
# TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll
|
||||
# Printers" and tidy up this stuff too.
|
||||
TXT_SIZE = GS + b'!'
|
||||
|
||||
TXT_NORMAL = ESC + b'!\x00' # Normal text
|
||||
|
||||
|
||||
TXT_STYLE = {
|
||||
'bold': {
|
||||
False: ESC + b'\x45\x00', # Bold font OFF
|
||||
True: ESC + b'\x45\x01' # Bold font ON
|
||||
},
|
||||
'underline': {
|
||||
0: ESC + b'\x2d\x00', # Underline font OFF
|
||||
1: ESC + b'\x2d\x01', # Underline font 1-dot ON
|
||||
2: ESC + b'\x2d\x02' # Underline font 2-dot ON
|
||||
},
|
||||
'size': {
|
||||
'normal': TXT_NORMAL + ESC + b'!\x00', # Normal text
|
||||
'2h': TXT_NORMAL + ESC + b'!\x10', # Double height text
|
||||
'2w': TXT_NORMAL + ESC + b'!\x20', # Double width text
|
||||
'2x': TXT_NORMAL + ESC + b'!\x30' # Quad area text
|
||||
},
|
||||
'font': {
|
||||
'a': ESC + b'\x4d\x00', # Font type A
|
||||
'b': ESC + b'\x4d\x00' # Font type B
|
||||
},
|
||||
'align': {
|
||||
'left': ESC + b'\x61\x00', # Left justification
|
||||
'center': ESC + b'\x61\x01', # Centering
|
||||
'right': ESC + b'\x61\x02' # Right justification
|
||||
},
|
||||
'invert': {
|
||||
True: GS + b'\x42\x01', # Inverse Printing ON
|
||||
False: GS + b'\x42\x00' # Inverse Printing OFF
|
||||
},
|
||||
'color': {
|
||||
'black': ESC + b'\x72\x00', # Default Color
|
||||
'red': ESC + b'\x72\x01' # Alternative Color, Usually Red
|
||||
},
|
||||
'flip': {
|
||||
True: ESC + b'\x7b\x01', # Flip ON
|
||||
False: ESC + b'\x7b\x00' # Flip OFF
|
||||
},
|
||||
'density': {
|
||||
0: GS + b'\x7c\x00', # Printing Density -50%
|
||||
1: GS + b'\x7c\x01', # Printing Density -37.5%
|
||||
2: GS + b'\x7c\x02', # Printing Density -25%
|
||||
3: GS + b'\x7c\x03', # Printing Density -12.5%
|
||||
4: GS + b'\x7c\x04', # Printing Density 0%
|
||||
5: GS + b'\x7c\x08', # Printing Density +50%
|
||||
6: GS + b'\x7c\x07', # Printing Density +37.5%
|
||||
7: GS + b'\x7c\x06', # Printing Density +25%
|
||||
8: GS + b'\x7c\x05' # Printing Density +12.5%
|
||||
},
|
||||
'smooth': {
|
||||
True: GS + b'\x62\x01', # Smooth ON
|
||||
False: GS + b'\x62\x00' # Smooth OFF
|
||||
},
|
||||
'height': { # Custom text height
|
||||
1: 0x00,
|
||||
2: 0x01,
|
||||
3: 0x02,
|
||||
4: 0x03,
|
||||
5: 0x04,
|
||||
6: 0x05,
|
||||
7: 0x06,
|
||||
8: 0x07
|
||||
},
|
||||
'width': { # Custom text width
|
||||
1: 0x00,
|
||||
2: 0x10,
|
||||
3: 0x20,
|
||||
4: 0x30,
|
||||
5: 0x40,
|
||||
6: 0x50,
|
||||
7: 0x60,
|
||||
8: 0x70
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
# Spacing
|
||||
LINESPACING_RESET = ESC + b'2'
|
||||
LINESPACING_FUNCS = {
|
||||
60: ESC + b'A', # line_spacing/60 of an inch, 0 <= line_spacing <= 85
|
||||
360: ESC + b'+', # line_spacing/360 of an inch, 0 <= line_spacing <= 255
|
||||
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.
|
||||
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
|
||||
|
||||
_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_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:
|
||||
# - Type A: "GS k <type as integer> <data> NUL"
|
||||
# - TYPE B: "GS k <type as letter> <data length> <data>"
|
||||
# The latter command supports more barcode types
|
||||
_SET_BARCODE_TYPE = lambda m: GS + b'k' + six.int2byte(m)
|
||||
|
||||
# Barcodes for printing function type A
|
||||
BARCODE_TYPE_A = {
|
||||
'UPC-A': _SET_BARCODE_TYPE(0),
|
||||
'UPC-E': _SET_BARCODE_TYPE(1),
|
||||
'EAN13': _SET_BARCODE_TYPE(2),
|
||||
'EAN8': _SET_BARCODE_TYPE(3),
|
||||
'CODE39': _SET_BARCODE_TYPE(4),
|
||||
'ITF': _SET_BARCODE_TYPE(5),
|
||||
'NW7': _SET_BARCODE_TYPE(6),
|
||||
'CODABAR': _SET_BARCODE_TYPE(6), # Same as NW7
|
||||
}
|
||||
|
||||
# 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),
|
||||
'EAN13': _SET_BARCODE_TYPE(67),
|
||||
'EAN8': _SET_BARCODE_TYPE(68),
|
||||
'CODE39': _SET_BARCODE_TYPE(69),
|
||||
'ITF': _SET_BARCODE_TYPE(70),
|
||||
'NW7': _SET_BARCODE_TYPE(71),
|
||||
'CODABAR': _SET_BARCODE_TYPE(71), # Same as NW7
|
||||
'CODE93': _SET_BARCODE_TYPE(72),
|
||||
'CODE128': _SET_BARCODE_TYPE(73),
|
||||
'GS1-128': _SET_BARCODE_TYPE(74),
|
||||
'GS1 DATABAR OMNIDIRECTIONAL': _SET_BARCODE_TYPE(75),
|
||||
'GS1 DATABAR TRUNCATED': _SET_BARCODE_TYPE(76),
|
||||
'GS1 DATABAR LIMITED': _SET_BARCODE_TYPE(77),
|
||||
'GS1 DATABAR EXPANDED': _SET_BARCODE_TYPE(78),
|
||||
}
|
||||
|
||||
BARCODE_FORMATS = {
|
||||
'UPC-A': ([(11, 12)], "^[0-9]{11,12}$"),
|
||||
'UPC-E': ([(7, 8), (11, 12)], "^([0-9]{7,8}|[0-9]{11,12})$"),
|
||||
'EAN13': ([(12, 13)], "^[0-9]{12,13}$"),
|
||||
'EAN8': ([(7, 8)], "^[0-9]{7,8}$"),
|
||||
'CODE39': ([(1, 255)], "^([0-9A-Z \$\%\+\-\.\/]+|\*[0-9A-Z \$\%\+\-\.\/]+\*)$"),
|
||||
'ITF': ([(2, 255)], "^([0-9]{2})+$"),
|
||||
'NW7': ([(1, 255)], "^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"),
|
||||
'CODABAR': ([(1, 255)], "^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"), # Same as NW7
|
||||
'CODE93': ([(1, 255)], "^[\\x00-\\x7F]+$"),
|
||||
'CODE128': ([(2, 255)], "^\{[A-C][\\x00-\\x7F]+$"),
|
||||
'GS1-128': ([(2, 255)], "^\{[A-C][\\x00-\\x7F]+$"), # same as CODE128
|
||||
'GS1 DATABAR OMNIDIRECTIONAL': ([(13,13)], "^[0-9]{13}$"),
|
||||
'GS1 DATABAR TRUNCATED': ([(13,13)], "^[0-9]{13}$"), # same as GS1 omnidirectional
|
||||
'GS1 DATABAR LIMITED': ([(13,13)], "^[01][0-9]{12}$"),
|
||||
'GS1 DATABAR EXPANDED': ([(2,255)], "^\([0-9][A-Za-z0-9 \!\"\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\_\{]+$"),
|
||||
}
|
||||
|
||||
BARCODE_TYPES = {
|
||||
'A': BARCODE_TYPE_A,
|
||||
'B': BARCODE_TYPE_B,
|
||||
}
|
||||
|
||||
# QRCode error correction levels
|
||||
QR_ECLEVEL_L = 0
|
||||
QR_ECLEVEL_M = 1
|
||||
QR_ECLEVEL_Q = 2
|
||||
QR_ECLEVEL_H = 3
|
||||
|
||||
# QRcode models
|
||||
QR_MODEL_1 = 1
|
||||
QR_MODEL_2 = 2
|
||||
QR_MICRO = 3
|
||||
|
||||
# Image format
|
||||
# NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image"
|
||||
# command. The constants include a fragment of the data's header.
|
||||
_PRINT_RASTER_IMG = lambda data: GS + b'v0' + data
|
||||
S_RASTER_N = _PRINT_RASTER_IMG(b'\x00') # Set raster image normal size
|
||||
S_RASTER_2W = _PRINT_RASTER_IMG(b'\x01') # Set raster image double width
|
||||
S_RASTER_2H = _PRINT_RASTER_IMG(b'\x02') # Set raster image double height
|
||||
S_RASTER_Q = _PRINT_RASTER_IMG(b'\x03') # Set raster image quadruple
|
||||
|
||||
# Status Command
|
||||
RT_STATUS = DLE + EOT
|
||||
RT_STATUS_ONLINE = RT_STATUS + b'\x01'
|
||||
RT_STATUS_PAPER = RT_STATUS + b'\x04'
|
||||
RT_MASK_ONLINE = 8
|
||||
RT_MASK_PAPER = 18
|
||||
RT_MASK_LOWPAPER = 30
|
||||
RT_MASK_NOPAPER = 114
|
||||
969
venv/lib/python3.11/site-packages/escpos/escpos.py
Normal file
969
venv/lib/python3.11/site-packages/escpos/escpos.py
Normal file
@@ -0,0 +1,969 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import time
|
||||
import qrcode
|
||||
import textwrap
|
||||
import six
|
||||
import barcode
|
||||
from barcode.writer import ImageWriter
|
||||
from re import match as re_match
|
||||
|
||||
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
|
||||
from .constants import BARCODE_FONT_A, BARCODE_FONT_B, BARCODE_FORMATS
|
||||
from .constants import BARCODE_TXT_OFF, BARCODE_TXT_BTH, BARCODE_TXT_ABV, BARCODE_TXT_BLW
|
||||
from .constants import TXT_SIZE, TXT_NORMAL
|
||||
from .constants import SET_FONT
|
||||
from .constants import LINESPACING_FUNCS, LINESPACING_RESET
|
||||
from .constants import LINE_DISPLAY_OPEN, LINE_DISPLAY_CLEAR, LINE_DISPLAY_CLOSE
|
||||
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
|
||||
from .constants import CTL_VT, CTL_CR, CTL_FF, CTL_LF, CTL_SET_HT, PANEL_BUTTON_OFF, PANEL_BUTTON_ON
|
||||
from .constants import TXT_STYLE
|
||||
from .constants import RT_STATUS_ONLINE, RT_MASK_ONLINE
|
||||
from .constants import RT_STATUS_PAPER, RT_MASK_PAPER, RT_MASK_LOWPAPER, RT_MASK_NOPAPER
|
||||
from .constants import BEEP
|
||||
|
||||
from abc import ABCMeta, abstractmethod # abstract base class support
|
||||
from .exceptions import BarcodeTypeError, BarcodeSizeError, TabPosError
|
||||
from .exceptions import CashDrawerError, SetVariableError, BarcodeCodeError
|
||||
from .exceptions import ImageWidthError
|
||||
from escpos.image import EscposImage
|
||||
|
||||
from .magicencode import MagicEncode
|
||||
from escpos.capabilities import get_profile, BARCODE_B
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class Escpos(object):
|
||||
""" ESC/POS Printer object
|
||||
|
||||
This class is the abstract base class for an esc/pos-printer.
|
||||
The printer implementations are children of this class.
|
||||
"""
|
||||
device = None
|
||||
|
||||
def __init__(self, profile=None, magic_encode_args=None, **kwargs):
|
||||
""" Initialize ESCPOS Printer
|
||||
|
||||
:param profile: Printer profile"""
|
||||
self.profile = get_profile(profile)
|
||||
self.magic = MagicEncode(self, **(magic_encode_args or {}))
|
||||
|
||||
def __del__(self):
|
||||
""" call self.close upon deletion """
|
||||
try:
|
||||
self.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _raw(self, msg):
|
||||
""" Sends raw data to the printer
|
||||
|
||||
This function has to be individually implemented by the implementations.
|
||||
|
||||
:param msg: message string to be sent to the printer
|
||||
:type msg: bytes
|
||||
"""
|
||||
pass
|
||||
|
||||
def _read(self):
|
||||
""" Returns a NotImplementedError if the instance of the class doesn't override this method.
|
||||
:raises NotImplementedError
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster",
|
||||
fragment_height=960, center=False):
|
||||
""" Print an image
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
:param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp`
|
||||
: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`
|
||||
:param fragment_height: Images larger than this will be split into multiple fragments *default:* 960
|
||||
:param center: Center image horizontally *default:* False
|
||||
|
||||
"""
|
||||
im = EscposImage(img_source)
|
||||
|
||||
try:
|
||||
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.")
|
||||
|
||||
max_width = int(self.profile.profile_data['media']['width']['pixels'])
|
||||
|
||||
if im.width > max_width:
|
||||
raise ImageWidthError('{} > {}'.format(im.width, max_width))
|
||||
|
||||
if center:
|
||||
im.center(max_width)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
"""
|
||||
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, center=False, impl="bitImageRaster"):
|
||||
""" Print QR Code for the provided string
|
||||
|
||||
:param content: The content of the code. Numeric data will be more efficiently compacted.
|
||||
:param ec: Error-correction level to use. One of QR_ECLEVEL_L (default), QR_ECLEVEL_M, QR_ECLEVEL_Q or
|
||||
QR_ECLEVEL_H.
|
||||
Higher error correction results in a less compact code.
|
||||
:param size: Pixel size to use. Must be 1-16 (default 3)
|
||||
: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)
|
||||
:param center: Centers the code *default:* False
|
||||
:param impl: Image-printing-implementation, refer to :meth:`.image()` for details
|
||||
"""
|
||||
# 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:
|
||||
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,
|
||||
QR_ECLEVEL_M: qrcode.constants.ERROR_CORRECT_M,
|
||||
QR_ECLEVEL_Q: qrcode.constants.ERROR_CORRECT_Q
|
||||
}
|
||||
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()
|
||||
im = qr_img._img.convert("RGB")
|
||||
|
||||
# Convert the RGB image in printable image
|
||||
self.text('\n')
|
||||
self.image(im, center=center, impl=impl)
|
||||
self.text('\n')
|
||||
self.text('\n')
|
||||
return
|
||||
|
||||
if center:
|
||||
raise NotImplementedError("Centering not implemented for native QR rendering")
|
||||
|
||||
# Native 2D code printing
|
||||
cn = b'1' # Code type for QR code
|
||||
# Select model: 1, 2 or micro.
|
||||
self._send_2d_code_data(six.int2byte(65), cn, six.int2byte(48 + model) + six.int2byte(0))
|
||||
# Set dot size.
|
||||
self._send_2d_code_data(six.int2byte(67), cn, six.int2byte(size))
|
||||
# Set error correction level: L, M, Q, or H
|
||||
self._send_2d_code_data(six.int2byte(69), cn, six.int2byte(48 + ec))
|
||||
# Send content & print
|
||||
self._send_2d_code_data(six.int2byte(80), cn, content.encode('utf-8'), b'0')
|
||||
self._send_2d_code_data(six.int2byte(81), cn, b'', b'0')
|
||||
|
||||
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.")
|
||||
header = self._int_low_high(len(data) + len(m) + 2, 2)
|
||||
self._raw(GS + b'(k' + header + cn + fn + m + data)
|
||||
|
||||
@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.
|
||||
|
||||
:param inp_number: Input number
|
||||
:param out_bytes: The number of bytes to output (1 - 4).
|
||||
"""
|
||||
max_input = (256 << (out_bytes * 8) - 1)
|
||||
if not 1 <= out_bytes <= 4:
|
||||
raise ValueError("Can only output 1-4 bytes")
|
||||
if not 0 <= inp_number <= max_input:
|
||||
raise ValueError("Number too large. Can only output up to {0} in {1} bytes".format(max_input, out_bytes))
|
||||
outp = b''
|
||||
for _ in range(0, out_bytes):
|
||||
outp += six.int2byte(inp_number % 256)
|
||||
inp_number //= 256
|
||||
return outp
|
||||
|
||||
def charcode(self, code="AUTO"):
|
||||
""" Set Character Code Table
|
||||
|
||||
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.)
|
||||
|
||||
:param code: Name of CharCode
|
||||
:raises: :py:exc:`~escpos.exceptions.CharCodeError`
|
||||
"""
|
||||
if code.upper() == "AUTO":
|
||||
self.magic.force_encoding(False)
|
||||
else:
|
||||
self.magic.force_encoding(code)
|
||||
|
||||
@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.
|
||||
|
||||
:param bc: barcode format, see :py:meth:`.barcode()`
|
||||
:param code: alphanumeric data to be printed as bar code, see :py:meth:`.barcode()`
|
||||
: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)
|
||||
|
||||
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A",
|
||||
align_ct=True, function_type=None, check=True):
|
||||
""" Print Barcode
|
||||
|
||||
This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to
|
||||
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.
|
||||
|
||||
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.)
|
||||
|
||||
.. todo:: On TM-T88II width from 1 to 6 is accepted. Try to acquire command reference and correct the code.
|
||||
.. todo:: Supplying pos does not have an effect for every barcode type. Check and document for which types this
|
||||
is true.
|
||||
|
||||
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.)
|
||||
|
||||
:param code: alphanumeric data to be printed as bar code
|
||||
:param bc: barcode format, possible values are for type A are:
|
||||
|
||||
* UPC-A
|
||||
* UPC-E
|
||||
* EAN13
|
||||
* EAN8
|
||||
* CODE39
|
||||
* ITF
|
||||
* NW7
|
||||
|
||||
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
|
||||
|
||||
If none is specified, the method raises :py:exc:`~escpos.exceptions.BarcodeTypeError`.
|
||||
:param height: barcode height, has to be between 1 and 255
|
||||
*default*: 64
|
||||
:type height: int
|
||||
:param width: barcode width, has to be between 2 and 6
|
||||
*default*: 3
|
||||
:type width: int
|
||||
:param pos: where to place the text relative to the barcode, *default*: BELOW
|
||||
|
||||
* ABOVE
|
||||
* BELOW
|
||||
* BOTH
|
||||
* OFF
|
||||
|
||||
:param font: select font (see ESC/POS-documentation, the device often has two fonts), *default*: A
|
||||
|
||||
* A
|
||||
* B
|
||||
|
||||
:param align_ct: If this parameter is True the barcode will be centered. Otherwise no alignment command will be
|
||||
issued.
|
||||
:type align_ct: bool
|
||||
|
||||
: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.
|
||||
*default*: A
|
||||
|
||||
:param check: If this parameter is True, the barcode format will be checked to ensure it meets the bc
|
||||
requirements as definged in the ESC/POS documentation. See :py:meth:`.check_barcode()`
|
||||
for more information. *default*: True.
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`,
|
||||
: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))
|
||||
|
||||
bc_types = BARCODE_TYPES[function_type.upper()]
|
||||
if bc.upper() not in bc_types.keys():
|
||||
raise BarcodeTypeError((
|
||||
"Barcode '{bc}' not valid for barcode function type "
|
||||
"{function_type}").format(
|
||||
bc=bc,
|
||||
function_type=function_type,
|
||||
))
|
||||
|
||||
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,
|
||||
))
|
||||
|
||||
# Align Bar Code()
|
||||
if align_ct:
|
||||
self._raw(TXT_STYLE['align']['center'])
|
||||
# Height
|
||||
if 1 <= height <= 255:
|
||||
self._raw(BARCODE_HEIGHT + six.int2byte(height))
|
||||
else:
|
||||
raise BarcodeSizeError("height = {height}".format(height=height))
|
||||
# Width
|
||||
if 2 <= width <= 6:
|
||||
self._raw(BARCODE_WIDTH + six.int2byte(width))
|
||||
else:
|
||||
raise BarcodeSizeError("width = {width}".format(width=width))
|
||||
# Font
|
||||
if font.upper() == "B":
|
||||
self._raw(BARCODE_FONT_B)
|
||||
else: # DEFAULT FONT: A
|
||||
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)
|
||||
else: # DEFAULT POSITION: BELOW
|
||||
self._raw(BARCODE_TXT_BLW)
|
||||
|
||||
self._raw(bc_types[bc.upper()])
|
||||
|
||||
if function_type.upper() == "B":
|
||||
self._raw(six.int2byte(len(code)))
|
||||
|
||||
# Print Code
|
||||
if code:
|
||||
self._raw(code.encode())
|
||||
else:
|
||||
raise BarcodeCodeError()
|
||||
|
||||
if function_type.upper() == "A":
|
||||
self._raw(NUL)
|
||||
|
||||
def soft_barcode(self, barcode_type, data, impl='bitImageColumn',
|
||||
module_height=5, module_width=0.2, text_distance=1,
|
||||
center=True):
|
||||
|
||||
image_writer = ImageWriter()
|
||||
|
||||
# Check if barcode type exists
|
||||
if barcode_type not in barcode.PROVIDED_BARCODES:
|
||||
raise BarcodeTypeError(
|
||||
'Barcode type {} not supported by software barcode renderer'
|
||||
.format(barcode_type))
|
||||
|
||||
# Render the barcode to a fake file
|
||||
barcode_class = barcode.get_barcode_class(barcode_type)
|
||||
my_code = barcode_class(data, writer=image_writer)
|
||||
|
||||
with open(os.devnull, "wb") as nullfile:
|
||||
my_code.write(nullfile, {
|
||||
'module_height': module_height,
|
||||
'module_width': module_width,
|
||||
'text_distance': text_distance
|
||||
})
|
||||
|
||||
# Retrieve the Pillow image and print it
|
||||
image = my_code.writer._image
|
||||
self.image(image, impl=impl, center=center)
|
||||
|
||||
def text(self, txt):
|
||||
""" Print alpha-numeric text
|
||||
|
||||
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
|
||||
:raises: :py:exc:`~escpos.exceptions.TextError`
|
||||
"""
|
||||
txt = six.text_type(txt)
|
||||
self.magic.write(txt)
|
||||
|
||||
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)
|
||||
|
||||
def block_text(self, txt, font=None, columns=None):
|
||||
""" Text is printed wrapped to specified columns
|
||||
|
||||
Text has to be encoded in unicode.
|
||||
|
||||
:param txt: text to be printed
|
||||
:param font: font to be used, can be :code:`a` or :code:`b`
|
||||
:param columns: amount of columns
|
||||
:return: None
|
||||
"""
|
||||
col_count = self.profile.get_columns(font) if columns is None else columns
|
||||
self.text(textwrap.fill(txt, col_count))
|
||||
|
||||
def set(self, align='left', font='a', bold=False, underline=0, width=1,
|
||||
height=1, density=9, invert=False, smooth=False, flip=False,
|
||||
double_width=False, double_height=False, custom_size=False):
|
||||
""" Set text properties by sending them to the printer
|
||||
|
||||
:param align: horizontal position for text, possible values are:
|
||||
|
||||
* 'center'
|
||||
* 'left'
|
||||
* 'right'
|
||||
|
||||
*default*: 'left'
|
||||
|
||||
:param font: font given as an index, a name, or one of the
|
||||
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
|
||||
: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
|
||||
:param density: print density, value from 0-8, if something else is supplied the density remains unchanged
|
||||
:param invert: True enables white on black printing, *default*: False
|
||||
:param smooth: True enables text smoothing. Effective on 4x4 size text and larger, *default*: False
|
||||
:param flip: True enables upside-down printing, *default*: False
|
||||
|
||||
:type font: str
|
||||
:type invert: bool
|
||||
:type bold: bool
|
||||
:type underline: bool
|
||||
:type smooth: bool
|
||||
:type flip: bool
|
||||
:type custom_size: bool
|
||||
:type double_width: bool
|
||||
:type double_height: bool
|
||||
:type align: str
|
||||
:type width: int
|
||||
:type height: int
|
||||
:type density: int
|
||||
"""
|
||||
|
||||
if custom_size:
|
||||
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()
|
||||
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'])
|
||||
|
||||
self._raw(TXT_STYLE['flip'][flip])
|
||||
self._raw(TXT_STYLE['smooth'][smooth])
|
||||
self._raw(TXT_STYLE['bold'][bold])
|
||||
self._raw(TXT_STYLE['underline'][underline])
|
||||
self._raw(SET_FONT(six.int2byte(self.profile.get_font(font))))
|
||||
self._raw(TXT_STYLE['align'][align])
|
||||
|
||||
if density != 9:
|
||||
self._raw(TXT_STYLE['density'][density])
|
||||
|
||||
self._raw(TXT_STYLE['invert'][invert])
|
||||
|
||||
def line_spacing(self, spacing=None, divisor=180):
|
||||
""" Set line character spacing.
|
||||
|
||||
If no spacing is given, we reset it to the default.
|
||||
|
||||
There are different commands for setting the line spacing, using
|
||||
a different denominator:
|
||||
|
||||
'+'' 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
|
||||
|
||||
Some printers may not support all of them. The most commonly
|
||||
available command (using a divisor of 180) is chosen.
|
||||
"""
|
||||
if spacing is None:
|
||||
self._raw(LINESPACING_RESET)
|
||||
return
|
||||
|
||||
if divisor not in LINESPACING_FUNCS:
|
||||
raise ValueError("divisor must be either 360, 180 or 60")
|
||||
if (divisor in [360, 180]
|
||||
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))
|
||||
|
||||
def cut(self, mode='FULL', feed=True):
|
||||
""" 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.
|
||||
|
||||
:param mode: set to 'PART' for a partial cut. default: 'FULL'
|
||||
:param feed: print and feed before cutting. default: true
|
||||
:raises ValueError: if mode not in ('FULL', 'PART')
|
||||
"""
|
||||
|
||||
if not feed:
|
||||
self._raw(GS + b'V' + six.int2byte(66) + b'\x00')
|
||||
return
|
||||
|
||||
self.print_and_feed(6)
|
||||
|
||||
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)
|
||||
|
||||
def beep(self):
|
||||
""" Send a beep sound
|
||||
|
||||
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]
|
||||
|
||||
:param pin: pin number, 2 or 5 or list of decimals
|
||||
:raises: :py:exc:`~escpos.exceptions.CashDrawerError`
|
||||
"""
|
||||
self._raw(BEEP)
|
||||
|
||||
|
||||
def cashdraw(self, pin):
|
||||
""" Send pulse to kick the cash drawer
|
||||
|
||||
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]
|
||||
|
||||
:param pin: pin number, 2 or 5 or list of decimals
|
||||
:raises: :py:exc:`~escpos.exceptions.CashDrawerError`
|
||||
"""
|
||||
if pin == 2:
|
||||
self._raw(CD_KICK_2)
|
||||
elif pin == 5:
|
||||
self._raw(CD_KICK_5)
|
||||
else:
|
||||
try:
|
||||
self._raw(CD_KICK_DEC_SEQUENCE(*pin))
|
||||
except TypeError as err:
|
||||
raise CashDrawerError(err)
|
||||
|
||||
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.
|
||||
|
||||
:param text: Text to display
|
||||
"""
|
||||
self.linedisplay_select(select_display=True)
|
||||
self.linedisplay_clear()
|
||||
self.text(text)
|
||||
self.linedisplay_select(select_display=False)
|
||||
|
||||
def hw(self, hw):
|
||||
""" Hardware operations
|
||||
|
||||
:param hw: hardware action, may be:
|
||||
|
||||
* INIT
|
||||
* SELECT
|
||||
* RESET
|
||||
"""
|
||||
if hw.upper() == "INIT":
|
||||
self._raw(HW_INIT)
|
||||
elif hw.upper() == "SELECT":
|
||||
self._raw(HW_SELECT)
|
||||
elif hw.upper() == "RESET":
|
||||
self._raw(HW_RESET)
|
||||
else: # DEFAULT: DOES NOTHING
|
||||
pass
|
||||
|
||||
def print_and_feed(self, n=1):
|
||||
""" Print data in print buffer and feed *n* lines
|
||||
|
||||
if n not in range (0, 255) then ValueError will be raised
|
||||
|
||||
:param n: number of n to feed. 0 <= n <= 255. default: 1
|
||||
: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")
|
||||
|
||||
def control(self, ctl, count=5, tab_size=8):
|
||||
""" Feed control sequences
|
||||
|
||||
: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*
|
||||
|
||||
: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
|
||||
:raises: :py:exc:`~escpos.exceptions.TabPosError`
|
||||
"""
|
||||
# Set position
|
||||
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":
|
||||
if not (0 <= count <= 32 and
|
||||
1 <= tab_size <= 255 and
|
||||
count * tab_size < 256):
|
||||
raise TabPosError()
|
||||
else:
|
||||
# Set tab positions
|
||||
self._raw(CTL_SET_HT)
|
||||
for iterator in range(1, count):
|
||||
self._raw(six.int2byte(iterator * tab_size))
|
||||
self._raw(NUL)
|
||||
elif ctl.upper() == "VT":
|
||||
self._raw(CTL_VT)
|
||||
|
||||
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)
|
||||
|
||||
def query_status(self, mode):
|
||||
"""
|
||||
Queries the printer for its status, and returns an array of integers containing it.
|
||||
|
||||
:param mode: Integer that sets the status mode queried to the printer.
|
||||
- RT_STATUS_ONLINE: Printer status.
|
||||
- RT_STATUS_PAPER: Paper sensor.
|
||||
:rtype: array(integer)
|
||||
"""
|
||||
self._raw(mode)
|
||||
time.sleep(1)
|
||||
status = self._read()
|
||||
return status
|
||||
|
||||
def is_online(self):
|
||||
"""
|
||||
Queries the online status of the printer.
|
||||
|
||||
:returns: When online, returns ``True``; ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
status = self.query_status(RT_STATUS_ONLINE)
|
||||
if len(status) == 0:
|
||||
return False
|
||||
return not (status[0] & RT_MASK_ONLINE)
|
||||
|
||||
def paper_status(self):
|
||||
"""
|
||||
Queries the paper status of the printer.
|
||||
|
||||
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.
|
||||
|
||||
:returns: 2: Paper is adequate. 1: Paper ending. 0: No paper.
|
||||
:rtype: int
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
and assign it to the EscposIO class.
|
||||
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()
|
||||
253
venv/lib/python3.11/site-packages/escpos/exceptions.py
Normal file
253
venv/lib/python3.11/site-packages/escpos/exceptions.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" ESC/POS Exceptions classes
|
||||
|
||||
Result/Exit codes:
|
||||
|
||||
- `0` = success
|
||||
- `10` = No Barcode type defined :py:exc:`~escpos.exceptions.BarcodeTypeError`
|
||||
- `20` = Barcode size values are out of range :py:exc:`~escpos.exceptions.BarcodeSizeError`
|
||||
- `30` = Barcode text not supplied :py:exc:`~escpos.exceptions.BarcodeCodeError`
|
||||
- `40` = Image height is too large :py:exc:`~escpos.exceptions.ImageSizeError`
|
||||
- `41` = Image width is too large :py:exc:`~escpos.exceptions.ImageWidthError`
|
||||
- `50` = No string supplied to be printed :py:exc:`~escpos.exceptions.TextError`
|
||||
- `60` = Invalid pin to send Cash Drawer pulse :py:exc:`~escpos.exceptions.CashDrawerError`
|
||||
- `70` = Invalid number of tab positions :py:exc:`~escpos.exceptions.TabPosError`
|
||||
- `80` = Invalid char code :py:exc:`~escpos.exceptions.CharCodeError`
|
||||
- `90` = USB device not found :py:exc:`~escpos.exceptions.USBNotFoundError`
|
||||
- `100` = Set variable out of range :py:exc:`~escpos.exceptions.SetVariableError`
|
||||
- `200` = Configuration not found :py:exc:`~escpos.exceptions.ConfigNotFoundError`
|
||||
- `210` = Configuration syntax error :py:exc:`~escpos.exceptions.ConfigSyntaxError`
|
||||
- `220` = Configuration section not found :py:exc:`~escpos.exceptions.ConfigSectionMissingError`
|
||||
|
||||
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
""" Base class for ESC/POS errors """
|
||||
def __init__(self, msg, status=None):
|
||||
Exception.__init__(self)
|
||||
self.msg = msg
|
||||
self.resultcode = 1
|
||||
if status is not None:
|
||||
self.resultcode = status
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class BarcodeTypeError(Error):
|
||||
""" No Barcode type defined.
|
||||
|
||||
This exception indicates that no known barcode-type has been entered. The barcode-type has to be
|
||||
one of those specified in :py:meth:`escpos.escpos.Escpos.barcode`.
|
||||
The returned error code is `10`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 10
|
||||
|
||||
def __str__(self):
|
||||
return "No Barcode type is defined ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class BarcodeSizeError(Error):
|
||||
""" Barcode size is out of range.
|
||||
|
||||
This exception indicates that the values for the barcode size are out of range.
|
||||
The size of the barcode has to be in the range that is specified in :py:meth:`escpos.escpos.Escpos.barcode`.
|
||||
The resulting returncode is `20`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 20
|
||||
|
||||
def __str__(self):
|
||||
return "Barcode size is out of range ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class BarcodeCodeError(Error):
|
||||
""" No Barcode code was supplied, or it is incorrect.
|
||||
|
||||
No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode` or the the `check` parameter
|
||||
was True and the check failed.
|
||||
The returncode for this exception is `30`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 30
|
||||
|
||||
def __str__(self):
|
||||
return "No Barcode code was supplied ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class ImageSizeError(Error):
|
||||
""" Image height is longer than 255px and can't be printed.
|
||||
|
||||
The returncode for this exception is `40`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 40
|
||||
|
||||
def __str__(self):
|
||||
return "Image height is longer than 255px and can't be printed ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class ImageWidthError(Error):
|
||||
""" Image width is too large.
|
||||
|
||||
The return code for this exception is `41`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 41
|
||||
|
||||
def __str__(self):
|
||||
return "Image width is too large ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class TextError(Error):
|
||||
""" Text string must be supplied to the `text()` method.
|
||||
|
||||
This exception is raised when an empty string is passed to :py:meth:`escpos.escpos.Escpos.text`.
|
||||
The returncode for this exception is `50`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 50
|
||||
|
||||
def __str__(self):
|
||||
return "Text string must be supplied to the text() method ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class CashDrawerError(Error):
|
||||
""" Valid pin must be set in order to send pulse.
|
||||
|
||||
A valid pin number has to be passed onto the method :py:meth:`escpos.escpos.Escpos.cashdraw`.
|
||||
The returncode for this exception is `60`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 60
|
||||
|
||||
def __str__(self):
|
||||
return "Valid pin must be set to send pulse ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class TabPosError(Error):
|
||||
""" Valid tab positions must be set by using from 1 to 32 tabs, and between 1 and 255 tab size values.
|
||||
Both values multiplied must not exceed 255, since it is the maximum tab value.
|
||||
|
||||
This exception is raised by :py:meth:`escpos.escpos.Escpos.control`.
|
||||
The returncode for this exception is `70`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 70
|
||||
|
||||
def __str__(self):
|
||||
return "Valid tab positions must be in the range 0 to 16 ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class CharCodeError(Error):
|
||||
""" Valid char code must be set.
|
||||
|
||||
The supplied charcode-name in :py:meth:`escpos.escpos.Escpos.charcode` is unknown.
|
||||
Ths returncode for this exception is `80`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 80
|
||||
|
||||
def __str__(self):
|
||||
return "Valid char code must be set ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class USBNotFoundError(Error):
|
||||
""" Device wasn't found (probably not plugged in)
|
||||
|
||||
The USB device seems to be not plugged in.
|
||||
Ths returncode for this exception is `90`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 90
|
||||
|
||||
def __str__(self):
|
||||
return "USB device not found ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class SetVariableError(Error):
|
||||
""" A set method variable was out of range
|
||||
|
||||
Check set variables against minimum and maximum values
|
||||
Ths returncode for this exception is `100`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 100
|
||||
|
||||
def __str__(self):
|
||||
return "Set variable out of range ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
# Configuration errors
|
||||
|
||||
class ConfigNotFoundError(Error):
|
||||
""" The configuration file was not found
|
||||
|
||||
The default or passed configuration file could not be read
|
||||
Ths returncode for this exception is `200`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 200
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration not found ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class ConfigSyntaxError(Error):
|
||||
""" The configuration file is invalid
|
||||
|
||||
The syntax is incorrect
|
||||
Ths returncode for this exception is `210`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 210
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration syntax is invalid ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class ConfigSectionMissingError(Error):
|
||||
""" The configuration file is missing a section
|
||||
|
||||
The part of the config asked for doesn't exist in the loaded configuration
|
||||
Ths returncode for this exception is `220`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 220
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration section is missing ({msg})".format(msg=self.msg)
|
||||
129
venv/lib/python3.11/site-packages/escpos/image.py
Normal file
129
venv/lib/python3.11/site-packages/escpos/image.py
Normal file
@@ -0,0 +1,129 @@
|
||||
""" 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: MIT
|
||||
"""
|
||||
|
||||
|
||||
import math
|
||||
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)
|
||||
|
||||
# store image for eventual further processing (splitting)
|
||||
self.img_original = img_original
|
||||
|
||||
# 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()
|
||||
|
||||
def split(self, fragment_height):
|
||||
"""
|
||||
Split an image into multiple fragments after fragment_height pixels
|
||||
|
||||
:param fragment_height: height of fragment
|
||||
:return: list of PIL objects
|
||||
"""
|
||||
passes = int(math.ceil(self.height/fragment_height))
|
||||
fragments = []
|
||||
for n in range(0, passes):
|
||||
left = 0
|
||||
right = self.width
|
||||
upper = n * fragment_height
|
||||
lower = min((n + 1) * fragment_height, self.height)
|
||||
box = (left, upper, right, lower)
|
||||
fragments.append(self.img_original.crop(box))
|
||||
return fragments
|
||||
|
||||
def center(self, max_width):
|
||||
"""In-place image centering
|
||||
|
||||
:param: Maximum width in order to deduce x offset for centering
|
||||
:return: None
|
||||
"""
|
||||
old_width, height = self._im.size
|
||||
new_size = (max_width, height)
|
||||
|
||||
new_im = Image.new("1", new_size)
|
||||
paste_x = int((max_width - old_width) / 2)
|
||||
|
||||
new_im.paste(self._im, (paste_x, 0))
|
||||
|
||||
self._im = new_im
|
||||
102
venv/lib/python3.11/site-packages/escpos/katakana.py
Normal file
102
venv/lib/python3.11/site-packages/escpos/katakana.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Helpers to encode Japanese characters.
|
||||
|
||||
I doubt that this currently works correctly.
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
import jaconv
|
||||
except ImportError:
|
||||
jaconv = None
|
||||
|
||||
|
||||
def encode_katakana(text):
|
||||
"""I don't think this quite works yet."""
|
||||
encoded = []
|
||||
for char in text:
|
||||
if jaconv:
|
||||
# try to convert japanese text to half-katakanas
|
||||
char = jaconv.z2h(jaconv.hira2kata(char))
|
||||
# TODO: "the conversion may result in multiple characters"
|
||||
# If that really can happen (I am not really shure), than the string would have to be split and every single
|
||||
# character has to passed through the following lines.
|
||||
|
||||
if char in TXT_ENC_KATAKANA_MAP:
|
||||
encoded.append(TXT_ENC_KATAKANA_MAP[char])
|
||||
else:
|
||||
# TODO doesn't this discard all that is not in the map? Can we be sure that the input does contain only
|
||||
# encodable characters? We could at least throw an exception if encoding is not possible.
|
||||
pass
|
||||
return b"".join(encoded)
|
||||
|
||||
|
||||
TXT_ENC_KATAKANA_MAP = {
|
||||
# Maps UTF-8 Katakana symbols to KATAKANA Page Codes
|
||||
# TODO: has this really to be hardcoded?
|
||||
|
||||
# Half-Width Katakanas
|
||||
'。': b'\xa1',
|
||||
'「': b'\xa2',
|
||||
'」': b'\xa3',
|
||||
'、': b'\xa4',
|
||||
'・': b'\xa5',
|
||||
'ヲ': b'\xa6',
|
||||
'ァ': b'\xa7',
|
||||
'ィ': b'\xa8',
|
||||
'ゥ': b'\xa9',
|
||||
'ェ': b'\xaa',
|
||||
'ォ': b'\xab',
|
||||
'ャ': b'\xac',
|
||||
'ュ': b'\xad',
|
||||
'ョ': b'\xae',
|
||||
'ッ': b'\xaf',
|
||||
'ー': b'\xb0',
|
||||
'ア': b'\xb1',
|
||||
'イ': b'\xb2',
|
||||
'ウ': b'\xb3',
|
||||
'エ': b'\xb4',
|
||||
'オ': b'\xb5',
|
||||
'カ': b'\xb6',
|
||||
'キ': b'\xb7',
|
||||
'ク': b'\xb8',
|
||||
'ケ': b'\xb9',
|
||||
'コ': b'\xba',
|
||||
'サ': b'\xbb',
|
||||
'シ': b'\xbc',
|
||||
'ス': b'\xbd',
|
||||
'セ': b'\xbe',
|
||||
'ソ': b'\xbf',
|
||||
'タ': b'\xc0',
|
||||
'チ': b'\xc1',
|
||||
'ツ': b'\xc2',
|
||||
'テ': b'\xc3',
|
||||
'ト': b'\xc4',
|
||||
'ナ': b'\xc5',
|
||||
'ニ': b'\xc6',
|
||||
'ヌ': b'\xc7',
|
||||
'ネ': b'\xc8',
|
||||
'ノ': b'\xc9',
|
||||
'ハ': b'\xca',
|
||||
'ヒ': b'\xcb',
|
||||
'フ': b'\xcc',
|
||||
'ヘ': b'\xcd',
|
||||
'ホ': b'\xce',
|
||||
'マ': b'\xcf',
|
||||
'ミ': b'\xd0',
|
||||
'ム': b'\xd1',
|
||||
'メ': b'\xd2',
|
||||
'モ': b'\xd3',
|
||||
'ヤ': b'\xd4',
|
||||
'ユ': b'\xd5',
|
||||
'ヨ': b'\xd6',
|
||||
'ラ': b'\xd7',
|
||||
'リ': b'\xd8',
|
||||
'ル': b'\xd9',
|
||||
'レ': b'\xda',
|
||||
'ロ': b'\xdb',
|
||||
'ワ': b'\xdc',
|
||||
'ン': b'\xdd',
|
||||
'゙': b'\xde',
|
||||
'゚': b'\xdf',
|
||||
}
|
||||
268
venv/lib/python3.11/site-packages/escpos/magicencode.py
Normal file
268
venv/lib/python3.11/site-packages/escpos/magicencode.py
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import six
|
||||
from builtins import bytes
|
||||
from .constants import CODEPAGE_CHANGE
|
||||
from .codepages import code_pages, CODEPAGE
|
||||
from .exceptions import Error
|
||||
|
||||
|
||||
class Encoder(object):
|
||||
"""Takes a list of available code spaces. Picks the right one for a
|
||||
given character.
|
||||
|
||||
Note: To determine the code page, it needs to do the conversion, and
|
||||
thus already knows what the final byte in the target encoding would
|
||||
be. Nevertheless, the API of this class doesn't return the byte.
|
||||
|
||||
The caller use to do the character conversion itself.
|
||||
|
||||
$ python -m timeit -s "{u'ö':'a'}.get(u'ö')"
|
||||
100000000 loops, best of 3: 0.0133 usec per loop
|
||||
|
||||
$ python -m timeit -s "u'ö'.encode('latin1')"
|
||||
100000000 loops, best of 3: 0.0141 usec per loop
|
||||
"""
|
||||
|
||||
def __init__(self, codepage_map):
|
||||
self.codepages = codepage_map
|
||||
self.available_encodings = set(codepage_map.keys())
|
||||
self.available_characters = {}
|
||||
self.used_encodings = set()
|
||||
|
||||
def get_sequence(self, encoding):
|
||||
return int(self.codepages[encoding])
|
||||
|
||||
@staticmethod
|
||||
def _get_codepage_char_list(encoding):
|
||||
"""Get codepage character list
|
||||
|
||||
Gets characters 128-255 for a given code page, as an array.
|
||||
|
||||
:param encoding: The name of the encoding. This must appear in the CodePage list
|
||||
"""
|
||||
if 'data' in CODEPAGE:
|
||||
encodable_chars = list("".join(CODEPAGE['data']))
|
||||
assert(len(encodable_chars) == 128)
|
||||
return encodable_chars
|
||||
elif 'python_encode' in CODEPAGE:
|
||||
encodable_chars = [u" "] * 128
|
||||
for i in range(0, 128):
|
||||
codepoint = i + 128
|
||||
try:
|
||||
encodable_chars[i] = bytes([codepoint]).decode(CODEPAGE['python_encode'])
|
||||
except UnicodeDecodeError:
|
||||
# Non-encodable character, just skip it
|
||||
pass
|
||||
return encodable_chars
|
||||
raise LookupError("Can't find a known encoding for {}".format(encoding))
|
||||
|
||||
def _get_codepage_char_map(self, encoding):
|
||||
""" Get codepage character map
|
||||
|
||||
Process an encoding and return a map of UTF-characters to code points
|
||||
in this encoding.
|
||||
|
||||
This is generated once only, and returned from a cache.
|
||||
|
||||
:param encoding: The name of the encoding.
|
||||
"""
|
||||
# Skip things that were loaded previously
|
||||
if encoding in self.available_characters:
|
||||
return self.available_characters[encoding]
|
||||
|
||||
codepage_char_list = self._get_codepage_char_list(encoding)
|
||||
codepage_char_map = dict(
|
||||
(utf8, i + 128) for (i, utf8) in enumerate(codepage_char_list)
|
||||
)
|
||||
self.available_characters[encoding] = codepage_char_map
|
||||
return codepage_char_map
|
||||
|
||||
def can_encode(self, encoding, char):
|
||||
"""Determine if a character is encodeable in the given code page.
|
||||
|
||||
:param encoding: The name of the encoding.
|
||||
:param char: The character to attempt to encode.
|
||||
"""
|
||||
available_map = {}
|
||||
try:
|
||||
available_map = self._get_codepage_char_map(encoding)
|
||||
except LookupError:
|
||||
return False
|
||||
|
||||
# Decide whether this character is encodeable in this code page
|
||||
is_ascii = ord(char) < 128
|
||||
is_encodable = char in available_map
|
||||
return is_ascii or is_encodable
|
||||
|
||||
@staticmethod
|
||||
def _encode_char(char, charmap, defaultchar):
|
||||
""" Encode a single character with the given encoding map
|
||||
|
||||
:param char: char to encode
|
||||
:param charmap: dictionary for mapping characters in this code page
|
||||
"""
|
||||
if ord(char) < 128:
|
||||
return ord(char)
|
||||
if char in charmap:
|
||||
return charmap[char]
|
||||
return ord(defaultchar)
|
||||
|
||||
def encode(self, text, encoding, defaultchar='?'):
|
||||
""" Encode text under the given encoding
|
||||
|
||||
:param text: Text to encode
|
||||
:param encoding: Encoding name to use (must be defined in capabilities)
|
||||
:param defaultchar: Fallback for non-encodable characters
|
||||
"""
|
||||
codepage_char_map = self._get_codepage_char_map(encoding)
|
||||
output_bytes = bytes([self._encode_char(char, codepage_char_map, defaultchar) for char in text])
|
||||
return output_bytes
|
||||
|
||||
def __encoding_sort_func(self, item):
|
||||
key, index = item
|
||||
return (
|
||||
key in self.used_encodings,
|
||||
index
|
||||
)
|
||||
|
||||
def find_suitable_encoding(self, char):
|
||||
"""The order of our search is a specific one:
|
||||
|
||||
1. code pages that we already tried before; there is a good
|
||||
chance they might work again, reducing the search space,
|
||||
and by re-using already used encodings we might also
|
||||
reduce the number of codepage change instructiosn we have
|
||||
to send. Still, any performance gains will presumably be
|
||||
fairly minor.
|
||||
|
||||
2. code pages in lower ESCPOS slots first. Presumably, they
|
||||
are more likely to be supported, so if a printer profile
|
||||
is missing or incomplete, we might increase our change
|
||||
that the code page we pick for this character is actually
|
||||
supported.
|
||||
"""
|
||||
sorted_encodings = sorted(
|
||||
self.codepages.items(),
|
||||
key=self.__encoding_sort_func)
|
||||
|
||||
for encoding, _ in sorted_encodings:
|
||||
if self.can_encode(encoding, char):
|
||||
# This encoding worked; at it to the set of used ones.
|
||||
self.used_encodings.add(encoding)
|
||||
return encoding
|
||||
|
||||
|
||||
def split_writable_text(encoder, text, encoding):
|
||||
"""Splits off as many characters from the begnning of text as
|
||||
are writable with "encoding". Returns a 2-tuple (writable, rest).
|
||||
"""
|
||||
if not encoding:
|
||||
return None, text
|
||||
|
||||
for idx, char in enumerate(text):
|
||||
if encoder.can_encode(encoding, char):
|
||||
continue
|
||||
return text[:idx], text[idx:]
|
||||
|
||||
return text, None
|
||||
|
||||
|
||||
class MagicEncode(object):
|
||||
"""A helper that helps us to automatically switch to the right
|
||||
code page to encode any given Unicode character.
|
||||
|
||||
This will consider the printers supported codepages, according
|
||||
to the printer profile, and if a character cannot be encoded
|
||||
with the current profile, it will attempt to find a suitable one.
|
||||
|
||||
If the printer does not support a suitable code page, it can
|
||||
insert an error character.
|
||||
"""
|
||||
def __init__(self, driver, encoding=None, disabled=False,
|
||||
defaultsymbol='?', encoder=None):
|
||||
"""
|
||||
|
||||
:param driver:
|
||||
:param encoding: If you know the current encoding of the printer
|
||||
when initializing this class, set it here. If the current
|
||||
encoding is unknown, the first character emitted will be a
|
||||
codepage switch.
|
||||
:param disabled:
|
||||
:param defaultsymbol:
|
||||
:param encoder:
|
||||
"""
|
||||
if disabled and not encoding:
|
||||
raise Error('If you disable magic encode, you need to define an encoding!')
|
||||
|
||||
self.driver = driver
|
||||
self.encoder = encoder or Encoder(code_pages)
|
||||
|
||||
# self.encoding = self.encoder.get_encoding_name(encoding) if encoding else None
|
||||
self.encoding = None
|
||||
self.defaultsymbol = defaultsymbol
|
||||
self.disabled = disabled
|
||||
|
||||
def force_encoding(self, encoding):
|
||||
"""Sets a fixed encoding. The change is emitted right away.
|
||||
|
||||
From now one, this buffer will switch the code page anymore.
|
||||
However, it will still keep track of the current code page.
|
||||
"""
|
||||
if not encoding:
|
||||
self.disabled = False
|
||||
else:
|
||||
self.write_with_encoding(encoding, None)
|
||||
self.disabled = True
|
||||
|
||||
def write(self, text):
|
||||
"""Write the text, automatically switching encodings.
|
||||
"""
|
||||
|
||||
if self.disabled:
|
||||
self.write_with_encoding(self.encoding, text)
|
||||
return
|
||||
|
||||
# See how far we can go into the text with the current encoding
|
||||
to_write, text = split_writable_text(self.encoder, text, self.encoding)
|
||||
if to_write:
|
||||
self.write_with_encoding(self.encoding, to_write)
|
||||
|
||||
while text:
|
||||
# See if any of the code pages that the printer profile
|
||||
# supports can encode this character.
|
||||
encoding = self.encoder.find_suitable_encoding(text[0])
|
||||
if not encoding:
|
||||
self._handle_character_failed(text[0])
|
||||
text = text[1:]
|
||||
continue
|
||||
|
||||
# Write as much text as possible with the encoding found.
|
||||
to_write, text = split_writable_text(self.encoder, text, encoding)
|
||||
if to_write:
|
||||
self.write_with_encoding(encoding, to_write)
|
||||
|
||||
def _handle_character_failed(self, char):
|
||||
"""Called when no codepage was found to render a character.
|
||||
"""
|
||||
# Writing the default symbol via write() allows us to avoid
|
||||
# unnecesary codepage switches.
|
||||
self.write(self.defaultsymbol)
|
||||
|
||||
def write_with_encoding(self, encoding, text):
|
||||
if text is not None and type(text) is not six.text_type:
|
||||
raise Error("The supplied text has to be unicode, but is of type {type}.".format(
|
||||
type=type(text)
|
||||
))
|
||||
|
||||
# We always know the current code page; if the new codepage
|
||||
# is different, emit a change command.
|
||||
if encoding != self.encoding:
|
||||
self.encoding = encoding
|
||||
self.driver._raw(
|
||||
CODEPAGE_CHANGE +
|
||||
six.int2byte(self.encoder.get_sequence(encoding)))
|
||||
|
||||
if text:
|
||||
self.driver._raw(self.encoder.encode(text, encoding))
|
||||
389
venv/lib/python3.11/site-packages/escpos/printer.py
Normal file
389
venv/lib/python3.11/site-packages/escpos/printer.py
Normal file
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
""" This module contains the implementations of abstract base class :py:class:`Escpos`.
|
||||
|
||||
:author: `Oscar Alvarez <oscar.alvarez.montero@gmail.com>`_ and others
|
||||
:copyright: Copyright (c) 2021 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
import os
|
||||
import serial
|
||||
import socket
|
||||
import usb.core
|
||||
import usb.util
|
||||
|
||||
from .escpos import Escpos
|
||||
from .exceptions import USBNotFoundError
|
||||
|
||||
|
||||
class Usb(Escpos):
|
||||
""" USB printer
|
||||
|
||||
This class describes a printer that natively speaks USB.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Usb
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, idVendor, idProduct, usb_args=None, timeout=0, in_ep=0x82, out_ep=0x01,
|
||||
*args, **kwargs): # noqa: N803
|
||||
"""
|
||||
:param idVendor: Vendor ID
|
||||
:param idProduct: Product ID
|
||||
:param usb_args: Optional USB arguments (e.g. custom_match)
|
||||
:param timeout: Is the time limit of the USB operation. Default without timeout.
|
||||
:param in_ep: Input end point
|
||||
:param out_ep: Output end point
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.timeout = timeout
|
||||
self.in_ep = in_ep
|
||||
self.out_ep = out_ep
|
||||
|
||||
usb_args = usb_args or {}
|
||||
if idVendor:
|
||||
usb_args['idVendor'] = idVendor
|
||||
if idProduct:
|
||||
usb_args['idProduct'] = idProduct
|
||||
self.open(usb_args)
|
||||
|
||||
def open(self, usb_args):
|
||||
""" Search device on USB tree and set it as escpos device.
|
||||
|
||||
:param usb_args: USB arguments
|
||||
"""
|
||||
self.device = usb.core.find(**usb_args)
|
||||
if self.device is None:
|
||||
raise USBNotFoundError("Device not found or cable not plugged in.")
|
||||
|
||||
self.idVendor = self.device.idVendor
|
||||
self.idProduct = self.device.idProduct
|
||||
|
||||
# pyusb has three backends: libusb0, libusb1 and openusb but
|
||||
# only libusb1 backend implements the methods is_kernel_driver_active()
|
||||
# and detach_kernel_driver().
|
||||
# This helps enable this library to work on Windows.
|
||||
if self.device.backend.__module__.endswith("libusb1"):
|
||||
check_driver = None
|
||||
|
||||
try:
|
||||
check_driver = self.device.is_kernel_driver_active(0)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
if check_driver is None or check_driver:
|
||||
try:
|
||||
self.device.detach_kernel_driver(0)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
except usb.core.USBError as e:
|
||||
if check_driver is not None:
|
||||
print("Could not detatch kernel driver: {0}".format(str(e)))
|
||||
|
||||
try:
|
||||
self.device.set_configuration()
|
||||
self.device.reset()
|
||||
except usb.core.USBError as e:
|
||||
print("Could not set configuration: {0}".format(str(e)))
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self.device.write(self.out_ep, msg, self.timeout)
|
||||
|
||||
def _read(self):
|
||||
""" Reads a data buffer and returns it to the caller. """
|
||||
return self.device.read(self.in_ep, 16)
|
||||
|
||||
def close(self):
|
||||
""" Release USB interface """
|
||||
if self.device:
|
||||
usb.util.dispose_resources(self.device)
|
||||
self.device = None
|
||||
|
||||
|
||||
class Serial(Escpos):
|
||||
""" Serial printer
|
||||
|
||||
This class describes a printer that is connected by serial interface.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Serial
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, devfile="/dev/ttyS0", baudrate=9600, bytesize=8, timeout=1,
|
||||
parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE,
|
||||
xonxoff=False, dsrdtr=True, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param devfile: Device file under dev filesystem
|
||||
:param baudrate: Baud rate for serial transmission
|
||||
:param bytesize: Serial buffer size
|
||||
:param timeout: Read/Write timeout
|
||||
:param parity: Parity checking
|
||||
:param stopbits: Number of stop bits
|
||||
:param xonxoff: Software flow control
|
||||
:param dsrdtr: Hardware flow control (False to enable RTS/CTS)
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.devfile = devfile
|
||||
self.baudrate = baudrate
|
||||
self.bytesize = bytesize
|
||||
self.timeout = timeout
|
||||
self.parity = parity
|
||||
self.stopbits = stopbits
|
||||
self.xonxoff = xonxoff
|
||||
self.dsrdtr = dsrdtr
|
||||
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
""" Setup serial port and set is as escpos device """
|
||||
if self.device is not None and self.device.is_open:
|
||||
self.close()
|
||||
self.device = serial.Serial(port=self.devfile, baudrate=self.baudrate,
|
||||
bytesize=self.bytesize, parity=self.parity,
|
||||
stopbits=self.stopbits, timeout=self.timeout,
|
||||
xonxoff=self.xonxoff, dsrdtr=self.dsrdtr)
|
||||
|
||||
if self.device is not None:
|
||||
print("Serial printer enabled")
|
||||
else:
|
||||
print("Unable to open serial printer on: {0}".format(str(self.devfile)))
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self.device.write(msg)
|
||||
|
||||
def _read(self):
|
||||
""" Reads a data buffer and returns it to the caller. """
|
||||
return self.device.read(16)
|
||||
|
||||
def close(self):
|
||||
""" Close Serial interface """
|
||||
if self.device is not None and self.device.is_open:
|
||||
self.device.flush()
|
||||
self.device.close()
|
||||
|
||||
|
||||
class Network(Escpos):
|
||||
""" Network printer
|
||||
|
||||
This class is used to attach to a networked printer. You can also use this
|
||||
in order to attach to a printer that is forwarded with ``socat``.
|
||||
|
||||
If you have a local printer on parallel port ``/dev/usb/lp0`` then you could start ``socat`` with:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
socat -u TCP4-LISTEN:4242,reuseaddr,fork OPEN:/dev/usb/lp0
|
||||
|
||||
Then you should be able to attach to port ``4242`` with this class.
|
||||
Otherwise the normal usecase would be to have a printer with ethernet interface. This type of printer should
|
||||
work the same with this class. For the address of the printer check its manuals.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Network
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host, port=9100, timeout=60, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param host: Printer's hostname or IP address
|
||||
:param port: Port to write to
|
||||
:param timeout: timeout in seconds for the socket-library
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
""" Open TCP socket with ``socket``-library and set it as escpos device """
|
||||
self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.device.settimeout(self.timeout)
|
||||
self.device.connect((self.host, self.port))
|
||||
|
||||
if self.device is None:
|
||||
print("Could not open socket for {0}".format(self.host))
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self.device.sendall(msg)
|
||||
|
||||
def _read(self):
|
||||
""" Read data from the TCP socket """
|
||||
|
||||
return self.device.recv(16)
|
||||
|
||||
def close(self):
|
||||
""" Close TCP connection """
|
||||
if self.device is not None:
|
||||
self.device.shutdown(socket.SHUT_RDWR)
|
||||
self.device.close()
|
||||
|
||||
|
||||
class File(Escpos):
|
||||
""" Generic file printer
|
||||
|
||||
This class is used for parallel port printer or other printers that are directly attached to the filesystem.
|
||||
Note that you should stay away from using USB-to-Parallel-Adapter since they are unreliable
|
||||
and produce arbitrary errors.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.File
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, devfile="/dev/usb/lp0", auto_flush=True, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param devfile: Device file under dev filesystem
|
||||
:param auto_flush: automatically call flush after every call of _raw()
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.devfile = devfile
|
||||
self.auto_flush = auto_flush
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
""" Open system file """
|
||||
self.device = open(self.devfile, "wb")
|
||||
|
||||
if self.device is None:
|
||||
print("Could not open the specified file {0}".format(self.devfile))
|
||||
|
||||
def flush(self):
|
||||
""" Flush printing content """
|
||||
self.device.flush()
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self.device.write(msg)
|
||||
if self.auto_flush:
|
||||
self.flush()
|
||||
|
||||
def close(self):
|
||||
""" Close system file """
|
||||
if self.device is not None:
|
||||
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):
|
||||
"""
|
||||
"""
|
||||
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)
|
||||
|
||||
def clear(self):
|
||||
""" Clear the buffer of the printer
|
||||
|
||||
This method can be called if you send the contents to a physical printer
|
||||
and want to use the Dummy printer for new output.
|
||||
"""
|
||||
del self._output_list[:]
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
_WIN32PRINT = False
|
||||
if os.name == 'nt':
|
||||
try:
|
||||
import win32print
|
||||
_WIN32PRINT = True
|
||||
except ImportError:
|
||||
print('Warning: missing lib... win32print')
|
||||
|
||||
if _WIN32PRINT:
|
||||
class Win32Raw(Escpos):
|
||||
def __init__(self, printer_name=None, *args, **kwargs):
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
if printer_name is not None:
|
||||
self.printer_name = printer_name
|
||||
else:
|
||||
self.printer_name = win32print.GetDefaultPrinter()
|
||||
self.hPrinter = None
|
||||
|
||||
def open(self, job_name="python-escpos"):
|
||||
if self.printer_name is None:
|
||||
raise Exception("Printer not found")
|
||||
self.hPrinter = win32print.OpenPrinter(self.printer_name)
|
||||
self.current_job = win32print.StartDocPrinter(self.hPrinter, 1, (job_name, None, "RAW"))
|
||||
win32print.StartPagePrinter(self.hPrinter)
|
||||
|
||||
def close(self):
|
||||
if not self.hPrinter:
|
||||
return
|
||||
win32print.EndPagePrinter(self.hPrinter)
|
||||
win32print.EndDocPrinter(self.hPrinter)
|
||||
win32print.ClosePrinter(self.hPrinter)
|
||||
self.hPrinter = None
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
if self.printer_name is None:
|
||||
raise Exception("Printer not found")
|
||||
if self.hPrinter is None:
|
||||
raise Exception("Printer job not opened")
|
||||
win32print.WritePrinter(self.hPrinter, msg)
|
||||
5
venv/lib/python3.11/site-packages/escpos/version.py
Normal file
5
venv/lib/python3.11/site-packages/escpos/version.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# coding: utf-8
|
||||
# file generated by setuptools_scm
|
||||
# don't change, don't track in version control
|
||||
|
||||
version = '2.0.0'
|
||||
Reference in New Issue
Block a user