Merge branch 'capabilities' into text-encoding

This commit is contained in:
Michael Elsdörfer 2016-08-30 13:36:53 +02:00
commit b37f4fc8cc
13 changed files with 277 additions and 276 deletions

View File

@ -15,6 +15,21 @@ contributors
- Patrick Kanzler (with code by Frédéric Van der Essen)
2016-08-26 - Version 2.2.0 - "Fate Amenable To Change"
------------------------------------------------------
changes
^^^^^^^
- fix improper API-use in qrcode()
- change setup.py shebang to make it compatible with virtualenvs.
- add constants for sheet mode and colors
- support changing the linespacing
contributors
^^^^^^^^^^^^
- Michael Elsdörfer
- Patrick Kanzler
2016-08-10 - Version 2.1.3 - "Ethics Gradient"
----------------------------------------------

View File

@ -54,14 +54,14 @@ The basic usage is:
.. code:: python
from escpos import *
from escpos.printer import Usb
""" Seiko Epson Corp. Receipt Printer M129 Definitions (EPSON TM-T88IV) """
Epson = escpos.Escpos(0x04b8,0x0202,0)
Epson.text("Hello World\n")
Epson.image("logo.gif")
Epson.barcode('1324354657687','EAN13',64,2,'','')
Epson.cut()
p = Usb(0x04b8,0x0202,0)
p.text("Hello World\n")
p.image("logo.gif")
p.barcode('1324354657687','EAN13',64,2,'','')
p.cut()
The full project-documentation is available on `Read the Docs <https://python-escpos.readthedocs.io>`_.

