mirror of
https://github.com/python-escpos/python-escpos
synced 2025-08-24 09:03:34 +00:00
@@ -7,8 +7,9 @@ import yaml
|
||||
# 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 NotSupported(Exception):
|
||||
@@ -54,6 +55,12 @@ class BaseProfile(object):
|
||||
"""
|
||||
return self.features.get(feature)
|
||||
|
||||
def get_code_pages(self):
|
||||
"""Return the support code pages as a {name: index} dict.
|
||||
"""
|
||||
return {v: k for k, v in self.codePages.items()}
|
||||
|
||||
|
||||
|
||||
def get_profile(name=None, **kwargs):
|
||||
"""Get the profile by name; if no name is given, return the
|
||||
|
22
src/escpos/codepages.py
Normal file
22
src/escpos/codepages.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from .capabilities import CAPABILITIES
|
||||
|
||||
|
||||
class CodePageManager:
|
||||
"""Holds information about all the code pages (as defined
|
||||
in escpos-printer-db).
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def get_all(self):
|
||||
return self.data.values()
|
||||
|
||||
def get_encoding_name(self, encoding):
|
||||
# TODO resolve the encoding alias
|
||||
return encoding.upper()
|
||||
|
||||
def get_encoding(self, encoding):
|
||||
return self.data[encoding]
|
||||
|
||||
CodePages = CodePageManager(CAPABILITIES['encodings'])
|
@@ -124,27 +124,9 @@ LINESPACING_FUNCS = {
|
||||
180: ESC + b'3', # line_spacing/180 of an inch, 0 <= line_spacing <= 255
|
||||
}
|
||||
|
||||
# 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
|
||||
# Prefix to change the codepage. You need to attach a byte to indicate
|
||||
# the codepage to use. We use escpos-printer-db as the data source.
|
||||
CODEPAGE_CHANGE = ESC + b'\x74'
|
||||
|
||||
# Barcode format
|
||||
_SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n
|
||||
|
@@ -20,6 +20,7 @@ import textwrap
|
||||
|
||||
from .constants import *
|
||||
from .exceptions import *
|
||||
from .magicencode import MagicEncode
|
||||
|
||||
from abc import ABCMeta, abstractmethod # abstract base class support
|
||||
from escpos.image import EscposImage
|
||||
@@ -34,13 +35,13 @@ class Escpos(object):
|
||||
class.
|
||||
"""
|
||||
device = None
|
||||
codepage = None
|
||||
|
||||
def __init__(self, profile=None):
|
||||
def __init__(self, profile=None, magic_encode_args=None, **kwargs):
|
||||
""" Initialize ESCPOS Printer
|
||||
|
||||
:param profile: Printer profile"""
|
||||
self.profile = get_profile(profile)
|
||||
self.magic = MagicEncode(self, **(magic_encode_args or {}))
|
||||
|
||||
def __del__(self):
|
||||
""" call self.close upon deletion """
|
||||
@@ -216,82 +217,20 @@ class Escpos(object):
|
||||
inp_number //= 256
|
||||
return outp
|
||||
|
||||
def charcode(self, code):
|
||||
def charcode(self, code="AUTO"):
|
||||
""" Set Character Code Table
|
||||
|
||||
Sends the control sequence from :py:mod:`escpos.constants` to the printer
|
||||
with :py:meth:`escpos.printer.'implementation'._raw()`.
|
||||
Sets the control sequence from ``CHARCODE`` in :py:mod:`escpos.constants` as active. It will be sent with
|
||||
the next text sequence. If you set the variable code to ``AUTO`` it will try to automatically guess the
|
||||
right codepage. (This is the standard behaviour.)
|
||||
|
||||
:param code: Name of CharCode
|
||||
:raises: :py:exc:`~escpos.exceptions.CharCodeError`
|
||||
"""
|
||||
# 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'
|
||||
if code.upper() == "AUTO":
|
||||
self.magic.force_encoding(False)
|
||||
else:
|
||||
raise CharCodeError()
|
||||
self.magic.force_encoding(code)
|
||||
|
||||
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A",
|
||||
align_ct=True, function_type=None):
|
||||
@@ -450,14 +389,8 @@ class Escpos(object):
|
||||
: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()
|
||||
txt = six.text_type(txt)
|
||||
self.magic.write(txt)
|
||||
|
||||
def block_text(self, txt, font=None, columns=None):
|
||||
""" Text is printed wrapped to specified columns
|
||||
|
@@ -87,7 +87,7 @@ class BarcodeCodeError(Error):
|
||||
self.resultcode = 30
|
||||
|
||||
def __str__(self):
|
||||
return "No Barcode code was supplied"
|
||||
return "No Barcode code was supplied ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class ImageSizeError(Error):
|
||||
@@ -101,7 +101,7 @@ class ImageSizeError(Error):
|
||||
self.resultcode = 40
|
||||
|
||||
def __str__(self):
|
||||
return "Image height is longer than 255px and can't be printed"
|
||||
return "Image height is longer than 255px and can't be printed ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class TextError(Error):
|
||||
@@ -116,7 +116,7 @@ class TextError(Error):
|
||||
self.resultcode = 50
|
||||
|
||||
def __str__(self):
|
||||
return "Text string must be supplied to the text() method"
|
||||
return "Text string must be supplied to the text() method ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class CashDrawerError(Error):
|
||||
@@ -131,7 +131,7 @@ class CashDrawerError(Error):
|
||||
self.resultcode = 60
|
||||
|
||||
def __str__(self):
|
||||
return "Valid pin must be set to send pulse"
|
||||
return "Valid pin must be set to send pulse ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class TabPosError(Error):
|
||||
@@ -146,7 +146,7 @@ class TabPosError(Error):
|
||||
self.resultcode = 70
|
||||
|
||||
def __str__(self):
|
||||
return "Valid tab positions must be in the range 0 to 16"
|
||||
return "Valid tab positions must be in the range 0 to 16 ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class CharCodeError(Error):
|
||||
@@ -161,7 +161,7 @@ class CharCodeError(Error):
|
||||
self.resultcode = 80
|
||||
|
||||
def __str__(self):
|
||||
return "Valid char code must be set"
|
||||
return "Valid char code must be set ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class USBNotFoundError(Error):
|
||||
@@ -176,7 +176,7 @@ class USBNotFoundError(Error):
|
||||
self.resultcode = 90
|
||||
|
||||
def __str__(self):
|
||||
return "USB device not found"
|
||||
return "USB device not found ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class SetVariableError(Error):
|
||||
@@ -191,7 +191,7 @@ class SetVariableError(Error):
|
||||
self.resultcode = 100
|
||||
|
||||
def __str__(self):
|
||||
return "Set variable out of range"
|
||||
return "Set variable out of range ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
# Configuration errors
|
||||
|
108
src/escpos/katakana.py
Normal file
108
src/escpos/katakana.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Helpers to encode Japanese characters.
|
||||
|
||||
I doubt that this currently works correctly.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
try:
|
||||
import jaconv
|
||||
except ImportError:
|
||||
jaconv = None
|
||||
|
||||
|
||||
def encode_katakana(text):
|
||||
"""I don't think this quite works yet."""
|
||||
encoded = []
|
||||
for char in text:
|
||||
if jaconv:
|
||||
# try to convert japanese text to half-katakanas
|
||||
char = jaconv.z2h(jaconv.hira2kata(char))
|
||||
# TODO: "the conversion may result in multiple characters"
|
||||
# If that really can happen (I am not really shure), than the string would have to be split and every single
|
||||
# character has to passed through the following lines.
|
||||
|
||||
if char in TXT_ENC_KATAKANA_MAP:
|
||||
encoded.append(TXT_ENC_KATAKANA_MAP[char])
|
||||
else:
|
||||
#TODO doesn't this discard all that is not in the map? Can we be shure that the input does contain only
|
||||
# encodable characters? We could at least throw an exception if encoding is not possible.
|
||||
pass
|
||||
return b"".join(encoded)
|
||||
|
||||
|
||||
|
||||
TXT_ENC_KATAKANA_MAP = {
|
||||
# Maps UTF-8 Katakana symbols to KATAKANA Page Codes
|
||||
# TODO: has this really to be hardcoded?
|
||||
|
||||
# Half-Width Katakanas
|
||||
'。': b'\xa1',
|
||||
'「': b'\xa2',
|
||||
'」': b'\xa3',
|
||||
'、': b'\xa4',
|
||||
'・': b'\xa5',
|
||||
'ヲ': b'\xa6',
|
||||
'ァ': b'\xa7',
|
||||
'ィ': b'\xa8',
|
||||
'ゥ': b'\xa9',
|
||||
'ェ': b'\xaa',
|
||||
'ォ': b'\xab',
|
||||
'ャ': b'\xac',
|
||||
'ュ': b'\xad',
|
||||
'ョ': b'\xae',
|
||||
'ッ': b'\xaf',
|
||||
'ー': b'\xb0',
|
||||
'ア': b'\xb1',
|
||||
'イ': b'\xb2',
|
||||
'ウ': b'\xb3',
|
||||
'エ': b'\xb4',
|
||||
'オ': b'\xb5',
|
||||
'カ': b'\xb6',
|
||||
'キ': b'\xb7',
|
||||
'ク': b'\xb8',
|
||||
'ケ': b'\xb9',
|
||||
'コ': b'\xba',
|
||||
'サ': b'\xbb',
|
||||
'シ': b'\xbc',
|
||||
'ス': b'\xbd',
|
||||
'セ': b'\xbe',
|
||||
'ソ': b'\xbf',
|
||||
'タ': b'\xc0',
|
||||
'チ': b'\xc1',
|
||||
'ツ': b'\xc2',
|
||||
'テ': b'\xc3',
|
||||
'ト': b'\xc4',
|
||||
'ナ': b'\xc5',
|
||||
'ニ': b'\xc6',
|
||||
'ヌ': b'\xc7',
|
||||
'ネ': b'\xc8',
|
||||
'ノ': b'\xc9',
|
||||
'ハ': b'\xca',
|
||||
'ヒ': b'\xcb',
|
||||
'フ': b'\xcc',
|
||||
'ヘ': b'\xcd',
|
||||
'ホ': b'\xce',
|
||||
'マ': b'\xcf',
|
||||
'ミ': b'\xd0',
|
||||
'ム': b'\xd1',
|
||||
'メ': b'\xd2',
|
||||
'モ': b'\xd3',
|
||||
'ヤ': b'\xd4',
|
||||
'ユ': b'\xd5',
|
||||
'ヨ': b'\xd6',
|
||||
'ラ': b'\xd7',
|
||||
'リ': b'\xd8',
|
||||
'ル': b'\xd9',
|
||||
'レ': b'\xda',
|
||||
'ロ': b'\xdb',
|
||||
'ワ': b'\xdc',
|
||||
'ン': b'\xdd',
|
||||
'゙': b'\xde',
|
||||
'゚': b'\xdf',
|
||||
}
|
290
src/escpos/magicencode.py
Normal file
290
src/escpos/magicencode.py
Normal file
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Magic Encode
|
||||
|
||||
This module tries to convert an UTF-8 string to an encoded string for the printer.
|
||||
It uses trial and error in order to guess the right codepage.
|
||||
The code is based on the encoding-code in py-xml-escpos by @fvdsn.
|
||||
|
||||
:author: `Patrick Kanzler <dev@pkanzler.de>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 Patrick Kanzler and Frédéric van der Essen
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import bytes
|
||||
from .constants import CODEPAGE_CHANGE
|
||||
from .exceptions import CharCodeError, Error
|
||||
from .capabilities import get_profile
|
||||
from .codepages import CodePages
|
||||
import copy
|
||||
import six
|
||||
|
||||
|
||||
class Encoder(object):
|
||||
"""Takes a list of available code spaces. Picks the right one for a
|
||||
given character.
|
||||
|
||||
Note: To determine the code page, it needs to do the conversion, and
|
||||
thus already knows what the final byte in the target encoding would
|
||||
be. Nevertheless, the API of this class doesn't return the byte.
|
||||
|
||||
The caller use to do the character conversion itself.
|
||||
|
||||
$ python -m timeit -s "{u'ö':'a'}.get(u'ö')"
|
||||
100000000 loops, best of 3: 0.0133 usec per loop
|
||||
|
||||
$ python -m timeit -s "u'ö'.encode('latin1')"
|
||||
100000000 loops, best of 3: 0.0141 usec per loop
|
||||
"""
|
||||
|
||||
def __init__(self, codepage_map):
|
||||
self.codepages = codepage_map
|
||||
self.available_encodings = set(codepage_map.keys())
|
||||
self.available_characters = {}
|
||||
self.used_encodings = set()
|
||||
|
||||
def get_sequence(self, encoding):
|
||||
return int(self.codepages[encoding])
|
||||
|
||||
def get_encoding_name(self, encoding):
|
||||
"""Given an encoding provided by the user, will return a
|
||||
canonical encoding name; and also validate that the encoding
|
||||
is supported.
|
||||
|
||||
TODO: Support encoding aliases: pc437 instead of cp437.
|
||||
"""
|
||||
encoding = CodePages.get_encoding_name(encoding)
|
||||
if not encoding in self.codepages:
|
||||
raise ValueError((
|
||||
'Encoding "{}" cannot be used for the current profile. '
|
||||
'Valid encodings are: {}'
|
||||
).format(encoding, ','.join(self.codepages.keys())))
|
||||
return encoding
|
||||
|
||||
def _get_codepage_char_list(self, encoding):
|
||||
"""Get codepage character list
|
||||
|
||||
Gets characters 128-255 for a given code page, as an array.
|
||||
|
||||
:param encoding: The name of the encoding. This must appear in the CodePage list
|
||||
"""
|
||||
codepage = CodePages.get_encoding(encoding)
|
||||
if 'data' in codepage:
|
||||
encodable_chars = list("".join(codepage['data']))
|
||||
assert(len(encodable_chars) == 128)
|
||||
return encodable_chars
|
||||
elif 'python_encode' in codepage:
|
||||
encodable_chars = [u" "] * 128
|
||||
for i in range(0, 128):
|
||||
codepoint = i + 128
|
||||
try:
|
||||
encodable_chars[i] = bytes([codepoint]).decode(codepage['python_encode'])
|
||||
except UnicodeDecodeError:
|
||||
# Non-encodable character, just skip it
|
||||
pass
|
||||
return encodable_chars
|
||||
raise LookupError("Can't find a known encoding for {}".format(encoding))
|
||||
|
||||
def _get_codepage_char_map(self, encoding):
|
||||
""" Get codepage character map
|
||||
|
||||
Process an encoding and return a map of UTF-characters to code points
|
||||
in this encoding.
|
||||
|
||||
This is generated once only, and returned from a cache.
|
||||
|
||||
:param encoding: The name of the encoding.
|
||||
"""
|
||||
# Skip things that were loaded previously
|
||||
if encoding in self.available_characters:
|
||||
return self.available_characters[encoding]
|
||||
codepage_char_list = self._get_codepage_char_list(encoding)
|
||||
codepage_char_map = dict((utf8, i + 128) for (i, utf8) in enumerate(codepage_char_list))
|
||||
self.available_characters[encoding] = codepage_char_map
|
||||
return codepage_char_map
|
||||
|
||||
def can_encode(self, encoding, char):
|
||||
"""Determine if a character is encodeable in the given code page.
|
||||
|
||||
:param encoding: The name of the encoding.
|
||||
:param char: The character to attempt to encode.
|
||||
"""
|
||||
available_map = {}
|
||||
try:
|
||||
available_map = self._get_codepage_char_map(encoding)
|
||||
except LookupError:
|
||||
return False
|
||||
|
||||
# Decide whether this character is encodeable in this code page
|
||||
is_ascii = ord(char) < 128
|
||||
is_encodable = char in available_map
|
||||
return is_ascii or is_encodable
|
||||
|
||||
def _encode_char(self, char, charmap, defaultchar):
|
||||
""" Encode a single character with the given encoding map
|
||||
|
||||
:param char: char to encode
|
||||
:param charmap: dictionary for mapping characters in this code page
|
||||
"""
|
||||
if ord(char) < 128:
|
||||
return ord(char)
|
||||
if char in charmap:
|
||||
return charmap[char]
|
||||
return ord(defaultchar)
|
||||
|
||||
def encode(self, text, encoding, defaultchar='?'):
|
||||
""" Encode text under the given encoding
|
||||
|
||||
:param text: Text to encode
|
||||
:param encoding: Encoding name to use (must be defined in capabilities)
|
||||
:param defaultchar: Fallback for non-encodable characters
|
||||
"""
|
||||
codepage_char_map = self._get_codepage_char_map(encoding)
|
||||
output_bytes = bytes([self._encode_char(char, codepage_char_map, defaultchar) for char in text])
|
||||
return output_bytes
|
||||
|
||||
def __encoding_sort_func(self, item):
|
||||
key, index = item
|
||||
return (
|
||||
key in self.used_encodings,
|
||||
index
|
||||
)
|
||||
|
||||
def find_suitable_encoding(self, char):
|
||||
"""The order of our search is a specific one:
|
||||
|
||||
1. code pages that we already tried before; there is a good
|
||||
chance they might work again, reducing the search space,
|
||||
and by re-using already used encodings we might also
|
||||
reduce the number of codepage change instructiosn we have
|
||||
to send. Still, any performance gains will presumably be
|
||||
fairly minor.
|
||||
|
||||
2. code pages in lower ESCPOS slots first. Presumably, they
|
||||
are more likely to be supported, so if a printer profile
|
||||
is missing or incomplete, we might increase our change
|
||||
that the code page we pick for this character is actually
|
||||
supported.
|
||||
"""
|
||||
sorted_encodings = sorted(
|
||||
self.codepages.items(),
|
||||
key=self.__encoding_sort_func)
|
||||
|
||||
for encoding, _ in sorted_encodings:
|
||||
if self.can_encode(encoding, char):
|
||||
# This encoding worked; at it to the set of used ones.
|
||||
self.used_encodings.add(encoding)
|
||||
return encoding
|
||||
|
||||
|
||||
def split_writable_text(encoder, text, encoding):
|
||||
"""Splits off as many characters from the begnning of text as
|
||||
are writable with "encoding". Returns a 2-tuple (writable, rest).
|
||||
"""
|
||||
if not encoding:
|
||||
return None, text
|
||||
|
||||
for idx, char in enumerate(text):
|
||||
if encoder.can_encode(encoding, char):
|
||||
continue
|
||||
return text[:idx], text[idx:]
|
||||
|
||||
return text, None
|
||||
|
||||
|
||||
class MagicEncode(object):
|
||||
"""A helper that helps us to automatically switch to the right
|
||||
code page to encode any given Unicode character.
|
||||
|
||||
This will consider the printers supported codepages, according
|
||||
to the printer profile, and if a character cannot be encoded
|
||||
with the current profile, it will attempt to find a suitable one.
|
||||
|
||||
If the printer does not support a suitable code page, it can
|
||||
insert an error character.
|
||||
|
||||
:param encoding: If you know the current encoding of the printer
|
||||
when initializing this class, set it here. If the current
|
||||
encoding is unknown, the first character emitted will be a
|
||||
codepage switch.
|
||||
"""
|
||||
def __init__(self, driver, encoding=None, disabled=False,
|
||||
defaultsymbol='?', encoder=None):
|
||||
if disabled and not encoding:
|
||||
raise Error('If you disable magic encode, you need to define an encoding!')
|
||||
|
||||
self.driver = driver
|
||||
self.encoder = encoder or Encoder(driver.profile.get_code_pages())
|
||||
|
||||
self.encoding = self.encoder.get_encoding_name(encoding) if encoding else None
|
||||
self.defaultsymbol = defaultsymbol
|
||||
self.disabled = disabled
|
||||
|
||||
def force_encoding(self, encoding):
|
||||
"""Sets a fixed encoding. The change is emitted right away.
|
||||
|
||||
From now one, this buffer will switch the code page anymore.
|
||||
However, it will still keep track of the current code page.
|
||||
"""
|
||||
if not encoding:
|
||||
self.disabled = False
|
||||
else:
|
||||
self.write_with_encoding(encoding, None)
|
||||
self.disabled = True
|
||||
|
||||
def write(self, text):
|
||||
"""Write the text, automatically switching encodings.
|
||||
"""
|
||||
|
||||
if self.disabled:
|
||||
self.write_with_encoding(self.encoding, text)
|
||||
return
|
||||
|
||||
# See how far we can go into the text with the current encoding
|
||||
to_write, text = split_writable_text(self.encoder, text, self.encoding)
|
||||
if to_write:
|
||||
self.write_with_encoding(self.encoding, to_write)
|
||||
|
||||
while text:
|
||||
# See if any of the code pages that the printer profile
|
||||
# supports can encode this character.
|
||||
encoding = self.encoder.find_suitable_encoding(text[0])
|
||||
if not encoding:
|
||||
self._handle_character_failed(text[0])
|
||||
text = text[1:]
|
||||
continue
|
||||
|
||||
# Write as much text as possible with the encoding found.
|
||||
to_write, text = split_writable_text(self.encoder, text, encoding)
|
||||
if to_write:
|
||||
self.write_with_encoding(encoding, to_write)
|
||||
|
||||
def _handle_character_failed(self, char):
|
||||
"""Called when no codepage was found to render a character.
|
||||
"""
|
||||
# Writing the default symbol via write() allows us to avoid
|
||||
# unnecesary codepage switches.
|
||||
self.write(self.defaultsymbol)
|
||||
|
||||
def write_with_encoding(self, encoding, text):
|
||||
if text is not None and type(text) is not six.text_type:
|
||||
raise Error("The supplied text has to be unicode, but is of type {type}.".format(
|
||||
type=type(text)
|
||||
))
|
||||
|
||||
# We always know the current code page; if the new codepage
|
||||
# is different, emit a change command.
|
||||
if encoding != self.encoding:
|
||||
self.encoding = encoding
|
||||
self.driver._raw(
|
||||
CODEPAGE_CHANGE +
|
||||
six.int2byte(self.encoder.get_sequence(encoding)))
|
||||
|
||||
if text:
|
||||
self.driver._raw(self.encoder.encode(text, encoding))
|
Reference in New Issue
Block a user