mirror of
https://github.com/python-escpos/python-escpos
synced 2025-09-13 09:09:58 +00:00
SETUP move code to src
This way we can ensure that the packaged code is tested.
See https://hynek.me/articles/testing-packaging/ or c62a78c015
+ DOC adapt doc to new structure and test doc with travis
This commit is contained in:
1
src/escpos/__init__.py
Normal file
1
src/escpos/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__all__ = ["constants", "escpos", "exceptions", "printer"]
|
545
src/escpos/cli.py
Normal file
545
src/escpos/cli.py
Normal file
@@ -0,0 +1,545 @@
|
||||
#!/usr/bin/env python
|
||||
""" 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 convience.
|
||||
|
||||
It requires you to have a configuration file. See documentation for details.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import six
|
||||
from . import config
|
||||
|
||||
|
||||
# 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,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'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='Altnerate 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',
|
||||
)
|
||||
|
||||
# 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',
|
||||
)
|
||||
|
||||
# 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 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')
|
||||
|
||||
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 specificed 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()
|
118
src/escpos/config.py
Normal file
118
src/escpos/config.py
Normal file
@@ -0,0 +1,118 @@
|
||||
""" ESC/POS configuration manager.
|
||||
|
||||
This module contains the implentations of abstract base class :py:class:`Config`.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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
|
216
src/escpos/constants.py
Normal file
216
src/escpos/constants.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# -*- 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 Bashlinux
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import six
|
||||
|
||||
# Control characters
|
||||
# as labelled in http://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_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
|
||||
|
||||
# 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
|
||||
|
||||
# Text format
|
||||
# TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll
|
||||
# Printers" and tidy up this stuff too.
|
||||
TXT_FLIP_ON = ESC + b'\x7b\x01'
|
||||
TXT_FLIP_OFF = ESC + b'\x7b\x00'
|
||||
TXT_SMOOTH_ON = GS + b'\x62\x01'
|
||||
TXT_SMOOTH_OFF = GS + b'\x62\x00'
|
||||
TXT_SIZE = GS + b'!'
|
||||
TXT_WIDTH = {1: 0x00,
|
||||
2: 0x10,
|
||||
3: 0x20,
|
||||
4: 0x30,
|
||||
5: 0x40,
|
||||
6: 0x50,
|
||||
7: 0x60,
|
||||
8: 0x70}
|
||||
TXT_HEIGHT = {1: 0x00,
|
||||
2: 0x01,
|
||||
3: 0x02,
|
||||
4: 0x03,
|
||||
5: 0x04,
|
||||
6: 0x05,
|
||||
7: 0x06,
|
||||
8: 0x07}
|
||||
TXT_NORMAL = ESC + b'!\x00' # Normal text
|
||||
TXT_2HEIGHT = ESC + b'!\x10' # Double height text
|
||||
TXT_2WIDTH = ESC + b'!\x20' # Double width text
|
||||
TXT_4SQUARE = ESC + b'!\x30' # Quad area text
|
||||
TXT_UNDERL_OFF = ESC + b'\x2d\x00' # Underline font OFF
|
||||
TXT_UNDERL_ON = ESC + b'\x2d\x01' # Underline font 1-dot ON
|
||||
TXT_UNDERL2_ON = ESC + b'\x2d\x02' # Underline font 2-dot ON
|
||||
TXT_BOLD_OFF = ESC + b'\x45\x00' # Bold font OFF
|
||||
TXT_BOLD_ON = ESC + b'\x45\x01' # Bold font ON
|
||||
TXT_FONT_A = ESC + b'\x4d\x00' # Font type A
|
||||
TXT_FONT_B = ESC + b'\x4d\x01' # Font type B
|
||||
TXT_ALIGN_LT = ESC + b'\x61\x00' # Left justification
|
||||
TXT_ALIGN_CT = ESC + b'\x61\x01' # Centering
|
||||
TXT_ALIGN_RT = ESC + b'\x61\x02' # Right justification
|
||||
TXT_INVERT_ON = GS + b'\x42\x01' # Inverse Printing ON
|
||||
TXT_INVERT_OFF = GS + b'\x42\x00' # Inverse Printing OFF
|
||||
|
||||
# Char code table
|
||||
CHARCODE_PC437 = ESC + b'\x74\x00' # USA: Standard Europe
|
||||
CHARCODE_JIS = ESC + b'\x74\x01' # Japanese Katakana
|
||||
CHARCODE_PC850 = ESC + b'\x74\x02' # Multilingual
|
||||
CHARCODE_PC860 = ESC + b'\x74\x03' # Portuguese
|
||||
CHARCODE_PC863 = ESC + b'\x74\x04' # Canadian-French
|
||||
CHARCODE_PC865 = ESC + b'\x74\x05' # Nordic
|
||||
CHARCODE_WEU = ESC + b'\x74\x06' # Simplified Kanji, Hirakana
|
||||
CHARCODE_GREEK = ESC + b'\x74\x07' # Simplified Kanji
|
||||
CHARCODE_HEBREW = ESC + b'\x74\x08' # Simplified Kanji
|
||||
CHARCODE_PC1252 = ESC + b'\x74\x11' # Western European Windows Code Set
|
||||
CHARCODE_PC866 = ESC + b'\x74\x12' # Cirillic #2
|
||||
CHARCODE_PC852 = ESC + b'\x74\x13' # Latin 2
|
||||
CHARCODE_PC858 = ESC + b'\x74\x14' # Euro
|
||||
CHARCODE_THAI42 = ESC + b'\x74\x15' # Thai character code 42
|
||||
CHARCODE_THAI11 = ESC + b'\x74\x16' # Thai character code 11
|
||||
CHARCODE_THAI13 = ESC + b'\x74\x17' # Thai character code 13
|
||||
CHARCODE_THAI14 = ESC + b'\x74\x18' # Thai character code 14
|
||||
CHARCODE_THAI16 = ESC + b'\x74\x19' # Thai character code 16
|
||||
CHARCODE_THAI17 = ESC + b'\x74\x1a' # Thai character code 17
|
||||
CHARCODE_THAI18 = ESC + b'\x74\x1b' # Thai character code 18
|
||||
|
||||
# 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),
|
||||
# These are all the same barcode, but using different charcter sets
|
||||
'CODE128A': _SET_BARCODE_TYPE(73) + b'{A', # CODE128 character set A
|
||||
'CODE128B': _SET_BARCODE_TYPE(73) + b'{B', # CODE128 character set B
|
||||
'CODE128C': _SET_BARCODE_TYPE(73) + b'{C', # CODE128 character set C
|
||||
'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_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
|
||||
|
||||
# Printing Density
|
||||
PD_N50 = GS + b'\x7c\x00' # Printing Density -50%
|
||||
PD_N37 = GS + b'\x7c\x01' # Printing Density -37.5%
|
||||
PD_N25 = GS + b'\x7c\x02' # Printing Density -25%
|
||||
PD_N12 = GS + b'\x7c\x03' # Printing Density -12.5%
|
||||
PD_0 = GS + b'\x7c\x04' # Printing Density 0%
|
||||
PD_P50 = GS + b'\x7c\x08' # Printing Density +50%
|
||||
PD_P37 = GS + b'\x7c\x07' # Printing Density +37.5%
|
||||
PD_P25 = GS + b'\x7c\x06' # Printing Density +25%
|
||||
PD_P12 = GS + b'\x7c\x05' # Printing Density +12.5%
|
746
src/escpos/escpos.py
Normal file
746
src/escpos/escpos.py
Normal file
@@ -0,0 +1,746 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Main class
|
||||
|
||||
This module contains the abstract base class :py:class:`Escpos`.
|
||||
|
||||
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012 Bashlinux
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import qrcode
|
||||
import textwrap
|
||||
|
||||
from .constants import *
|
||||
from .exceptions import *
|
||||
|
||||
from abc import ABCMeta, abstractmethod # abstract base class support
|
||||
from escpos.image import EscposImage
|
||||
|
||||
|
||||
@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
|
||||
codepage = None
|
||||
|
||||
def __init__(self, columns=32):
|
||||
""" Initialize ESCPOS Printer
|
||||
|
||||
:param columns: Text columns used by the printer. Defaults to 32."""
|
||||
self.columns = columns
|
||||
|
||||
def __del__(self):
|
||||
""" call self.close upon deletion """
|
||||
self.close()
|
||||
|
||||
@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 image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster"):
|
||||
""" 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
|
||||
|
||||
: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`
|
||||
|
||||
"""
|
||||
im = EscposImage(img_source)
|
||||
|
||||
if impl == "bitImageRaster":
|
||||
# GS v 0, raster format bit image
|
||||
density_byte = (0 if high_density_horizontal else 1) + (0 if high_density_vertical else 2)
|
||||
header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) + self._int_low_high(im.height, 2)
|
||||
self._raw(header + im.to_raster_format())
|
||||
|
||||
if impl == "graphics":
|
||||
# GS ( L raster format graphics
|
||||
img_header = self._int_low_high(im.width, 2) + self._int_low_high(im.height, 2)
|
||||
tone = b'0'
|
||||
colors = b'1'
|
||||
ym = six.int2byte(1 if high_density_vertical else 2)
|
||||
xm = six.int2byte(1 if high_density_horizontal else 2)
|
||||
header = tone + xm + ym + colors + img_header
|
||||
raster_data = im.to_raster_format()
|
||||
self._image_send_graphics_data(b'0', b'p', header + raster_data)
|
||||
self._image_send_graphics_data(b'0', b'2', b'')
|
||||
|
||||
if impl == "bitImageColumn":
|
||||
# ESC *, column format bit image
|
||||
density_byte = (1 if high_density_horizontal else 0) + (32 if high_density_vertical else 0)
|
||||
header = ESC + b"*" + six.int2byte(density_byte) + self._int_low_high(im.width, 2)
|
||||
outp = [ESC + b"3" + six.int2byte(16)] # Adjust line-feed size
|
||||
for blob in im.to_column_format(high_density_vertical):
|
||||
outp.append(header + blob + b"\n")
|
||||
outp.append(ESC + b"2") # Reset line-feed size
|
||||
self._raw(b''.join(outp))
|
||||
|
||||
def _image_send_graphics_data(self, m, fn, data):
|
||||
"""
|
||||
Wrapper for GS ( L, to calculate and send correct data length.
|
||||
|
||||
:param m: Modifier//variant for function. Usually '0'
|
||||
:param fn: Function number to use, as byte
|
||||
:param data: Data to send
|
||||
"""
|
||||
header = self._int_low_high(len(data) + 2, 2)
|
||||
self._raw(GS + b'(L' + header + m + fn + data)
|
||||
|
||||
def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False):
|
||||
""" Print QR Code for the provided string
|
||||
|
||||
: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)
|
||||
"""
|
||||
# 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.image(im)
|
||||
return
|
||||
# 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 byes")
|
||||
if not 0 <= inp_number <= max_input:
|
||||
raise ValueError("Number too large. Can only output up to {0} in {1} byes".format(max_input, out_bytes))
|
||||
outp = b''
|
||||
for _ in range(0, out_bytes):
|
||||
outp += six.int2byte(inp_number % 256)
|
||||
inp_number //= 256
|
||||
return outp
|
||||
|
||||
def charcode(self, code):
|
||||
""" Set Character Code Table
|
||||
|
||||
Sends the control sequence from :py:mod:`escpos.constants` to the printer
|
||||
with :py:meth:`escpos.printer.'implementation'._raw()`.
|
||||
|
||||
:param code: Name of CharCode
|
||||
:raises: :py:exc:`~escpos.exceptions.CharCodeError`
|
||||
"""
|
||||
# TODO improve this (rather unhandy code)
|
||||
# TODO check the codepages
|
||||
if code.upper() == "USA":
|
||||
self._raw(CHARCODE_PC437)
|
||||
self.codepage = 'cp437'
|
||||
elif code.upper() == "JIS":
|
||||
self._raw(CHARCODE_JIS)
|
||||
self.codepage = 'cp932'
|
||||
elif code.upper() == "MULTILINGUAL":
|
||||
self._raw(CHARCODE_PC850)
|
||||
self.codepage = 'cp850'
|
||||
elif code.upper() == "PORTUGUESE":
|
||||
self._raw(CHARCODE_PC860)
|
||||
self.codepage = 'cp860'
|
||||
elif code.upper() == "CA_FRENCH":
|
||||
self._raw(CHARCODE_PC863)
|
||||
self.codepage = 'cp863'
|
||||
elif code.upper() == "NORDIC":
|
||||
self._raw(CHARCODE_PC865)
|
||||
self.codepage = 'cp865'
|
||||
elif code.upper() == "WEST_EUROPE":
|
||||
self._raw(CHARCODE_WEU)
|
||||
self.codepage = 'latin_1'
|
||||
elif code.upper() == "GREEK":
|
||||
self._raw(CHARCODE_GREEK)
|
||||
self.codepage = 'cp737'
|
||||
elif code.upper() == "HEBREW":
|
||||
self._raw(CHARCODE_HEBREW)
|
||||
self.codepage = 'cp862'
|
||||
# elif code.upper() == "LATVIAN": # this is not listed in the constants
|
||||
# self._raw(CHARCODE_PC755)
|
||||
# self.codepage = 'cp'
|
||||
elif code.upper() == "WPC1252":
|
||||
self._raw(CHARCODE_PC1252)
|
||||
self.codepage = 'cp1252'
|
||||
elif code.upper() == "CIRILLIC2":
|
||||
self._raw(CHARCODE_PC866)
|
||||
self.codepage = 'cp866'
|
||||
elif code.upper() == "LATIN2":
|
||||
self._raw(CHARCODE_PC852)
|
||||
self.codepage = 'cp852'
|
||||
elif code.upper() == "EURO":
|
||||
self._raw(CHARCODE_PC858)
|
||||
self.codepage = 'cp858'
|
||||
elif code.upper() == "THAI42":
|
||||
self._raw(CHARCODE_THAI42)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI11":
|
||||
self._raw(CHARCODE_THAI11)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI13":
|
||||
self._raw(CHARCODE_THAI13)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI14":
|
||||
self._raw(CHARCODE_THAI14)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI16":
|
||||
self._raw(CHARCODE_THAI16)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI17":
|
||||
self._raw(CHARCODE_THAI17)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI18":
|
||||
self._raw(CHARCODE_THAI18)
|
||||
self.codepage = 'cp874'
|
||||
else:
|
||||
raise CharCodeError()
|
||||
|
||||
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True, function_type="A"):
|
||||
""" 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. Currently you have to check manually whether your barcode text is correct. 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.
|
||||
|
||||
.. todo:: Add a method to check barcode codes. Alternatively or as an addition write explanations about each
|
||||
barcode-type. Research whether the check digits can be computed autmatically.
|
||||
|
||||
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.
|
||||
*default*: A
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`,
|
||||
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
|
||||
:py:exc:`~escpos.exceptions.BarcodeCodeError`
|
||||
"""
|
||||
# Align Bar Code()
|
||||
if align_ct:
|
||||
self._raw(TXT_ALIGN_CT)
|
||||
# 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)
|
||||
|
||||
bc_types = BARCODE_TYPES[function_type.upper()]
|
||||
if bc.upper() not in bc_types.keys():
|
||||
# TODO: Raise a better error, or fix the message of this error type
|
||||
raise BarcodeTypeError("Barcode type {bc} not valid for barcode function type {function_type}".format(
|
||||
bc=bc,
|
||||
function_type=function_type,
|
||||
))
|
||||
|
||||
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 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`
|
||||
"""
|
||||
if txt:
|
||||
if self.codepage:
|
||||
self._raw(txt.encode(self.codepage))
|
||||
else:
|
||||
self._raw(txt.encode())
|
||||
else:
|
||||
# TODO: why is it problematic to print an empty string?
|
||||
raise TextError()
|
||||
|
||||
def block_text(self, txt, columns=None):
|
||||
""" Text is printed wrapped to specified columns
|
||||
|
||||
Text has to be encoded in unicode.
|
||||
|
||||
:param txt: text to be printed
|
||||
:param columns: amount of columns
|
||||
:return: None
|
||||
"""
|
||||
col_count = self.columns if columns is None else columns
|
||||
self.text(textwrap.fill(txt, col_count))
|
||||
|
||||
def set(self, align='left', font='a', text_type='normal', width=1, height=1, density=9, invert=False, smooth=False,
|
||||
flip=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 type, possible values are A or B, *default*: A
|
||||
:param text_type: text type, possible values are:
|
||||
|
||||
* B for bold
|
||||
* U for underlined
|
||||
* U2 for underlined, version 2
|
||||
* BU for bold and underlined
|
||||
* BU2 for bold and underlined, version 2
|
||||
* NORMAL for normal text
|
||||
|
||||
*default*: NORMAL
|
||||
:param width: text width multiplier, decimal range 1-8, *default*: 1
|
||||
:param height: text height multiplier, 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 invert: bool
|
||||
"""
|
||||
# Width
|
||||
if height == 2 and width == 2:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_4SQUARE)
|
||||
elif height == 2 and width == 1:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_2HEIGHT)
|
||||
elif width == 2 and height == 1:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_2WIDTH)
|
||||
elif width == 1 and height == 1:
|
||||
self._raw(TXT_NORMAL)
|
||||
elif 1 <= width <= 8 and 1 <= height <= 8 and isinstance(width, int) and isinstance(height, int):
|
||||
self._raw(TXT_SIZE + six.int2byte(TXT_WIDTH[width] + TXT_HEIGHT[height]))
|
||||
else:
|
||||
raise SetVariableError()
|
||||
# Upside down
|
||||
if flip:
|
||||
self._raw(TXT_FLIP_ON)
|
||||
else:
|
||||
self._raw(TXT_FLIP_OFF)
|
||||
# Smoothing
|
||||
if smooth:
|
||||
self._raw(TXT_SMOOTH_ON)
|
||||
else:
|
||||
self._raw(TXT_SMOOTH_OFF)
|
||||
# Type
|
||||
if text_type.upper() == "B":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL_OFF)
|
||||
elif text_type.upper() == "U":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL_ON)
|
||||
elif text_type.upper() == "U2":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL2_ON)
|
||||
elif text_type.upper() == "BU":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL_ON)
|
||||
elif text_type.upper() == "BU2":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL2_ON)
|
||||
elif text_type.upper() == "NORMAL":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL_OFF)
|
||||
# Font
|
||||
if font.upper() == "B":
|
||||
self._raw(TXT_FONT_B)
|
||||
else: # DEFAULT FONT: A
|
||||
self._raw(TXT_FONT_A)
|
||||
# Align
|
||||
if align.upper() == "CENTER":
|
||||
self._raw(TXT_ALIGN_CT)
|
||||
elif align.upper() == "RIGHT":
|
||||
self._raw(TXT_ALIGN_RT)
|
||||
elif align.upper() == "LEFT":
|
||||
self._raw(TXT_ALIGN_LT)
|
||||
# Density
|
||||
if density == 0:
|
||||
self._raw(PD_N50)
|
||||
elif density == 1:
|
||||
self._raw(PD_N37)
|
||||
elif density == 2:
|
||||
self._raw(PD_N25)
|
||||
elif density == 3:
|
||||
self._raw(PD_N12)
|
||||
elif density == 4:
|
||||
self._raw(PD_0)
|
||||
elif density == 5:
|
||||
self._raw(PD_P12)
|
||||
elif density == 6:
|
||||
self._raw(PD_P25)
|
||||
elif density == 7:
|
||||
self._raw(PD_P37)
|
||||
elif density == 8:
|
||||
self._raw(PD_P50)
|
||||
else: # DEFAULT: DOES NOTHING
|
||||
pass
|
||||
# Invert Printing
|
||||
if invert:
|
||||
self._raw(TXT_INVERT_ON)
|
||||
else:
|
||||
self._raw(TXT_INVERT_OFF)
|
||||
|
||||
def cut(self, mode=''):
|
||||
""" 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.
|
||||
|
||||
.. todo:: Check this function on TM-T88II.
|
||||
|
||||
:param mode: set to 'PART' for a partial cut
|
||||
"""
|
||||
# Fix the size between last line and cut
|
||||
# TODO: handle this with a line feed
|
||||
self._raw(b"\n\n\n\n\n\n")
|
||||
if mode.upper() == "PART":
|
||||
self._raw(PAPER_PART_CUT)
|
||||
else: # DEFAULT MODE: FULL CUT
|
||||
self._raw(PAPER_FULL_CUT)
|
||||
|
||||
def cashdraw(self, pin):
|
||||
""" Send pulse to kick the cash drawer
|
||||
|
||||
Kick cash drawer on pin 2 or pin 5 according to parameter.
|
||||
|
||||
:param pin: pin number, 2 or 5
|
||||
:raises: :py:exc:`~escpos.exceptions.CashDrawerError`
|
||||
"""
|
||||
if pin == 2:
|
||||
self._raw(CD_KICK_2)
|
||||
elif pin == 5:
|
||||
self._raw(CD_KICK_5)
|
||||
else:
|
||||
raise CashDrawerError()
|
||||
|
||||
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 control(self, ctl, pos=4):
|
||||
""" 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 pos: integer between 1 and 16, controls the horizontal tab position
|
||||
:raises: :py:exc:`~escpos.exceptions.TabPosError`
|
||||
"""
|
||||
# Set tab positions
|
||||
if not (1 <= pos <= 16):
|
||||
raise TabPosError()
|
||||
else:
|
||||
self._raw(CTL_SET_HT + six.int2byte(pos))
|
||||
# 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":
|
||||
self._raw(CTL_HT)
|
||||
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)
|
||||
|
||||
|
||||
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 EsposIO-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()
|
241
src/escpos/exceptions.py
Normal file
241
src/escpos/exceptions.py
Normal file
@@ -0,0 +1,241 @@
|
||||
# -*- 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`
|
||||
- `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 Bashlinux
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
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.
|
||||
|
||||
No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode`.
|
||||
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"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
class TabPosError(Error):
|
||||
""" Valid tab positions must be in the range 0 to 16.
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
# 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)
|
90
src/escpos/image.py
Normal file
90
src/escpos/image.py
Normal file
@@ -0,0 +1,90 @@
|
||||
""" Image format handling class
|
||||
|
||||
This module contains the image format handler :py:class:`EscposImage`.
|
||||
|
||||
:author: `Michael Billington <michael.billington@gmail.com>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 Michael Billington <michael.billington@gmail.com>
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
|
||||
class EscposImage(object):
|
||||
"""
|
||||
Load images in, and output ESC/POS formats.
|
||||
|
||||
The class is designed to efficiently delegate image processing to
|
||||
PIL, rather than spend CPU cycles looping over pixels.
|
||||
"""
|
||||
|
||||
def __init__(self, img_source):
|
||||
"""
|
||||
Load in an image
|
||||
|
||||
:param img_source: PIL.Image, or filename to load one from.
|
||||
"""
|
||||
if isinstance(img_source, Image.Image):
|
||||
img_original = img_source
|
||||
else:
|
||||
img_original = Image.open(img_source)
|
||||
|
||||
# Convert to white RGB background, paste over white background
|
||||
# to strip alpha.
|
||||
img_original = img_original.convert('RGBA')
|
||||
im = Image.new("RGB", img_original.size, (255, 255, 255))
|
||||
im.paste(img_original, mask=img_original.split()[3])
|
||||
# Convert down to greyscale
|
||||
im = im.convert("L")
|
||||
# Invert: Only works on 'L' images
|
||||
im = ImageOps.invert(im)
|
||||
# Pure black and white
|
||||
self._im = im.convert("1")
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""
|
||||
Width of image in pixels
|
||||
"""
|
||||
width_pixels, _ = self._im.size
|
||||
return width_pixels
|
||||
|
||||
@property
|
||||
def width_bytes(self):
|
||||
"""
|
||||
Width of image if you use 8 pixels per byte and 0-pad at the end.
|
||||
"""
|
||||
return (self.width + 7) >> 3
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""
|
||||
Height of image in pixels
|
||||
"""
|
||||
_, height_pixels = self._im.size
|
||||
return height_pixels
|
||||
|
||||
def to_column_format(self, high_density_vertical=True):
|
||||
"""
|
||||
Extract slices of an image as equal-sized blobs of column-format data.
|
||||
|
||||
:param high_density_vertical: Printed line height in dots
|
||||
"""
|
||||
im = self._im.transpose(Image.ROTATE_270).transpose(Image.FLIP_LEFT_RIGHT)
|
||||
line_height = 24 if high_density_vertical else 8
|
||||
width_pixels, height_pixels = im.size
|
||||
top = 0
|
||||
left = 0
|
||||
while left < width_pixels:
|
||||
box = (left, top, left + line_height, top + height_pixels)
|
||||
im_slice = im.transform((line_height, height_pixels), Image.EXTENT, box)
|
||||
im_bytes = im_slice.tobytes()
|
||||
yield(im_bytes)
|
||||
left += line_height
|
||||
|
||||
def to_raster_format(self):
|
||||
"""
|
||||
Convert image to raster-format binary
|
||||
"""
|
||||
return self._im.tobytes()
|
300
src/escpos/printer.py
Normal file
300
src/escpos/printer.py
Normal file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
""" This module contains the implementations of abstract base class :py:class:`Escpos`.
|
||||
|
||||
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012 Bashlinux
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
import serial
|
||||
import socket
|
||||
|
||||
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, interface=0, in_ep=0x82, out_ep=0x01, *args, **kwargs):
|
||||
"""
|
||||
:param idVendor: Vendor ID
|
||||
:param idProduct: Product ID
|
||||
:param interface: USB device interface
|
||||
:param in_ep: Input end point
|
||||
:param out_ep: Output end point
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.idVendor = idVendor
|
||||
self.idProduct = idProduct
|
||||
self.interface = interface
|
||||
self.in_ep = in_ep
|
||||
self.out_ep = out_ep
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
""" Search device on USB tree and set it as escpos device """
|
||||
self.device = usb.core.find(idVendor=self.idVendor, idProduct=self.idProduct)
|
||||
if self.device is None:
|
||||
raise USBNotFoundError("Device not found or cable not plugged in.")
|
||||
|
||||
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 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.interface)
|
||||
|
||||
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 """
|
||||
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 close(self):
|
||||
""" Close Serial interface """
|
||||
if self.device is not None:
|
||||
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 close(self):
|
||||
""" Close TCP connection """
|
||||
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", *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param devfile : Device file under dev filesystem
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.devfile = devfile
|
||||
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)
|
||||
|
||||
def close(self):
|
||||
""" Close system file """
|
||||
self.device.flush()
|
||||
self.device.close()
|
||||
|
||||
|
||||
class Dummy(Escpos):
|
||||
""" Dummy printer
|
||||
|
||||
This class is used for saving commands to a variable, for use in situations where
|
||||
there is no need to send commands to an actual printer. This includes
|
||||
generating print jobs for later use, or testing output.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Dummy
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
"""
|
||||
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 close(self):
|
||||
pass
|
Reference in New Issue
Block a user