View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
import os
import sys
@ -83,7 +83,8 @@ setup(
platforms='any',
package_dir={"": "src"},
packages=find_packages(where="src", exclude=["tests", "tests.*"]),
package_data={'': ['COPYING']},
package_data={'': ['COPYING', 'src/escpos/capabilities.json']},
include_package_data=True,
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
@ -116,7 +117,7 @@ setup(
setup_requires=[
'setuptools_scm',
],
tests_require=['tox', 'nose', 'scripttest', 'mock', 'hypothesis'],
tests_require=['tox', 'pytest', 'pytest-cov', 'nose', 'scripttest', 'mock', 'hypothesis'],
cmdclass={'test': Tox},
entry_points={
'console_scripts': [

File diff suppressed because one or more lines are too long

View File

@ -1,40 +1,61 @@
import re
import six
from os import path
import yaml
with open(path.join(path.dirname(__file__), 'capabilities.yml')) as f:
PROFILES = yaml.load(f)
# Load external printer database
with open(path.join(path.dirname(__file__), 'capabilities.json')) as f:
CAPABILITIES = yaml.load(f)
PROFILES = CAPABILITIES['profiles']
ENCODINGS = CAPABILITIES['encodings']
class Profile(object):
class NotSupported(Exception):
pass
BARCODE_B = 'barcodeB'
class BaseProfile(object):
"""This respresents a printer profile.
A printer profile knows about the number of columns, supported
features, colors and more.
"""
profile_data = {}
def __init__(self, columns=None):
self.default_columns = columns
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(
'"%s" is not a valid font in the current profile' % font)
return font
def get_columns(self, font):
""" Return the number of columns for the given font.
"""
if self.default_columns:
return self.default_columns
font = self.get_font(font)
return self.fonts[six.text_type(font)]['columns']
if 'columnConfigs' in self.profile_data:
columns_def = self.columnConfigs[self.defaultColumnConfig]
elif 'columns' in self.profile_data:
columns_def = self.columns
if isinstance(columns_def, int):
return columns_def
return columns_def[font]
def supports(self, feature):
"""Return true/false for the given feature.
"""
return self.features.get(feature)
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
@ -42,15 +63,19 @@ def get_profile(name=None, **kwargs):
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 not name in CLASS_CACHE:
profile_data = resolve_profile_data(name)
class_name = '%sProfile' % clean(name)
new_class = type(class_name, (Profile,), {'profile_data': profile_data})
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]
@ -64,20 +89,21 @@ def clean(s):
return str(s)
def resolve_profile_data(name):
data = PROFILES[name]
inherits = data.get('inherits')
if not inherits:
return data
# For users, who want to provide their profile
class Profile(get_profile_class('default')):
def __init__(self, columns=None, features={}):
super(Profile, self).__init__()
self.columns = columns
self.features = features
def get_columns(self, font):
if self.columns is not None:
return self.columns
return super(Profile, self).get_columns(font)
if not isinstance(inherits, (tuple, list)):
inherits = [inherits]
merged = {}
for base in reversed(inherits):
base_data = resolve_profile_data(base)
merged.update(base_data)
merged.update(data)
return merged

View File

@ -1,207 +0,0 @@
# Description of the format
abstract:
# Defines non-standard code pages that the printer supports, but
# that we won't find in Python's encoding system. If you define one
# here, don't forget to add it to codePageMap to assign it to a slot.
customCodePages:
sample:
# This maps the indexed code page slots to code page names.
# Often, the slot assignment is the same, but the device only
# supports a subset.
codePageMap:
0: "CP437"
1: "CP932"
3: "sample"
# Maybe not all of the codepages in the map are supported. This
# is for subprofiles to select which ones the device knows.
codePages: [sample, cp932]
# Many recent Epson-branded thermal receipt printers.
default:
columns: 42
barcodeB: true
bitImage: true
graphics: true
starCommands: false
qrCode: true
customCodePages:
TCVN-3-1: [
" ",
" ",
" ăâêôơưđ ",
" àảãáạ ằẳẵắ ",
" ặầẩẫấậè ẻẽ",
"éẹềểễếệìỉ ĩíịò",
" ỏõóọồổỗốộờởỡớợù",
" ủũúụừửữứựỳỷỹýỵ ",
]
TCVN-3-2: [
" ",
" ",
" ĂÂ Ð ÊÔƠƯ ",
" ÀẢÃÁẠ ẰẲẴẮ ",
" ẶẦẨẪẤẬÈ ẺẼ",
"ÉẸỀỂỄẾỆÌỈ ĨÍỊÒ",
" ỎÕÓỌỒỔỖỐỘỜỞỠỚỢÙ",
" ỦŨÚỤỪỬỮỨỰỲỶỸÝỴ "
]
# Commented-out slots are TODO (might just need uncomment, might
# need verification/research)
codePageMap:
0: "CP437"
1: "CP932"
2: "CP850"
3: "CP860"
4: "CP863"
5: "CP865"
#6: // Hiragana
#7: // One-pass printing Kanji characters
#8: // Page 8 [One-pass printing Kanji characters]
11: "CP851"
12: "CP853"
13: "CP857"
14: "CP737"
15: "ISO8859_7"
16: "CP1252"
17: "CP866"
18: "CP852"
19: "CP858"
#20: // Thai Character Code 42
#21: // Thai Character Code 1"
#22: // Thai Character Code 13
#23: // Thai Character Code 14
#24: // Thai Character Code 16
#25: // Thai Character Code 17
#26: // Thai Character Code 18
30: 'TCVN-3-1' # TCVN-3: Vietnamese
31: 'TCVN-3-2' # TCVN-3: Vietnamese
32: "CP720"
33: "CP775"
34: "CP855"
35: "CP861"
36: "CP862"
37: "CP864"
38: "CP869"
39: "ISO8859_2"
40: "ISO8859_15"
41: "CP1098"
42: "CP774"
43: "CP772"
44: "CP1125"
45: "CP1250"
46: "CP1251"
47: "CP1253"
48: "CP1254"
49: "CP1255"
50: "CP1256"
51: "CP1257"
52: "CP1258"
53: "RK1048"
#66: // Devanagari
#67: // Bengali
#68: // Tamil
#69: // Telugu
#70: // Assamese
#71: // Oriya
#72: // Kannada
#73: // Malayalam
#74: // Gujarati
#75: // Punjabi
#82: // Marathi
#254:
#255:
# Designed for non-Epson printers sold online. Without knowing
# their character encoding table, only CP437 output is assumed,
# and graphics() calls will be disabled, as it usually prints junk
# on these models.
simple:
codePages:
- cp437
graphics: false
# Profile for Star-branded printers.
star:
inherits: default
starCommands: true
epson:
inherits: default
manufacturer: "Epson"
"P-822D":
inherits: default
graphics: false
# http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf
"TM-T88II":
inherits: epson
columns:
a: 42
b: 56
codePages:
- cp437 # 0
- Katakana # 1
- cp850 # 2
- cp860 # 3
- cp863 # 4
- cp865 # 5
- cp858 # 19
- blank
# http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf
"TM-T88III":
inherits: epson
columns:
a: 42
b: 56
codePages:
- CP437 # 0
- Katakana # 1
- CP850 # 2
- CP860 # 3
- CP863 # 4
- CP865 # 5
- PC1252 # 16
- CP866 # 17
- CP852 # 18
- CP858 # 19
- blank
"TM-P80":
inherits: epson
defaultColumnConfig: default
columnConfigs:
default: {'a': 48, 'b': 64, 'kanji': 24}
'42_emulation': {'a': 42, 'b': 60, 'kanji': 21}
"TM-P60II 2":
inherits: epson
columnConfigs:
'58mm_paper': {'a': 35, 'b': 42, 'c': 52}
'60mm_paper': {'a': 36, 'b': 43, 'c': 54}
"TM-P20 2":
inherits: epson
# Has 5 fonts!
"TM-T90":
inherits: epson
colors:
- black
- red

View File

@ -55,11 +55,18 @@ _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
# 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.
@ -93,14 +100,29 @@ 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
# 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
# Text colors
TXT_COLOR_BLACK = ESC + b'\x72\x00' # Default Color
TXT_COLOR_RED = ESC + b'\x72\x01' # Alternative Color (Usually Red)
# 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
}
CODEPAGE_CHANGE = ESC + b'\x74'
# Char code table

View File

@ -24,7 +24,7 @@ from .magicencode import MagicEncode
from abc import ABCMeta, abstractmethod # abstract base class support
from escpos.image import EscposImage
from escpos.capabilities import get_profile
from escpos.capabilities import get_profile, BARCODE_B
@six.add_metaclass(ABCMeta)
@ -232,7 +232,8 @@ class Escpos(object):
else:
self.magic.force_encoding(code)
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True, function_type="A"):
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A",
align_ct=True, function_type=None):
""" Print Barcode
This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to
@ -303,14 +304,40 @@ class Escpos(object):
issued.
:type align_ct: bool
:param function_type: Choose between ESCPOS function type A or B, depending on printer support and desired
barcode.
: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
: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 type '{bc}' not valid for barcode function type "
"{function_type}").format(
bc=bc,
function_type=function_type,
))
# Align Bar Code()
if align_ct:
self._raw(TXT_ALIGN_CT)
@ -339,14 +366,6 @@ class Escpos(object):
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":
@ -385,8 +404,8 @@ class Escpos(object):
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', text_type='normal', width=1, height=1, density=9, invert=False, smooth=False,
flip=False):
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:
@ -396,7 +415,9 @@ class Escpos(object):
* RIGHT
*default*: LEFT
:param font: font type, possible values are A or B, *default*: A
:param font: font given as an index, a name, or one of the
special values 'a' or 'b', refering to fonts 0 and 1.
:param text_type: text type, possible values are:
* B for bold
@ -461,10 +482,8 @@ class Escpos(object):
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)
self._raw(SET_FONT(six.int2byte(self.profile.get_font(font))))
# Align
if align.upper() == "CENTER":
self._raw(TXT_ALIGN_CT)
@ -499,6 +518,35 @@ class Escpos(object):
else:
self._raw(TXT_INVERT_OFF)
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=''):
""" Cut paper.

