Merge branch 'capabilities' into text-encoding
This commit is contained in:
commit
b37f4fc8cc
|
@ -15,6 +15,21 @@ contributors
|
||||||
- Patrick Kanzler (with code by Frédéric Van der Essen)
|
- 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"
|
2016-08-10 - Version 2.1.3 - "Ethics Gradient"
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
|
12
README.rst
12
README.rst
|
@ -54,14 +54,14 @@ The basic usage is:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from escpos import *
|
from escpos.printer import Usb
|
||||||
|
|
||||||
""" Seiko Epson Corp. Receipt Printer M129 Definitions (EPSON TM-T88IV) """
|
""" Seiko Epson Corp. Receipt Printer M129 Definitions (EPSON TM-T88IV) """
|
||||||
Epson = escpos.Escpos(0x04b8,0x0202,0)
|
p = Usb(0x04b8,0x0202,0)
|
||||||
Epson.text("Hello World\n")
|
p.text("Hello World\n")
|
||||||
Epson.image("logo.gif")
|
p.image("logo.gif")
|
||||||
Epson.barcode('1324354657687','EAN13',64,2,'','')
|
p.barcode('1324354657687','EAN13',64,2,'','')
|
||||||
Epson.cut()
|
p.cut()
|
||||||
|
|
||||||
The full project-documentation is available on `Read the Docs <https://python-escpos.readthedocs.io>`_.
|
The full project-documentation is available on `Read the Docs <https://python-escpos.readthedocs.io>`_.
|
||||||
|
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
@ -83,7 +83,8 @@ setup(
|
||||||
platforms='any',
|
platforms='any',
|
||||||
package_dir={"": "src"},
|
package_dir={"": "src"},
|
||||||
packages=find_packages(where="src", exclude=["tests", "tests.*"]),
|
packages=find_packages(where="src", exclude=["tests", "tests.*"]),
|
||||||
package_data={'': ['COPYING']},
|
package_data={'': ['COPYING', 'src/escpos/capabilities.json']},
|
||||||
|
include_package_data=True,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
|
@ -116,7 +117,7 @@ setup(
|
||||||
setup_requires=[
|
setup_requires=[
|
||||||
'setuptools_scm',
|
'setuptools_scm',
|
||||||
],
|
],
|
||||||
tests_require=['tox', 'nose', 'scripttest', 'mock', 'hypothesis'],
|
tests_require=['tox', 'pytest', 'pytest-cov', 'nose', 'scripttest', 'mock', 'hypothesis'],
|
||||||
cmdclass={'test': Tox},
|
cmdclass={'test': Tox},
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,40 +1,61 @@
|
||||||
import re
|
import re
|
||||||
|
import six
|
||||||
from os import path
|
from os import path
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
with open(path.join(path.dirname(__file__), 'capabilities.yml')) as f:
|
# Load external printer database
|
||||||
PROFILES = yaml.load(f)
|
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 = {}
|
profile_data = {}
|
||||||
|
|
||||||
def __init__(self, columns=None):
|
|
||||||
self.default_columns = columns
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return self.profile_data[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):
|
def get_columns(self, font):
|
||||||
""" Return the number of columns for the given font.
|
""" Return the number of columns for the given font.
|
||||||
"""
|
"""
|
||||||
if self.default_columns:
|
font = self.get_font(font)
|
||||||
return self.default_columns
|
return self.fonts[six.text_type(font)]['columns']
|
||||||
|
|
||||||
if 'columnConfigs' in self.profile_data:
|
def supports(self, feature):
|
||||||
columns_def = self.columnConfigs[self.defaultColumnConfig]
|
"""Return true/false for the given feature.
|
||||||
|
"""
|
||||||
elif 'columns' in self.profile_data:
|
return self.features.get(feature)
|
||||||
columns_def = self.columns
|
|
||||||
|
|
||||||
if isinstance(columns_def, int):
|
|
||||||
return columns_def
|
|
||||||
return columns_def[font]
|
|
||||||
|
|
||||||
|
|
||||||
def get_profile(name=None, **kwargs):
|
def get_profile(name=None, **kwargs):
|
||||||
|
"""Get the profile by name; if no name is given, return the
|
||||||
|
default profile.
|
||||||
|
"""
|
||||||
if isinstance(name, Profile):
|
if isinstance(name, Profile):
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
@ -42,15 +63,19 @@ def get_profile(name=None, **kwargs):
|
||||||
return clazz(**kwargs)
|
return clazz(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CLASS_CACHE = {}
|
CLASS_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
def get_profile_class(name):
|
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:
|
if not name in CLASS_CACHE:
|
||||||
profile_data = resolve_profile_data(name)
|
profile_data = PROFILES[name]
|
||||||
class_name = '%sProfile' % clean(name)
|
profile_name = clean(name)
|
||||||
new_class = type(class_name, (Profile,), {'profile_data': profile_data})
|
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
|
CLASS_CACHE[name] = new_class
|
||||||
|
|
||||||
return CLASS_CACHE[name]
|
return CLASS_CACHE[name]
|
||||||
|
@ -64,20 +89,21 @@ def clean(s):
|
||||||
return str(s)
|
return str(s)
|
||||||
|
|
||||||
|
|
||||||
def resolve_profile_data(name):
|
# For users, who want to provide their profile
|
||||||
data = PROFILES[name]
|
class Profile(get_profile_class('default')):
|
||||||
inherits = data.get('inherits')
|
|
||||||
if not inherits:
|
def __init__(self, columns=None, features={}):
|
||||||
return data
|
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -55,11 +55,18 @@ _CUT_PAPER = lambda m: GS + b'V' + m
|
||||||
PAPER_FULL_CUT = _CUT_PAPER(b'\x00') # Full cut paper
|
PAPER_FULL_CUT = _CUT_PAPER(b'\x00') # Full cut paper
|
||||||
PAPER_PART_CUT = _CUT_PAPER(b'\x01') # Partial cut paper
|
PAPER_PART_CUT = _CUT_PAPER(b'\x01') # Partial cut paper
|
||||||
|
|
||||||
|
# Beep
|
||||||
|
BEEP = b'\x07'
|
||||||
|
|
||||||
# Panel buttons (e.g. the FEED button)
|
# Panel buttons (e.g. the FEED button)
|
||||||
_PANEL_BUTTON = lambda n: ESC + b'c5' + six.int2byte(n)
|
_PANEL_BUTTON = lambda n: ESC + b'c5' + six.int2byte(n)
|
||||||
PANEL_BUTTON_ON = _PANEL_BUTTON(0) # enable all panel buttons
|
PANEL_BUTTON_ON = _PANEL_BUTTON(0) # enable all panel buttons
|
||||||
PANEL_BUTTON_OFF = _PANEL_BUTTON(1) # disable 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
|
# Text format
|
||||||
# TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll
|
# TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll
|
||||||
# Printers" and tidy up this stuff too.
|
# 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_UNDERL2_ON = ESC + b'\x2d\x02' # Underline font 2-dot ON
|
||||||
TXT_BOLD_OFF = ESC + b'\x45\x00' # Bold font OFF
|
TXT_BOLD_OFF = ESC + b'\x45\x00' # Bold font OFF
|
||||||
TXT_BOLD_ON = ESC + b'\x45\x01' # Bold font ON
|
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_LT = ESC + b'\x61\x00' # Left justification
|
||||||
TXT_ALIGN_CT = ESC + b'\x61\x01' # Centering
|
TXT_ALIGN_CT = ESC + b'\x61\x01' # Centering
|
||||||
TXT_ALIGN_RT = ESC + b'\x61\x02' # Right justification
|
TXT_ALIGN_RT = ESC + b'\x61\x02' # Right justification
|
||||||
TXT_INVERT_ON = GS + b'\x42\x01' # Inverse Printing ON
|
TXT_INVERT_ON = GS + b'\x42\x01' # Inverse Printing ON
|
||||||
TXT_INVERT_OFF = GS + b'\x42\x00' # Inverse Printing OFF
|
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'
|
CODEPAGE_CHANGE = ESC + b'\x74'
|
||||||
# Char code table
|
# Char code table
|
||||||
|
@ -260,7 +282,7 @@ QR_ECLEVEL_L = 0
|
||||||
QR_ECLEVEL_M = 1
|
QR_ECLEVEL_M = 1
|
||||||
QR_ECLEVEL_Q = 2
|
QR_ECLEVEL_Q = 2
|
||||||
QR_ECLEVEL_H = 3
|
QR_ECLEVEL_H = 3
|
||||||
|
|
||||||
# QRcode models
|
# QRcode models
|
||||||
QR_MODEL_1 = 1
|
QR_MODEL_1 = 1
|
||||||
QR_MODEL_2 = 2
|
QR_MODEL_2 = 2
|
||||||
|
|
|
@ -24,7 +24,7 @@ from .magicencode import MagicEncode
|
||||||
|
|
||||||
from abc import ABCMeta, abstractmethod # abstract base class support
|
from abc import ABCMeta, abstractmethod # abstract base class support
|
||||||
from escpos.image import EscposImage
|
from escpos.image import EscposImage
|
||||||
from escpos.capabilities import get_profile
|
from escpos.capabilities import get_profile, BARCODE_B
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(ABCMeta)
|
@six.add_metaclass(ABCMeta)
|
||||||
|
@ -232,7 +232,8 @@ class Escpos(object):
|
||||||
else:
|
else:
|
||||||
self.magic.force_encoding(code)
|
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
|
""" Print Barcode
|
||||||
|
|
||||||
This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to
|
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.
|
issued.
|
||||||
:type align_ct: bool
|
:type align_ct: bool
|
||||||
|
|
||||||
:param function_type: Choose between ESCPOS function type A or B, depending on printer support and desired
|
:param function_type: Choose between ESCPOS function type A or B,
|
||||||
barcode.
|
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
|
*default*: A
|
||||||
|
|
||||||
:raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`,
|
:raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`,
|
||||||
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
|
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
|
||||||
:py:exc:`~escpos.exceptions.BarcodeCodeError`
|
: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()
|
# Align Bar Code()
|
||||||
if align_ct:
|
if align_ct:
|
||||||
self._raw(TXT_ALIGN_CT)
|
self._raw(TXT_ALIGN_CT)
|
||||||
|
@ -339,14 +366,6 @@ class Escpos(object):
|
||||||
else: # DEFAULT POSITION: BELOW
|
else: # DEFAULT POSITION: BELOW
|
||||||
self._raw(BARCODE_TXT_BLW)
|
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()])
|
self._raw(bc_types[bc.upper()])
|
||||||
|
|
||||||
if function_type.upper() == "B":
|
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
|
col_count = self.profile.get_columns(font) if columns is None else columns
|
||||||
self.text(textwrap.fill(txt, col_count))
|
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,
|
def set(self, align='left', font='a', text_type='normal', width=1,
|
||||||
flip=False):
|
height=1, density=9, invert=False, smooth=False, flip=False):
|
||||||
""" Set text properties by sending them to the printer
|
""" Set text properties by sending them to the printer
|
||||||
|
|
||||||
:param align: horizontal position for text, possible values are:
|
:param align: horizontal position for text, possible values are:
|
||||||
|
@ -396,7 +415,9 @@ class Escpos(object):
|
||||||
* RIGHT
|
* RIGHT
|
||||||
|
|
||||||
*default*: LEFT
|
*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:
|
:param text_type: text type, possible values are:
|
||||||
|
|
||||||
* B for bold
|
* B for bold
|
||||||
|
@ -461,10 +482,8 @@ class Escpos(object):
|
||||||
self._raw(TXT_BOLD_OFF)
|
self._raw(TXT_BOLD_OFF)
|
||||||
self._raw(TXT_UNDERL_OFF)
|
self._raw(TXT_UNDERL_OFF)
|
||||||
# Font
|
# Font
|
||||||
if font.upper() == "B":
|
self._raw(SET_FONT(six.int2byte(self.profile.get_font(font))))
|
||||||
self._raw(TXT_FONT_B)
|
|
||||||
else: # DEFAULT FONT: A
|
|
||||||
self._raw(TXT_FONT_A)
|
|
||||||
# Align
|
# Align
|
||||||
if align.upper() == "CENTER":
|
if align.upper() == "CENTER":
|
||||||
self._raw(TXT_ALIGN_CT)
|
self._raw(TXT_ALIGN_CT)
|
||||||
|
@ -499,6 +518,35 @@ class Escpos(object):
|
||||||
else:
|
else:
|
||||||
self._raw(TXT_INVERT_OFF)
|
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=''):
|
def cut(self, mode=''):
|
||||||
""" Cut paper.
|
""" Cut paper.
|
||||||
|
|
||||||
|
|
|
@ -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''
|
|
@ -16,13 +16,12 @@ import mock
|
||||||
from hypothesis import given
|
from hypothesis import given
|
||||||
import hypothesis.strategies as st
|
import hypothesis.strategies as st
|
||||||
from escpos.printer import Dummy
|
from escpos.printer import Dummy
|
||||||
import escpos.printer as printer
|
|
||||||
|
|
||||||
|
|
||||||
@given(text=st.text())
|
@given(text=st.text())
|
||||||
def test_function_text_dies_ist_ein_test_lf(text):
|
def test_function_text_dies_ist_ein_test_lf(text):
|
||||||
"""test the text printing function with simple string and compare output"""
|
"""test the text printing function with simple string and compare output"""
|
||||||
instance = printer.Dummy()
|
instance = Dummy()
|
||||||
instance.magic.encode_text = mock.Mock()
|
instance.magic.encode_text = mock.Mock()
|
||||||
instance.text(text)
|
instance.text(text)
|
||||||
instance.magic.encode_text.assert_called_with(txt=text)
|
instance.magic.encode_text.assert_called_with(txt=text)
|
||||||
|
@ -33,4 +32,4 @@ def test_block_text():
|
||||||
printer.block_text(
|
printer.block_text(
|
||||||
"All the presidents men were eating falafel for breakfast.", font='a')
|
"All the presidents men were eating falafel for breakfast.", font='a')
|
||||||
assert printer.output == \
|
assert printer.output == \
|
||||||
'All the presidents men were eating falafel\nfor breakfast.'
|
b'All the presidents men were eating falafel\nfor breakfast.'
|
||||||
|
|
|
@ -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)
|
|
@ -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')
|
4
tox.ini
4
tox.ini
|
@ -6,8 +6,10 @@ deps = nose
|
||||||
coverage
|
coverage
|
||||||
scripttest
|
scripttest
|
||||||
mock
|
mock
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
hypothesis
|
hypothesis
|
||||||
commands = nosetests --with-coverage --cover-erase --cover-branches
|
commands = py.test --cov reports
|
||||||
|
|
||||||
[testenv:docs]
|
[testenv:docs]
|
||||||
basepython = python
|
basepython = python
|
||||||
|
|
Loading…
Reference in New Issue