1
0
mirror of https://github.com/python-escpos/python-escpos synced 2025-08-24 09:03:34 +00:00

Merge branch 'capabilities' into text-encoding

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

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
@@ -260,7 +282,7 @@ 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

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.