View File

@ -0,0 +1,34 @@
#!/usr/bin/python
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import escpos.printer as printer
from escpos.constants import BARCODE_TYPE_A, BARCODE_TYPE_B
from escpos.capabilities import Profile, BARCODE_B
from escpos.exceptions import BarcodeTypeError
import pytest
@pytest.mark.parametrize("bctype,data,expected", [
('EAN13', '4006381333931',
b'\x1ba\x01\x1dh@\x1dw\x03\x1df\x00\x1dH\x02\x1dk\x024006381333931\x00')
])
def test_barcode(bctype, data, expected):
instance = printer.Dummy()
instance.barcode(data, bctype)
assert instance.output == expected
@pytest.mark.parametrize("bctype,supports_b", [
('invalid', True),
('CODE128', False),
])
def test_lacks_support(bctype, supports_b):
profile = Profile(features={BARCODE_B: supports_b})
instance = printer.Dummy(profile=profile)
with pytest.raises(BarcodeTypeError):
instance.barcode('test', bctype)
assert instance.output == b''

View File

@ -16,13 +16,12 @@ import mock
from hypothesis import given
import hypothesis.strategies as st
from escpos.printer import Dummy
import escpos.printer as printer
@given(text=st.text())
def test_function_text_dies_ist_ein_test_lf(text):
"""test the text printing function with simple string and compare output"""
instance = printer.Dummy()
instance = Dummy()
instance.magic.encode_text = mock.Mock()
instance.text(text)
instance.magic.encode_text.assert_called_with(txt=text)
@ -33,4 +32,4 @@ def test_block_text():
printer.block_text(
"All the presidents men were eating falafel for breakfast.", font='a')
assert printer.output == \
'All the presidents men were eating falafel\nfor breakfast.'
b'All the presidents men were eating falafel\nfor breakfast.'

26
test/test_functions.py Normal file
View File

@ -0,0 +1,26 @@
from nose.tools import assert_raises
from escpos.printer import Dummy
def test_line_spacing_code_gen():
printer = Dummy()
printer.line_spacing(10)
assert printer.output == b'\x1b3\n'
def test_line_spacing_rest():
printer = Dummy()
printer.line_spacing()
assert printer.output == b'\x1b2'
def test_line_spacing_error_handling():
printer = Dummy()
with assert_raises(ValueError):
printer.line_spacing(99, divisor=44)
with assert_raises(ValueError):
printer.line_spacing(divisor=80, spacing=86)
with assert_raises(ValueError):
printer.line_spacing(divisor=360, spacing=256)
with assert_raises(ValueError):
printer.line_spacing(divisor=180, spacing=256)

34
test/test_profile.py Normal file
View File

@ -0,0 +1,34 @@
import pytest
from escpos.capabilities import get_profile, NotSupported, BARCODE_B, Profile
@pytest.fixture
def profile():
return get_profile('default')
class TestBaseProfile:
def test_get_font(self, profile):
with pytest.raises(NotSupported):
assert profile.get_font('3')
assert profile.get_font(1) == 1
assert profile.get_font('a') == 0
def test_supports(self, profile):
assert not profile.supports('asdf asdf')
assert profile.supports(BARCODE_B)
def test_get_columns(self, profile):
assert profile.get_columns('a') > 5
with pytest.raises(NotSupported):
assert profile.get_columns('asdfasdf')
class TestCustomProfile:
def test_columns(self):
assert Profile(columns=10).get_columns('sdfasdf') == 10
def test_features(self):
assert Profile(features={'foo': True}).supports('foo')

View File

@ -6,8 +6,10 @@ deps = nose
coverage
scripttest
mock
pytest
pytest-cov
hypothesis
commands = nosetests --with-coverage --cover-erase --cover-branches
commands = py.test --cov reports
[testenv:docs]
basepython = python