From 3546e0c4bb5169aff242d73fe964d5dded6fd6f5 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sat, 23 Jul 2016 22:09:35 +0200 Subject: [PATCH 01/15] improve the exceptions also adds a stump for the tests for MagicEncode --- src/escpos/exceptions.py | 16 ++++++------ test/test_magicencode.py | 54 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 test/test_magicencode.py diff --git a/src/escpos/exceptions.py b/src/escpos/exceptions.py index d0e2bd6..0f9f058 100644 --- a/src/escpos/exceptions.py +++ b/src/escpos/exceptions.py @@ -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 diff --git a/test/test_magicencode.py b/test/test_magicencode.py new file mode 100644 index 0000000..403bc75 --- /dev/null +++ b/test/test_magicencode.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +"""tests for panel button function + +:author: `Patrick Kanzler `_ +:organization: `python-escpos `_ +:copyright: Copyright (c) 2016 `python-escpos `_ +:license: GNU GPL v3 +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from nose.tools import with_setup + +import escpos.printer as printer +import os + +devfile = 'testfile' + + +def setup_testfile(): + """create a testfile as devfile""" + fhandle = open(devfile, 'a') + try: + os.utime(devfile, None) + finally: + fhandle.close() + + +def teardown_testfile(): + """destroy testfile again""" + os.remove(devfile) + + +@with_setup(setup_testfile, teardown_testfile) +def test_function_panel_button_on(): + """test the panel button function (enabling) by comparing output""" + instance = printer.File(devfile=devfile) + instance.panel_buttons() + instance.flush() + with open(devfile, "rb") as f: + assert(f.read() == b'\x1B\x63\x35\x00') + + +@with_setup(setup_testfile, teardown_testfile) +def test_function_panel_button_off(): + """test the panel button function (disabling) by comparing output""" + instance = printer.File(devfile=devfile) + instance.panel_buttons(False) + instance.flush() + with open(devfile, "rb") as f: + assert(f.read() == b'\x1B\x63\x35\x01') From b0af9e9652fbe82493f04368ae13aaa3afb20650 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sat, 23 Jul 2016 22:10:44 +0200 Subject: [PATCH 02/15] improve restructure charcode-table restructured the charcode table in order to be more accessible to programmatic usage --- src/escpos/constants.py | 112 +++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 20 deletions(-) diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 74b26eb..32c1a76 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -102,26 +102,98 @@ 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 +CHARCODE = { + 'PC437': + [ESC + b'\x74\x00', 'cp437'], # PC437 USA + 'KATAKANA': + [ESC + b'\x74\x01', 'katakana'], # KATAKANA (JAPAN) + 'PC850': + [ESC + b'\x74\x02', 'cp850'], # PC850 Multilingual + 'PC860': + [ESC + b'\x74\x03', 'cp860'], # PC860 Portuguese + 'PC863': + [ESC + b'\x74\x04', 'cp863'], # PC863 Canadian-French + 'PC865': + [ESC + b'\x74\x05', 'cp865'], # PC865 Nordic + 'KANJI6': + [ESC + b'\x74\x06', ''], # One-pass Kanji, Hiragana + 'KANJI7': + [ESC + b'\x74\x07', ''], # One-pass Kanji + 'KANJI8': + [ESC + b'\x74\x08', ''], # One-pass Kanji + 'PC851': + [ESC + b'\x74\x0b', 'cp851'], # PC851 Greek + 'PC853': + [ESC + b'\x74\x0c', 'cp853'], # PC853 Turkish + 'PC857': + [ESC + b'\x74\x0d', 'cp857'], # PC857 Turkish + 'PC737': + [ESC + b'\x74\x0e', 'cp737'], # PC737 Greek + '8859_7': + [ESC + b'\x74\x0f', 'iso8859_7'], # ISO8859-7 Greek + 'WPC1252': + [ESC + b'\x74\x10', 'cp1252'], # WPC1252 + 'PC866': + [ESC + b'\x74\x11', 'cp866'], # PC866 Cyrillic #2 + 'PC852': + [ESC + b'\x74\x12', 'cp852'], # PC852 Latin2 + 'PC858': + [ESC + b'\x74\x13', 'cp858'], # PC858 Euro + 'KU42': + [ESC + b'\x74\x14', ''], # KU42 Thai + 'TIS11': + [ESC + b'\x74\x15', ''], # TIS11 Thai + 'TIS18': + [ESC + b'\x74\x1a', ''], # TIS18 Thai + 'TCVN3': + [ESC + b'\x74\x1e', ''], # TCVN3 Vietnamese + 'TCVN3B': + [ESC + b'\x74\x1f', ''], # TCVN3 Vietnamese + 'PC720': + [ESC + b'\x74\x20', 'cp720'], # PC720 Arabic + 'WPC775': + [ESC + b'\x74\x21', ''], # WPC775 Baltic Rim + 'PC855': + [ESC + b'\x74\x22', 'cp855'], # PC855 Cyrillic + 'PC861': + [ESC + b'\x74\x23', 'cp861'], # PC861 Icelandic + 'PC862': + [ESC + b'\x74\x24', 'cp862'], # PC862 Hebrew + 'PC864': + [ESC + b'\x74\x25', 'cp864'], # PC864 Arabic + 'PC869': + [ESC + b'\x74\x26', 'cp869'], # PC869 Greek + '8859_2': + [ESC + b'\x74\x27', 'iso8859_2'], # ISO8859-2 Latin2 + '8859_9': + [ESC + b'\x74\x28', 'iso8859_9'], # ISO8859-2 Latin9 + 'PC1098': + [ESC + b'\x74\x29', 'cp1098'], # PC1098 Farsi + 'PC1118': + [ESC + b'\x74\x2a', 'cp1118'], # PC1118 Lithuanian + 'PC1119': + [ESC + b'\x74\x2b', 'cp1119'], # PC1119 Lithuanian + 'PC1125': + [ESC + b'\x74\x2c', 'cp1125'], # PC1125 Ukrainian + 'WPC1250': + [ESC + b'\x74\x2d', 'cp1250'], # WPC1250 Latin2 + 'WPC1251': + [ESC + b'\x74\x2e', 'cp1251'], # WPC1251 Cyrillic + 'WPC1253': + [ESC + b'\x74\x2f', 'cp1253'], # WPC1253 Greek + 'WPC1254': + [ESC + b'\x74\x30', 'cp1254'], # WPC1254 Turkish + 'WPC1255': + [ESC + b'\x74\x31', 'cp1255'], # WPC1255 Hebrew + 'WPC1256': + [ESC + b'\x74\x32', 'cp1256'], # WPC1256 Arabic + 'WPC1257': + [ESC + b'\x74\x33', 'cp1257'], # WPC1257 Baltic Rim + 'WPC1258': + [ESC + b'\x74\x34', 'cp1258'], # WPC1258 Vietnamese + 'KZ1048': + [ESC + b'\x74\x35', 'kz1048'], # KZ-1048 Kazakhstan +} # Barcode format _SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n From 0cfedb5706faec4a30b22fb982a935042cde404b Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sat, 23 Jul 2016 22:16:11 +0200 Subject: [PATCH 03/15] add automatic codepage-changing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This code is adapted from the works by Frédéric Van der Essen in pyxmlescpos. I had to adapt the code completely in order to make it compatible with modern unicode-handling Further changes: * improve text unittests in CLI and MagicEncode with hypothesis * add feature force_encoding in order to enable old behaviour * disable cli_text_test (for now) * fix charcode(): it does now cooperate with the new structure * remove redundant variable codepage from class Escpos --- src/escpos/escpos.py | 92 ++----------- src/escpos/magicencode.py | 252 ++++++++++++++++++++++++++++++++++ test/Dies ist ein Test.LF.txt | 1 - test/test_cli.py | 3 +- test/test_function_text.py | 36 ++--- test/test_magicencode.py | 114 ++++++++++----- 6 files changed, 357 insertions(+), 141 deletions(-) create mode 100644 src/escpos/magicencode.py delete mode 100644 test/Dies ist ein Test.LF.txt diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 081130a..05e4ab4 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -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 @@ -33,13 +34,13 @@ class Escpos(object): class. """ device = None - codepage = None - def __init__(self, columns=32): + def __init__(self, columns=32, **kwargs): """ Initialize ESCPOS Printer :param columns: Text columns used by the printer. Defaults to 32.""" self.columns = columns + self.magic = MagicEncode(**kwargs) def __del__(self): """ call self.close upon deletion """ @@ -203,82 +204,21 @@ 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.encoding = self.magic.codepage_sequence(code) + self.magic.force_encoding = True def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True, function_type="A"): """ Print Barcode @@ -418,14 +358,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._raw(self.magic.encode_text(txt=txt)) def block_text(self, txt, columns=None): """ Text is printed wrapped to specified columns diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py new file mode 100644 index 0000000..61f5f8e --- /dev/null +++ b/src/escpos/magicencode.py @@ -0,0 +1,252 @@ +#!/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 `_ +:organization: `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 .constants import CHARCODE +from .exceptions import CharCodeError, Error +import copy +import six + +try: + import jcconv +except ImportError: + jcconv = None + +class MagicEncode(object): + """ Magic Encode Class + + It tries to automatically encode utf-8 input into the right coding. When encoding is impossible a configurable + symbol will be inserted. + """ + def __init__(self, startencoding='PC437', force_encoding=False, defaultsymbol=b'', defaultencoding='PC437'): + # running these functions makes sure that the encoding is suitable + MagicEncode.codepage_name(startencoding) + MagicEncode.codepage_name(defaultencoding) + + self.encoding = startencoding + self.defaultsymbol = defaultsymbol + if type(self.defaultsymbol) is not six.binary_type: + raise Error("The supplied symbol {sym} has to be a binary string".format(sym=defaultsymbol)) + self.defaultencoding = defaultencoding + self.force_encoding = force_encoding + + def set_encoding(self, encoding='PC437', force_encoding=False): + """sets an encoding (normally not used) + + This function should normally not be used since it manipulates the automagic behaviour. However, if you want to + force a certain codepage, then you can use this function. + + :param encoding: must be a valid encoding from CHARCODE + :param force_encoding: whether the encoding should not be changed automatically + """ + self.codepage_name(encoding) + self.encoding = encoding + self.force_encoding = force_encoding + + @staticmethod + def codepage_sequence(codepage): + """returns the corresponding codepage-sequence""" + try: + return CHARCODE[codepage][0] + except KeyError: + raise CharCodeError("The encoding {enc} is unknown.".format(enc=codepage)) + + @staticmethod + def codepage_name(codepage): + """returns the corresponding codepage-name (for python)""" + try: + name = CHARCODE[codepage][1] + if name == '': + raise CharCodeError("The codepage {enc} does not have a connected python-codepage".format(enc=codepage)) + return name + except KeyError: + raise CharCodeError("The encoding {enc} is unknown.".format(enc=codepage)) + + def encode_char(self, char): + """ + Encodes a single unicode character into a sequence of + esc-pos code page change instructions and character declarations + """ + if type(char) is not six.text_type: + raise Error("The supplied text has to be unicode, but is of type {type}.".format( + type=type(char) + )) + encoded = b'' + encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character + remaining = copy.copy(CHARCODE) + + while True: # Trying all encoding until one succeeds + try: + if encoding == 'KATAKANA': # Japanese characters + if jcconv: + # try to convert japanese text to half-katakanas + kata = jcconv.kata2half(jcconv.hira2kata(char)) + if kata != char: + self.extra_chars += len(kata) - 1 + # the conversion may result in multiple characters + return self.encode_str(kata) + else: + kata = char + + if kata in TXT_ENC_KATAKANA_MAP: + encoded = TXT_ENC_KATAKANA_MAP[kata] + break + else: + raise ValueError() + else: + try: + enc_name = MagicEncode.codepage_name(encoding) + encoded = char.encode(enc_name) + assert type(encoded) is bytes + except LookupError: + raise ValueError("The encoding {enc} seems to not exist in Python".format(enc=encoding)) + except CharCodeError: + raise ValueError("The encoding {enc} is not fully configured in constants".format( + enc=encoding + )) + break + + except ValueError: # the encoding failed, select another one and retry + if encoding in remaining: + del remaining[encoding] + if len(remaining) >= 1: + encoding = list(remaining)[0] + else: + encoding = self.defaultencoding + encoded = self.defaultsymbol # could not encode, output error character + break + + if encoding != self.encoding: + # if the encoding changed, remember it and prefix the character with + # the esc-pos encoding change sequence + self.encoding = encoding + encoded = CHARCODE[encoding][0] + encoded + + return encoded + + def encode_str(self, txt): + # make sure the right codepage is set in the printer + buffer = self.codepage_sequence(self.encoding) + if self.force_encoding: + buffer += txt.encode(self.codepage) + else: + for c in txt: + buffer += self.encode_char(c) + return buffer + + def encode_text(self, txt): + """returns a byte-string with encoded text + + :param txt: text that shall be encoded + :return: byte-string for the printer + """ + if not txt: + return + + self.extra_chars = 0 + + txt = self.encode_str(txt) + + # if the utf-8 -> codepage conversion inserted extra characters, + # remove double spaces to try to restore the original string length + # and prevent printing alignment issues + while self.extra_chars > 0: + dspace = txt.find(' ') + if dspace > 0: + txt = txt[:dspace] + txt[dspace+1:] + self.extra_chars -= 1 + else: + break + + return txt + + +# todo emoticons mit charmap encoden +# todo Escpos liste von unterdrückten charcodes mitgeben +# todo Doku anpassen +# todo Changelog schreiben + + +TXT_ENC_KATAKANA_MAP = { + # Maps UTF-8 Katakana symbols to KATAKANA Page Codes + + # 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', +} diff --git a/test/Dies ist ein Test.LF.txt b/test/Dies ist ein Test.LF.txt deleted file mode 100644 index d7e5cff..0000000 --- a/test/Dies ist ein Test.LF.txt +++ /dev/null @@ -1 +0,0 @@ -Dies ist ein Test. diff --git a/test/test_cli.py b/test/test_cli.py index b9aebc3..817e305 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -10,7 +10,7 @@ from __future__ import unicode_literals import os import sys from scripttest import TestFileEnvironment -from nose.tools import assert_equals +from nose.tools import assert_equals, nottest import escpos TEST_DIR = os.path.abspath('test/test-cli-output') @@ -89,6 +89,7 @@ class TestCLI(): assert not result.stderr assert_equals(escpos.__version__, result.stdout.strip()) + @nottest # disable this test as it is not that easy anymore to predict the outcome of this call def test_cli_text(self): """ Make sure text returns what we sent it """ test_text = 'this is some text' diff --git a/test/test_function_text.py b/test/test_function_text.py index b0b1ca1..c9b0bd0 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -12,34 +12,16 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -from nose.tools import with_setup +import mock +from hypothesis import given +import hypothesis.strategies as st import escpos.printer as printer -import os -import filecmp - -devfile = 'testfile' - - -def setup_testfile(): - """create a testfile as devfile""" - fhandle = open(devfile, 'a') - try: - os.utime(devfile, None) - finally: - fhandle.close() - - -def teardown_testfile(): - """destroy testfile again""" - os.remove(devfile) - - -@with_setup(setup_testfile, teardown_testfile) -def test_function_text_dies_ist_ein_test_lf(): +@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.File(devfile=devfile) - instance.text('Dies ist ein Test.\n') - instance.flush() - assert(filecmp.cmp('test/Dies ist ein Test.LF.txt', devfile)) + instance = printer.Dummy() + instance.magic.encode_text = mock.Mock() + instance.text(text) + instance.magic.encode_text.assert_called_with(txt=text) diff --git a/test/test_magicencode.py b/test/test_magicencode.py index 403bc75..2789da7 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -1,5 +1,6 @@ #!/usr/bin/python -"""tests for panel button function +# -*- coding: utf-8 -*- +"""tests for the magic encode module :author: `Patrick Kanzler `_ :organization: `python-escpos `_ @@ -12,43 +13,90 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -from nose.tools import with_setup +from nose.tools import raises, assert_raises +from hypothesis import given, example +import hypothesis.strategies as st +from escpos.magicencode import MagicEncode +from escpos.exceptions import CharCodeError, Error +from escpos.constants import CHARCODE -import escpos.printer as printer -import os +@raises(CharCodeError) +def test_magic_encode_unkown_char_constant_as_startenc(): + """tests whether MagicEncode raises the proper Exception when an unknown charcode-name is passed as startencoding""" + MagicEncode(startencoding="something") -devfile = 'testfile' +@raises(CharCodeError) +def test_magic_encode_unkown_char_constant_as_defaultenc(): + """tests whether MagicEncode raises the proper Exception when an unknown charcode-name is passed as defaultenc.""" + MagicEncode(defaultencoding="something") + +def test_magic_encode_wo_arguments(): + """tests whether MagicEncode works in the standard configuration""" + MagicEncode() + +@raises(Error) +def test_magic_encode_w_non_binary_defaultsymbol(): + """tests whether MagicEncode catches non-binary defaultsymbols""" + MagicEncode(defaultsymbol="non-binary") + +@given(symbol=st.binary()) +def test_magic_encode_w_binary_defaultsymbol(symbol): + """tests whether MagicEncode works with any binary symbol""" + MagicEncode(defaultsymbol=symbol) + +@given(st.text()) +@example("カタカナ") +@example("あいうえお") +@example("ハンカクカタカナ") +def test_magic_encode_encode_text_unicode_string(text): + """tests whether MagicEncode can accept a unicode string""" + me = MagicEncode() + me.encode_text(text) + +@given(char=st.characters()) +def test_magic_encode_encode_char(char): + """tests the encode_char-method of MagicEncode""" + me = MagicEncode() + me.encode_char(char) + +@raises(Error) +@given(char=st.binary()) +def test_magic_encode_encode_char_binary(char): + """tests the encode_char-method of MagicEncode with binary input""" + me = MagicEncode() + me.encode_char(char) -def setup_testfile(): - """create a testfile as devfile""" - fhandle = open(devfile, 'a') - try: - os.utime(devfile, None) - finally: - fhandle.close() +def test_magic_encode_string_with_katakana_and_hiragana(): + """tests the encode_string-method with katakana and hiragana""" + me = MagicEncode() + me.encode_str("カタカナ") + me.encode_str("あいうえお") +@raises(CharCodeError) +def test_magic_encode_codepage_sequence_unknown_key(): + """tests whether MagicEncode.codepage_sequence raises the proper Exception with unknown charcode-names""" + MagicEncode.codepage_sequence("something") -def teardown_testfile(): - """destroy testfile again""" - os.remove(devfile) +@raises(CharCodeError) +def test_magic_encode_codepage_name_unknown_key(): + """tests whether MagicEncode.codepage_name raises the proper Exception with unknown charcode-names""" + MagicEncode.codepage_name("something") +def test_magic_encode_constants_getter(): + """tests whether the constants are properly fetched""" + for key in CHARCODE: + name = CHARCODE[key][1] + if name == '': + assert_raises(CharCodeError, MagicEncode.codepage_name, key) + else: + assert name == MagicEncode.codepage_name(key) + assert MagicEncode.codepage_sequence(key) == CHARCODE[key][0] -@with_setup(setup_testfile, teardown_testfile) -def test_function_panel_button_on(): - """test the panel button function (enabling) by comparing output""" - instance = printer.File(devfile=devfile) - instance.panel_buttons() - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'\x1B\x63\x35\x00') - - -@with_setup(setup_testfile, teardown_testfile) -def test_function_panel_button_off(): - """test the panel button function (disabling) by comparing output""" - instance = printer.File(devfile=devfile) - instance.panel_buttons(False) - instance.flush() - with open(devfile, "rb") as f: - assert(f.read() == b'\x1B\x63\x35\x01') +def test_magic_encode_force_encoding(): + """test whether force_encoding works as expected""" + me = MagicEncode() + assert me.force_encoding is False + me.set_encoding(encoding='KATAKANA', force_encoding=True) + assert me.encoding == 'KATAKANA' + assert me.force_encoding is True From 13937ab0da58a41b17fb4c6f5db17312d882a8c8 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sun, 24 Jul 2016 02:14:23 +0200 Subject: [PATCH 04/15] doc update documentation regarding codepages --- doc/user/usage.rst | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/doc/user/usage.rst b/doc/user/usage.rst index 1d848a1..3ef1c58 100644 --- a/doc/user/usage.rst +++ b/doc/user/usage.rst @@ -177,6 +177,20 @@ And for a network printer:: host: 127.0.0.1 port: 9000 +Printing text right +------------------- +Python-escpos is designed to accept unicode. So make sure that you use ``u'strings'`` or import ``unicode_literals`` +from ``__future__`` if you are on Python2. On Version 3 you should be fine. + +For normal usage you can simply pass your text to the printers ``text()``-function. It will automatically guess +the right codepage and then send the encoded data to the printer. If this feature should not work, please try to +isolate the error and then create an issue. + +I you want or need to you can manually set the codepage. For this please use the ``charcode()``-function. You can set +any key-value that is in ``CHARCODE``. If something is wrong, an ``CharCodeError`` will be raised. +After you have set the codepage manually the printer won't change it anymore. You can get back to normal behaviour +by setting charcode to ``AUTO``. + Advanced Usage: Print from binary blob -------------------------------------- @@ -204,19 +218,3 @@ Here you can download an example, that will print a set of common barcodes: * :download:`barcode.bin ` by `@mike42 `_ -How to update your code for USB printers ----------------------------------------- - -Old code - -:: - - Epson = escpos.Escpos(0x04b8,0x0202,0) - -New code - -:: - - Epson = printer.Usb(0x04b8,0x0202) - -Nothe that "0" which is the interface number is no longer needed. From f0bdbc4322f45f979a1c3f9f5894f3b30982ac4a Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sun, 24 Jul 2016 02:18:18 +0200 Subject: [PATCH 05/15] doc changelog and todos updated --- CHANGELOG.rst | 3 +++ src/escpos/magicencode.py | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4a04f09..47227ae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,9 +6,12 @@ Changelog changes ^^^^^^^ +- feature: the driver tries now to guess the appropriate codepage and sets it automatically +- as an alternative you can force the codepage with the old API contributors ^^^^^^^^^^^^ +- Patrick Kanzler (with code by Frédéric Van der Essen) 2016-08-02 - Version 2.1.1 - "Contents May Differ" diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 61f5f8e..9e7aeb6 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -178,9 +178,6 @@ class MagicEncode(object): # todo emoticons mit charmap encoden # todo Escpos liste von unterdrückten charcodes mitgeben -# todo Doku anpassen -# todo Changelog schreiben - TXT_ENC_KATAKANA_MAP = { # Maps UTF-8 Katakana symbols to KATAKANA Page Codes From 046a08896c42721c2b56fd7b3ad5a67ec17fac32 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 25 Jul 2016 16:52:24 +0200 Subject: [PATCH 06/15] =?UTF-8?q?Ideen=20f=C3=BCr=20unittest=20REBASE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_magicencode.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/test_magicencode.py b/test/test_magicencode.py index 2789da7..3c7f356 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -100,3 +100,12 @@ def test_magic_encode_force_encoding(): me.set_encoding(encoding='KATAKANA', force_encoding=True) assert me.encoding == 'KATAKANA' assert me.force_encoding is True + + +# TODO Idee für unittest: hypothesis-strings erzeugen, in encode_text werfen +# Ergebnis durchgehen: Vorkommnisse von Stuersequenzen suchen und daran den Text splitten in ein sortiertes dict mit Struktur: +# encoding: textfolge +# das alles wieder in unicode dekodieren mit den codepages und dann zusammenbauen +# fertigen String mit hypothesis-string vergleichen (Achtung bei katana-conversion. Die am besten auch auf den hypothesis-string +# anwenden) +# TODO bei nicht kodierbarem Zeichen Fehler werfen! Als Option das verhalten von jetzt hinzufügen From 87a66470530bd754f2681171ad8ff013b93aafe7 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 25 Jul 2016 17:25:13 +0200 Subject: [PATCH 07/15] fix force-encoding REBASE (contains todos) * fixed the code of forced-encoding in order to make it work * extended unittest for forced-encoding * fixed the constant for Katakana-encoding --- src/escpos/constants.py | 2 +- src/escpos/magicencode.py | 3 ++- test/test_magicencode.py | 18 +++++++++++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 32c1a76..f91cbdf 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -106,7 +106,7 @@ CHARCODE = { 'PC437': [ESC + b'\x74\x00', 'cp437'], # PC437 USA 'KATAKANA': - [ESC + b'\x74\x01', 'katakana'], # KATAKANA (JAPAN) + [ESC + b'\x74\x01', ''], # KATAKANA (JAPAN) 'PC850': [ESC + b'\x74\x02', 'cp850'], # PC850 Multilingual 'PC860': diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 9e7aeb6..1091b31 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -143,7 +143,7 @@ class MagicEncode(object): # make sure the right codepage is set in the printer buffer = self.codepage_sequence(self.encoding) if self.force_encoding: - buffer += txt.encode(self.codepage) + buffer += txt.encode(self.codepage_name(self.encoding)) else: for c in txt: buffer += self.encode_char(c) @@ -178,6 +178,7 @@ class MagicEncode(object): # todo emoticons mit charmap encoden # todo Escpos liste von unterdrückten charcodes mitgeben +# TODO Sichtbarkeit der Methode anpassen (Eigentlich braucht man nur die set_encode und die encode_text) TXT_ENC_KATAKANA_MAP = { # Maps UTF-8 Katakana symbols to KATAKANA Page Codes diff --git a/test/test_magicencode.py b/test/test_magicencode.py index 3c7f356..eb0f07b 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -93,12 +93,22 @@ def test_magic_encode_constants_getter(): assert name == MagicEncode.codepage_name(key) assert MagicEncode.codepage_sequence(key) == CHARCODE[key][0] -def test_magic_encode_force_encoding(): +@given(st.text()) +def test_magic_encode_force_encoding(text): """test whether force_encoding works as expected""" me = MagicEncode() assert me.force_encoding is False - me.set_encoding(encoding='KATAKANA', force_encoding=True) - assert me.encoding == 'KATAKANA' + me.set_encoding(encoding='PC850', force_encoding=True) + assert me.encoding == 'PC850' + assert me.force_encoding is True + try: + me.encode_text(text) + except UnicodeEncodeError: + # we discard these errors as they are to be expected + # what we want to check here is, whether encoding or codepage will switch through some of the magic code + # being called accidentally + pass + assert me.encoding == 'PC850' assert me.force_encoding is True @@ -109,3 +119,5 @@ def test_magic_encode_force_encoding(): # fertigen String mit hypothesis-string vergleichen (Achtung bei katana-conversion. Die am besten auch auf den hypothesis-string # anwenden) # TODO bei nicht kodierbarem Zeichen Fehler werfen! Als Option das verhalten von jetzt hinzufügen +# TODO tests sollten eigentlich nicht gehen, wenn encode_char gerufen wird (extra_char ist nicht definiert) +# TODO verhalten bei leerem String festlegen und testen From 214aa0d36301ca72a0a57201221f3b80e340305e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Fri, 26 Aug 2016 15:14:02 +0200 Subject: [PATCH 08/15] Fix issue with manually setting the encoding. --- src/escpos/escpos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 05e4ab4..276ded9 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -217,7 +217,8 @@ class Escpos(object): if code.upper() == "AUTO": self.magic.force_encoding = False else: - self.magic.encoding = self.magic.codepage_sequence(code) + self.magic.codepage_sequence(code) + self.magic.encoding = code self.magic.force_encoding = True def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True, function_type="A"): From c7864fd7850a7ff05b591e99b4f1d1e0aabc3f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Sat, 27 Aug 2016 11:09:08 +0200 Subject: [PATCH 09/15] Largely rewrite the magic text encoding feature. --- src/escpos/capabilities.yml | 34 ++-- src/escpos/constants.py | 186 ++++++++++---------- src/escpos/escpos.py | 12 +- src/escpos/magicencode.py | 336 ++++++++++++++++++++++-------------- test/conftest.py | 7 + test/test_magicencode.py | 168 +++++++++--------- 6 files changed, 411 insertions(+), 332 deletions(-) create mode 100644 test/conftest.py diff --git a/src/escpos/capabilities.yml b/src/escpos/capabilities.yml index e105687..5849218 100644 --- a/src/escpos/capabilities.yml +++ b/src/escpos/capabilities.yml @@ -80,8 +80,8 @@ default: #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 + 30: 'TCVN-3-1' # TCVN-3: Vietnamese + 31: 'TCVN-3-2' # TCVN-3: Vietnamese 32: "CP720" 33: "CP775" 34: "CP855" @@ -152,13 +152,13 @@ epson: a: 42 b: 56 codePages: - - PC437 # 0 + - cp437 # 0 - Katakana # 1 - - PC850 # 2 - - PC860 # 3 - - PC863 # 4 - - PC865 # 5 - - PC858 # 19 + - 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 @@ -168,16 +168,16 @@ epson: a: 42 b: 56 codePages: - - PC437 # 0 + - CP437 # 0 - Katakana # 1 - - PC850 # 2 - - PC860 # 3 - - PC863 # 4 - - PC865 # 5 - - WPC1252 # 16 - - PC866 # 17 - - PC852 # 18 - - PC858 # 19 + - CP850 # 2 + - CP860 # 3 + - CP863 # 4 + - CP865 # 5 + - PC1252 # 16 + - CP866 # 17 + - CP852 # 18 + - CP858 # 19 - blank diff --git a/src/escpos/constants.py b/src/escpos/constants.py index b9625c1..aa7857b 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -101,99 +101,101 @@ 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 + +CODEPAGE_CHANGE = ESC + b'\x74' # Char code table -CHARCODE = { - 'PC437': - [ESC + b'\x74\x00', 'cp437'], # PC437 USA - 'KATAKANA': - [ESC + b'\x74\x01', ''], # KATAKANA (JAPAN) - 'PC850': - [ESC + b'\x74\x02', 'cp850'], # PC850 Multilingual - 'PC860': - [ESC + b'\x74\x03', 'cp860'], # PC860 Portuguese - 'PC863': - [ESC + b'\x74\x04', 'cp863'], # PC863 Canadian-French - 'PC865': - [ESC + b'\x74\x05', 'cp865'], # PC865 Nordic - 'KANJI6': - [ESC + b'\x74\x06', ''], # One-pass Kanji, Hiragana - 'KANJI7': - [ESC + b'\x74\x07', ''], # One-pass Kanji - 'KANJI8': - [ESC + b'\x74\x08', ''], # One-pass Kanji - 'PC851': - [ESC + b'\x74\x0b', 'cp851'], # PC851 Greek - 'PC853': - [ESC + b'\x74\x0c', 'cp853'], # PC853 Turkish - 'PC857': - [ESC + b'\x74\x0d', 'cp857'], # PC857 Turkish - 'PC737': - [ESC + b'\x74\x0e', 'cp737'], # PC737 Greek - '8859_7': - [ESC + b'\x74\x0f', 'iso8859_7'], # ISO8859-7 Greek - 'WPC1252': - [ESC + b'\x74\x10', 'cp1252'], # WPC1252 - 'PC866': - [ESC + b'\x74\x11', 'cp866'], # PC866 Cyrillic #2 - 'PC852': - [ESC + b'\x74\x12', 'cp852'], # PC852 Latin2 - 'PC858': - [ESC + b'\x74\x13', 'cp858'], # PC858 Euro - 'KU42': - [ESC + b'\x74\x14', ''], # KU42 Thai - 'TIS11': - [ESC + b'\x74\x15', ''], # TIS11 Thai - 'TIS18': - [ESC + b'\x74\x1a', ''], # TIS18 Thai - 'TCVN3': - [ESC + b'\x74\x1e', ''], # TCVN3 Vietnamese - 'TCVN3B': - [ESC + b'\x74\x1f', ''], # TCVN3 Vietnamese - 'PC720': - [ESC + b'\x74\x20', 'cp720'], # PC720 Arabic - 'WPC775': - [ESC + b'\x74\x21', ''], # WPC775 Baltic Rim - 'PC855': - [ESC + b'\x74\x22', 'cp855'], # PC855 Cyrillic - 'PC861': - [ESC + b'\x74\x23', 'cp861'], # PC861 Icelandic - 'PC862': - [ESC + b'\x74\x24', 'cp862'], # PC862 Hebrew - 'PC864': - [ESC + b'\x74\x25', 'cp864'], # PC864 Arabic - 'PC869': - [ESC + b'\x74\x26', 'cp869'], # PC869 Greek - '8859_2': - [ESC + b'\x74\x27', 'iso8859_2'], # ISO8859-2 Latin2 - '8859_9': - [ESC + b'\x74\x28', 'iso8859_9'], # ISO8859-2 Latin9 - 'PC1098': - [ESC + b'\x74\x29', 'cp1098'], # PC1098 Farsi - 'PC1118': - [ESC + b'\x74\x2a', 'cp1118'], # PC1118 Lithuanian - 'PC1119': - [ESC + b'\x74\x2b', 'cp1119'], # PC1119 Lithuanian - 'PC1125': - [ESC + b'\x74\x2c', 'cp1125'], # PC1125 Ukrainian - 'WPC1250': - [ESC + b'\x74\x2d', 'cp1250'], # WPC1250 Latin2 - 'WPC1251': - [ESC + b'\x74\x2e', 'cp1251'], # WPC1251 Cyrillic - 'WPC1253': - [ESC + b'\x74\x2f', 'cp1253'], # WPC1253 Greek - 'WPC1254': - [ESC + b'\x74\x30', 'cp1254'], # WPC1254 Turkish - 'WPC1255': - [ESC + b'\x74\x31', 'cp1255'], # WPC1255 Hebrew - 'WPC1256': - [ESC + b'\x74\x32', 'cp1256'], # WPC1256 Arabic - 'WPC1257': - [ESC + b'\x74\x33', 'cp1257'], # WPC1257 Baltic Rim - 'WPC1258': - [ESC + b'\x74\x34', 'cp1258'], # WPC1258 Vietnamese - 'KZ1048': - [ESC + b'\x74\x35', 'kz1048'], # KZ-1048 Kazakhstan -} +# CHARCODE = { +# 'PC437': +# [ESC + b'\x74\x00', 'cp437'], # PC437 USA +# 'KATAKANA': +# [ESC + b'\x74\x01', ''], # KATAKANA (JAPAN) +# 'PC850': +# [ESC + b'\x74\x02', 'cp850'], # PC850 Multilingual +# 'PC860': +# [ESC + b'\x74\x03', 'cp860'], # PC860 Portuguese +# 'PC863': +# [ESC + b'\x74\x04', 'cp863'], # PC863 Canadian-French +# 'PC865': +# [ESC + b'\x74\x05', 'cp865'], # PC865 Nordic +# 'KANJI6': +# [ESC + b'\x74\x06', ''], # One-pass Kanji, Hiragana +# 'KANJI7': +# [ESC + b'\x74\x07', ''], # One-pass Kanji +# 'KANJI8': +# [ESC + b'\x74\x08', ''], # One-pass Kanji +# 'PC851': +# [ESC + b'\x74\x0b', 'cp851'], # PC851 Greek +# 'PC853': +# [ESC + b'\x74\x0c', 'cp853'], # PC853 Turkish +# 'PC857': +# [ESC + b'\x74\x0d', 'cp857'], # PC857 Turkish +# 'PC737': +# [ESC + b'\x74\x0e', 'cp737'], # PC737 Greek +# '8859_7': +# [ESC + b'\x74\x0f', 'iso8859_7'], # ISO8859-7 Greek +# 'WPC1252': +# [ESC + b'\x74\x10', 'cp1252'], # WPC1252 +# 'PC866': +# [ESC + b'\x74\x11', 'cp866'], # PC866 Cyrillic #2 +# 'PC852': +# [ESC + b'\x74\x12', 'cp852'], # PC852 Latin2 +# 'PC858': +# [ESC + b'\x74\x13', 'cp858'], # PC858 Euro +# 'KU42': +# [ESC + b'\x74\x14', ''], # KU42 Thai +# 'TIS11': +# [ESC + b'\x74\x15', ''], # TIS11 Thai +# 'TIS18': +# [ESC + b'\x74\x1a', ''], # TIS18 Thai +# 'TCVN3': +# [ESC + b'\x74\x1e', ''], # TCVN3 Vietnamese +# 'TCVN3B': +# [ESC + b'\x74\x1f', ''], # TCVN3 Vietnamese +# 'PC720': +# [ESC + b'\x74\x20', 'cp720'], # PC720 Arabic +# 'WPC775': +# [ESC + b'\x74\x21', ''], # WPC775 Baltic Rim +# 'PC855': +# [ESC + b'\x74\x22', 'cp855'], # PC855 Cyrillic +# 'PC861': +# [ESC + b'\x74\x23', 'cp861'], # PC861 Icelandic +# 'PC862': +# [ESC + b'\x74\x24', 'cp862'], # PC862 Hebrew +# 'PC864': +# [ESC + b'\x74\x25', 'cp864'], # PC864 Arabic +# 'PC869': +# [ESC + b'\x74\x26', 'cp869'], # PC869 Greek +# '8859_2': +# [ESC + b'\x74\x27', 'iso8859_2'], # ISO8859-2 Latin2 +# '8859_9': +# [ESC + b'\x74\x28', 'iso8859_9'], # ISO8859-2 Latin9 +# 'PC1098': +# [ESC + b'\x74\x29', 'cp1098'], # PC1098 Farsi +# 'PC1118': +# [ESC + b'\x74\x2a', 'cp1118'], # PC1118 Lithuanian +# 'PC1119': +# [ESC + b'\x74\x2b', 'cp1119'], # PC1119 Lithuanian +# 'PC1125': +# [ESC + b'\x74\x2c', 'cp1125'], # PC1125 Ukrainian +# 'WPC1250': +# [ESC + b'\x74\x2d', 'cp1250'], # WPC1250 Latin2 +# 'WPC1251': +# [ESC + b'\x74\x2e', 'cp1251'], # WPC1251 Cyrillic +# 'WPC1253': +# [ESC + b'\x74\x2f', 'cp1253'], # WPC1253 Greek +# 'WPC1254': +# [ESC + b'\x74\x30', 'cp1254'], # WPC1254 Turkish +# 'WPC1255': +# [ESC + b'\x74\x31', 'cp1255'], # WPC1255 Hebrew +# 'WPC1256': +# [ESC + b'\x74\x32', 'cp1256'], # WPC1256 Arabic +# 'WPC1257': +# [ESC + b'\x74\x33', 'cp1257'], # WPC1257 Baltic Rim +# 'WPC1258': +# [ESC + b'\x74\x34', 'cp1258'], # WPC1258 Vietnamese +# 'KZ1048': +# [ESC + b'\x74\x35', 'kz1048'], # KZ-1048 Kazakhstan +# } # Barcode format _SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index a217db7..2dc17e0 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -36,12 +36,12 @@ class Escpos(object): """ device = None - def __init__(self, profile=None, **kwargs): + 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(**kwargs) + self.magic = MagicEncode(self, **(magic_encode_args or {})) def __del__(self): """ call self.close upon deletion """ @@ -228,11 +228,9 @@ class Escpos(object): :raises: :py:exc:`~escpos.exceptions.CharCodeError` """ if code.upper() == "AUTO": - self.magic.force_encoding = False + self.magic.force_encoding(False) else: - self.magic.codepage_sequence(code) - self.magic.encoding = code - self.magic.force_encoding = True + self.magic.force_encoding(code) def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True, function_type="A"): """ Print Barcode @@ -373,7 +371,7 @@ class Escpos(object): :raises: :py:exc:`~escpos.exceptions.TextError` """ txt = six.text_type(txt) - self._raw(self.magic.encode_text(txt=txt)) + self.magic.write(txt) def block_text(self, txt, font=None, columns=None): """ Text is printed wrapped to specified columns diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 1091b31..ed8a4bc 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -17,8 +17,9 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -from .constants import CHARCODE +from .constants import CODEPAGE_CHANGE from .exceptions import CharCodeError, Error +from .capabilities import get_profile import copy import six @@ -27,153 +28,230 @@ try: except ImportError: jcconv = None + +def encode_katakana(text): + """I don't think this quite works yet.""" + encoded = [] + for char in text: + if jcconv: + # try to convert japanese text to half-katakanas + char = jcconv.kata2half(jcconv.hira2kata(char)) + # TODO: "the conversion may result in multiple characters" + # When? What should we do about it? + + if char in TXT_ENC_KATAKANA_MAP: + encoded.append(TXT_ENC_KATAKANA_MAP[char]) + else: + encoded.append(char) + print(encoded) + return b"".join(encoded) + + + +# TODO: When the capabilities.yml format is finished, this should be +# in the profile itself. +def get_encodings_from_profile(profile): + mapping = {k: v.lower() for k, v in profile.codePageMap.items()} + if hasattr(profile, 'codePages'): + code_pages = [n.lower() for n in profile.codePages] + return {k: v for k, v in mapping.items() if v in code_pages} + else: + return mapping + + +class CodePages: + def get_all(self): + return get_encodings_from_profile(get_profile()).values() + + def encode(self, text, encoding, errors='strict'): + # Python has not have this builtin? + if encoding.upper() == 'KATAKANA': + return encode_katakana(text) + + return text.encode(encoding, errors=errors) + + def get_encoding(self, encoding): + # resolve the encoding alias + return encoding.lower() + +code_pages = CodePages() + + +class Encoder(object): + """Takes a list of available code spaces. Picks the right one for a + given character. + + Note: To determine the codespace, 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, codepages): + self.codepages = codepages + self.reverse = {v:k for k, v in codepages.items()} + self.available_encodings = set(codepages.values()) + self.used_encodings = set() + + def get_sequence(self, encoding): + return self.reverse[encoding] + + def get_encoding(self, encoding): + """resolve aliases + + check that the profile allows this encoding + """ + encoding = code_pages.get_encoding(encoding) + if not encoding in self.available_encodings: + raise ValueError('This encoding cannot be used for the current profile') + return encoding + + def get_encodings(self): + """ + - remove the ones not supported + - order by used first, then others + - do not use a cache, because encode already is so fast + """ + return self.available_encodings + + def can_encode(self, encoding, char): + try: + encoded = code_pages.encode(char, encoding) + assert type(encoded) is bytes + return encoded + except LookupError: + # We don't have this encoding + return False + except UnicodeEncodeError: + return False + + return True + + def find_suitable_codespace(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. + + # XXX actually do speed up the search + """ + for encoding in self.get_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 + + class MagicEncode(object): """ Magic Encode Class It tries to automatically encode utf-8 input into the right coding. When encoding is impossible a configurable symbol will be inserted. + + 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, startencoding='PC437', force_encoding=False, defaultsymbol=b'', defaultencoding='PC437'): - # running these functions makes sure that the encoding is suitable - MagicEncode.codepage_name(startencoding) - MagicEncode.codepage_name(defaultencoding) + 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.encoding = startencoding + self.driver = driver + self.encoder = encoder or Encoder(get_encodings_from_profile(driver.profile)) + + self.encoding = self.encoder.get_encoding(encoding) if encoding else None self.defaultsymbol = defaultsymbol - if type(self.defaultsymbol) is not six.binary_type: - raise Error("The supplied symbol {sym} has to be a binary string".format(sym=defaultsymbol)) - self.defaultencoding = defaultencoding - self.force_encoding = force_encoding + self.disabled = disabled - def set_encoding(self, encoding='PC437', force_encoding=False): - """sets an encoding (normally not used) + def force_encoding(self, encoding): + """Sets a fixed encoding. The change is emitted right away. - This function should normally not be used since it manipulates the automagic behaviour. However, if you want to - force a certain codepage, then you can use this function. - - :param encoding: must be a valid encoding from CHARCODE - :param force_encoding: whether the encoding should not be changed automatically + From now one, this buffer will switch the code page anymore. + However, it will still keep track of the current code page. """ - self.codepage_name(encoding) - self.encoding = encoding - self.force_encoding = force_encoding - - @staticmethod - def codepage_sequence(codepage): - """returns the corresponding codepage-sequence""" - try: - return CHARCODE[codepage][0] - except KeyError: - raise CharCodeError("The encoding {enc} is unknown.".format(enc=codepage)) - - @staticmethod - def codepage_name(codepage): - """returns the corresponding codepage-name (for python)""" - try: - name = CHARCODE[codepage][1] - if name == '': - raise CharCodeError("The codepage {enc} does not have a connected python-codepage".format(enc=codepage)) - return name - except KeyError: - raise CharCodeError("The encoding {enc} is unknown.".format(enc=codepage)) - - def encode_char(self, char): - """ - Encodes a single unicode character into a sequence of - esc-pos code page change instructions and character declarations - """ - if type(char) is not six.text_type: - raise Error("The supplied text has to be unicode, but is of type {type}.".format( - type=type(char) - )) - encoded = b'' - encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character - remaining = copy.copy(CHARCODE) - - while True: # Trying all encoding until one succeeds - try: - if encoding == 'KATAKANA': # Japanese characters - if jcconv: - # try to convert japanese text to half-katakanas - kata = jcconv.kata2half(jcconv.hira2kata(char)) - if kata != char: - self.extra_chars += len(kata) - 1 - # the conversion may result in multiple characters - return self.encode_str(kata) - else: - kata = char - - if kata in TXT_ENC_KATAKANA_MAP: - encoded = TXT_ENC_KATAKANA_MAP[kata] - break - else: - raise ValueError() - else: - try: - enc_name = MagicEncode.codepage_name(encoding) - encoded = char.encode(enc_name) - assert type(encoded) is bytes - except LookupError: - raise ValueError("The encoding {enc} seems to not exist in Python".format(enc=encoding)) - except CharCodeError: - raise ValueError("The encoding {enc} is not fully configured in constants".format( - enc=encoding - )) - break - - except ValueError: # the encoding failed, select another one and retry - if encoding in remaining: - del remaining[encoding] - if len(remaining) >= 1: - encoding = list(remaining)[0] - else: - encoding = self.defaultencoding - encoded = self.defaultsymbol # could not encode, output error character - break - - if encoding != self.encoding: - # if the encoding changed, remember it and prefix the character with - # the esc-pos encoding change sequence - self.encoding = encoding - encoded = CHARCODE[encoding][0] + encoded - - return encoded - - def encode_str(self, txt): - # make sure the right codepage is set in the printer - buffer = self.codepage_sequence(self.encoding) - if self.force_encoding: - buffer += txt.encode(self.codepage_name(self.encoding)) + if not encoding: + self.disabled = False else: - for c in txt: - buffer += self.encode_char(c) - return buffer + self.write_with_encoding(encoding, None) + self.disabled = True - def encode_text(self, txt): - """returns a byte-string with encoded text - - :param txt: text that shall be encoded - :return: byte-string for the printer + def write(self, text): + """Write the text, automatically switching encodings. """ - if not txt: + + if self.disabled: + self.write_with_encoding(self.encoding, text) return - self.extra_chars = 0 + # TODO: Currently this very simple loop means we send every + # character individually to the printer. We can probably + # improve performace by searching the text for the first + # character that cannot be rendered using the current code + # page, and then sending all of those characters at once. + # Or, should a lower-level buffer be responsible for that? - txt = self.encode_str(txt) + for char in text: + # See if the current code page works for this character. + # The encoder object will use a cache to be able to answer + # this question fairly easily. + if self.encoding and self.encoder.can_encode(self.encoding, char): + self.write_with_encoding(self.encoding, char) + continue - # if the utf-8 -> codepage conversion inserted extra characters, - # remove double spaces to try to restore the original string length - # and prevent printing alignment issues - while self.extra_chars > 0: - dspace = txt.find(' ') - if dspace > 0: - txt = txt[:dspace] + txt[dspace+1:] - self.extra_chars -= 1 - else: - break + # We have to find another way to print this character. + # See if any of the code pages that the printer profile supports + # can encode this character. + codespace = self.encoder.find_suitable_codespace(char) + if not codespace: + self._handle_character_failed(char) + continue - return txt + self.write_with_encoding(codespace, char) + + 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) + )) + + encoding = self.encoder.get_encoding(encoding) + + # 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(b'{}{}'.format( + CODEPAGE_CHANGE, + six.int2byte(self.encoder.get_sequence(encoding)) + )) + + if text: + self.driver._raw(code_pages.encode(text, encoding, errors="replace")) # todo emoticons mit charmap encoden diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..2dad088 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,7 @@ +import pytest +from escpos.printer import Dummy + + +@pytest.fixture +def driver(): + return Dummy() diff --git a/test/test_magicencode.py b/test/test_magicencode.py index eb0f07b..24fb0db 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -13,103 +13,97 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals +import pytest from nose.tools import raises, assert_raises from hypothesis import given, example import hypothesis.strategies as st -from escpos.magicencode import MagicEncode +from escpos.magicencode import MagicEncode, Encoder, encode_katakana from escpos.exceptions import CharCodeError, Error -from escpos.constants import CHARCODE - -@raises(CharCodeError) -def test_magic_encode_unkown_char_constant_as_startenc(): - """tests whether MagicEncode raises the proper Exception when an unknown charcode-name is passed as startencoding""" - MagicEncode(startencoding="something") - -@raises(CharCodeError) -def test_magic_encode_unkown_char_constant_as_defaultenc(): - """tests whether MagicEncode raises the proper Exception when an unknown charcode-name is passed as defaultenc.""" - MagicEncode(defaultencoding="something") - -def test_magic_encode_wo_arguments(): - """tests whether MagicEncode works in the standard configuration""" - MagicEncode() - -@raises(Error) -def test_magic_encode_w_non_binary_defaultsymbol(): - """tests whether MagicEncode catches non-binary defaultsymbols""" - MagicEncode(defaultsymbol="non-binary") - -@given(symbol=st.binary()) -def test_magic_encode_w_binary_defaultsymbol(symbol): - """tests whether MagicEncode works with any binary symbol""" - MagicEncode(defaultsymbol=symbol) - -@given(st.text()) -@example("カタカナ") -@example("あいうえお") -@example("ハンカクカタカナ") -def test_magic_encode_encode_text_unicode_string(text): - """tests whether MagicEncode can accept a unicode string""" - me = MagicEncode() - me.encode_text(text) - -@given(char=st.characters()) -def test_magic_encode_encode_char(char): - """tests the encode_char-method of MagicEncode""" - me = MagicEncode() - me.encode_char(char) - -@raises(Error) -@given(char=st.binary()) -def test_magic_encode_encode_char_binary(char): - """tests the encode_char-method of MagicEncode with binary input""" - me = MagicEncode() - me.encode_char(char) -def test_magic_encode_string_with_katakana_and_hiragana(): - """tests the encode_string-method with katakana and hiragana""" - me = MagicEncode() - me.encode_str("カタカナ") - me.encode_str("あいうえお") -@raises(CharCodeError) -def test_magic_encode_codepage_sequence_unknown_key(): - """tests whether MagicEncode.codepage_sequence raises the proper Exception with unknown charcode-names""" - MagicEncode.codepage_sequence("something") +class TestEncoder: -@raises(CharCodeError) -def test_magic_encode_codepage_name_unknown_key(): - """tests whether MagicEncode.codepage_name raises the proper Exception with unknown charcode-names""" - MagicEncode.codepage_name("something") + def test_can_encode(self): + assert not Encoder({1: 'cp437'}).can_encode('cp437', u'€') + assert Encoder({1: 'cp437'}).can_encode('cp437', u'á') + assert not Encoder({1: 'foobar'}).can_encode('foobar', 'a') -def test_magic_encode_constants_getter(): - """tests whether the constants are properly fetched""" - for key in CHARCODE: - name = CHARCODE[key][1] - if name == '': - assert_raises(CharCodeError, MagicEncode.codepage_name, key) - else: - assert name == MagicEncode.codepage_name(key) - assert MagicEncode.codepage_sequence(key) == CHARCODE[key][0] + def test_find_suitable_encoding(self): + assert not Encoder({1: 'cp437'}).find_suitable_codespace(u'€') + assert Encoder({1: 'cp858'}).find_suitable_codespace(u'€') == 'cp858' -@given(st.text()) -def test_magic_encode_force_encoding(text): - """test whether force_encoding works as expected""" - me = MagicEncode() - assert me.force_encoding is False - me.set_encoding(encoding='PC850', force_encoding=True) - assert me.encoding == 'PC850' - assert me.force_encoding is True - try: - me.encode_text(text) - except UnicodeEncodeError: - # we discard these errors as they are to be expected - # what we want to check here is, whether encoding or codepage will switch through some of the magic code - # being called accidentally - pass - assert me.encoding == 'PC850' - assert me.force_encoding is True + @raises(ValueError) + def test_get_encoding(self): + Encoder({}).get_encoding('latin1') + + +class TestMagicEncode: + + class TestInit: + + def test_disabled_requires_encoding(self, driver): + with pytest.raises(Error): + MagicEncode(driver, disabled=True) + + class TestWriteWithEncoding: + + def test_init_from_none(self, driver): + encode = MagicEncode(driver, encoding=None) + encode.write_with_encoding('cp858', '€ ist teuro.') + assert driver.output == b'\x1bt\xd5 ist teuro.' + + def test_change_from_another(self, driver): + encode = MagicEncode(driver, encoding='cp437') + encode.write_with_encoding('cp858', '€ ist teuro.') + assert driver.output == b'\x1bt\xd5 ist teuro.' + + def test_no_change(self, driver): + encode = MagicEncode(driver, encoding='cp858') + encode.write_with_encoding('cp858', '€ ist teuro.') + assert driver.output == b'\xd5 ist teuro.' + + class TestWrite: + + def test_write(self, driver): + encode = MagicEncode(driver) + encode.write('€ ist teuro.') + assert driver.output == b'\x1bt\xa4 ist teuro.' + + def test_write_disabled(self, driver): + encode = MagicEncode(driver, encoding='cp437', disabled=True) + encode.write('€ ist teuro.') + assert driver.output == b'? ist teuro.' + + def test_write_no_codepage(self, driver): + encode = MagicEncode( + driver, defaultsymbol="_", encoder=Encoder({1: 'cp437'}), + encoding='cp437') + encode.write(u'€ ist teuro.') + assert driver.output == b'_ ist teuro.' + + class TestForceEncoding: + + def test(self, driver): + encode = MagicEncode(driver) + encode.force_encoding('cp437') + assert driver.output == b'\x1bt' + + encode.write('€ ist teuro.') + assert driver.output == b'\x1bt? ist teuro.' + + +class TestKatakana: + @given(st.text()) + @example("カタカナ") + @example("あいうえお") + @example("ハンカクカタカナ") + def test_accept(self, text): + encode_katakana(text) + + def test_result(self): + assert encode_katakana('カタカナ') == b'\xb6\xc0\xb6\xc5' + assert encode_katakana("あいうえお") == b'\xb1\xb2\xb3\xb4\xb5' # TODO Idee für unittest: hypothesis-strings erzeugen, in encode_text werfen From 630423d24a72083536412ff0e5c8134fa435e4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 13:33:35 +0200 Subject: [PATCH 10/15] Generate coverage reports. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5412ac3..b5555cc 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ deps = nose pytest pytest-cov hypothesis -commands = py.test +commands = py.test --cov reports [testenv:docs] basepython = python From 2f89f3fe3a72a475eb6a5ff682a25eef379f3f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 17:05:31 +0200 Subject: [PATCH 11/15] Port to current version of escpos-printer-db. --- src/escpos/capabilities.py | 9 +- src/escpos/codepages.py | 32 ++++++ src/escpos/constants.py | 96 +---------------- src/escpos/katakana.py | 104 +++++++++++++++++++ src/escpos/magicencode.py | 207 ++++++++----------------------------- test/test_function_text.py | 20 ++-- test/test_magicencode.py | 25 ++--- 7 files changed, 216 insertions(+), 277 deletions(-) create mode 100644 src/escpos/codepages.py create mode 100644 src/escpos/katakana.py diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index 3dd5c32..1330964 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -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): @@ -51,6 +52,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.lower(): 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 diff --git a/src/escpos/codepages.py b/src/escpos/codepages.py new file mode 100644 index 0000000..9666fb2 --- /dev/null +++ b/src/escpos/codepages.py @@ -0,0 +1,32 @@ +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 encode(self, text, encoding, errors='strict'): + """Adds support for Japanese to the builtin str.encode(). + + TODO: Add support for custom code page data from + escpos-printer-db. + """ + # Python has not have this builtin? + if encoding.upper() == 'KATAKANA': + return encode_katakana(text) + + return text.encode(encoding, errors=errors) + + def get_encoding(self, encoding): + # resolve the encoding alias + return encoding.lower() + + +CodePages = CodePageManager(CAPABILITIES['encodings']) \ No newline at end of file diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 2bec85c..c4e63af 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -123,101 +123,9 @@ LINESPACING_FUNCS = { 180: ESC + b'3', # line_spacing/180 of an inch, 0 <= line_spacing <= 255 } - +# 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' -# Char code table -# CHARCODE = { -# 'PC437': -# [ESC + b'\x74\x00', 'cp437'], # PC437 USA -# 'KATAKANA': -# [ESC + b'\x74\x01', ''], # KATAKANA (JAPAN) -# 'PC850': -# [ESC + b'\x74\x02', 'cp850'], # PC850 Multilingual -# 'PC860': -# [ESC + b'\x74\x03', 'cp860'], # PC860 Portuguese -# 'PC863': -# [ESC + b'\x74\x04', 'cp863'], # PC863 Canadian-French -# 'PC865': -# [ESC + b'\x74\x05', 'cp865'], # PC865 Nordic -# 'KANJI6': -# [ESC + b'\x74\x06', ''], # One-pass Kanji, Hiragana -# 'KANJI7': -# [ESC + b'\x74\x07', ''], # One-pass Kanji -# 'KANJI8': -# [ESC + b'\x74\x08', ''], # One-pass Kanji -# 'PC851': -# [ESC + b'\x74\x0b', 'cp851'], # PC851 Greek -# 'PC853': -# [ESC + b'\x74\x0c', 'cp853'], # PC853 Turkish -# 'PC857': -# [ESC + b'\x74\x0d', 'cp857'], # PC857 Turkish -# 'PC737': -# [ESC + b'\x74\x0e', 'cp737'], # PC737 Greek -# '8859_7': -# [ESC + b'\x74\x0f', 'iso8859_7'], # ISO8859-7 Greek -# 'WPC1252': -# [ESC + b'\x74\x10', 'cp1252'], # WPC1252 -# 'PC866': -# [ESC + b'\x74\x11', 'cp866'], # PC866 Cyrillic #2 -# 'PC852': -# [ESC + b'\x74\x12', 'cp852'], # PC852 Latin2 -# 'PC858': -# [ESC + b'\x74\x13', 'cp858'], # PC858 Euro -# 'KU42': -# [ESC + b'\x74\x14', ''], # KU42 Thai -# 'TIS11': -# [ESC + b'\x74\x15', ''], # TIS11 Thai -# 'TIS18': -# [ESC + b'\x74\x1a', ''], # TIS18 Thai -# 'TCVN3': -# [ESC + b'\x74\x1e', ''], # TCVN3 Vietnamese -# 'TCVN3B': -# [ESC + b'\x74\x1f', ''], # TCVN3 Vietnamese -# 'PC720': -# [ESC + b'\x74\x20', 'cp720'], # PC720 Arabic -# 'WPC775': -# [ESC + b'\x74\x21', ''], # WPC775 Baltic Rim -# 'PC855': -# [ESC + b'\x74\x22', 'cp855'], # PC855 Cyrillic -# 'PC861': -# [ESC + b'\x74\x23', 'cp861'], # PC861 Icelandic -# 'PC862': -# [ESC + b'\x74\x24', 'cp862'], # PC862 Hebrew -# 'PC864': -# [ESC + b'\x74\x25', 'cp864'], # PC864 Arabic -# 'PC869': -# [ESC + b'\x74\x26', 'cp869'], # PC869 Greek -# '8859_2': -# [ESC + b'\x74\x27', 'iso8859_2'], # ISO8859-2 Latin2 -# '8859_9': -# [ESC + b'\x74\x28', 'iso8859_9'], # ISO8859-2 Latin9 -# 'PC1098': -# [ESC + b'\x74\x29', 'cp1098'], # PC1098 Farsi -# 'PC1118': -# [ESC + b'\x74\x2a', 'cp1118'], # PC1118 Lithuanian -# 'PC1119': -# [ESC + b'\x74\x2b', 'cp1119'], # PC1119 Lithuanian -# 'PC1125': -# [ESC + b'\x74\x2c', 'cp1125'], # PC1125 Ukrainian -# 'WPC1250': -# [ESC + b'\x74\x2d', 'cp1250'], # WPC1250 Latin2 -# 'WPC1251': -# [ESC + b'\x74\x2e', 'cp1251'], # WPC1251 Cyrillic -# 'WPC1253': -# [ESC + b'\x74\x2f', 'cp1253'], # WPC1253 Greek -# 'WPC1254': -# [ESC + b'\x74\x30', 'cp1254'], # WPC1254 Turkish -# 'WPC1255': -# [ESC + b'\x74\x31', 'cp1255'], # WPC1255 Hebrew -# 'WPC1256': -# [ESC + b'\x74\x32', 'cp1256'], # WPC1256 Arabic -# 'WPC1257': -# [ESC + b'\x74\x33', 'cp1257'], # WPC1257 Baltic Rim -# 'WPC1258': -# [ESC + b'\x74\x34', 'cp1258'], # WPC1258 Vietnamese -# 'KZ1048': -# [ESC + b'\x74\x35', 'kz1048'], # KZ-1048 Kazakhstan -# } # Barcode format _SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n diff --git a/src/escpos/katakana.py b/src/escpos/katakana.py new file mode 100644 index 0000000..7c2e2c7 --- /dev/null +++ b/src/escpos/katakana.py @@ -0,0 +1,104 @@ +# -*- 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 jcconv +except ImportError: + jcconv = None + + +def encode_katakana(text): + """I don't think this quite works yet.""" + encoded = [] + for char in text: + if jcconv: + # try to convert japanese text to half-katakanas + char = jcconv.kata2half(jcconv.hira2kata(char)) + # TODO: "the conversion may result in multiple characters" + # When? What should we do about it? + + if char in TXT_ENC_KATAKANA_MAP: + encoded.append(TXT_ENC_KATAKANA_MAP[char]) + else: + pass + return b"".join(encoded) + + + +TXT_ENC_KATAKANA_MAP = { + # Maps UTF-8 Katakana symbols to KATAKANA Page Codes + + # 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', +} diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index ed8a4bc..8cfecf9 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -20,68 +20,16 @@ from __future__ import unicode_literals from .constants import CODEPAGE_CHANGE from .exceptions import CharCodeError, Error from .capabilities import get_profile +from .codepages import CodePages import copy import six -try: - import jcconv -except ImportError: - jcconv = None - - -def encode_katakana(text): - """I don't think this quite works yet.""" - encoded = [] - for char in text: - if jcconv: - # try to convert japanese text to half-katakanas - char = jcconv.kata2half(jcconv.hira2kata(char)) - # TODO: "the conversion may result in multiple characters" - # When? What should we do about it? - - if char in TXT_ENC_KATAKANA_MAP: - encoded.append(TXT_ENC_KATAKANA_MAP[char]) - else: - encoded.append(char) - print(encoded) - return b"".join(encoded) - - - -# TODO: When the capabilities.yml format is finished, this should be -# in the profile itself. -def get_encodings_from_profile(profile): - mapping = {k: v.lower() for k, v in profile.codePageMap.items()} - if hasattr(profile, 'codePages'): - code_pages = [n.lower() for n in profile.codePages] - return {k: v for k, v in mapping.items() if v in code_pages} - else: - return mapping - - -class CodePages: - def get_all(self): - return get_encodings_from_profile(get_profile()).values() - - def encode(self, text, encoding, errors='strict'): - # Python has not have this builtin? - if encoding.upper() == 'KATAKANA': - return encode_katakana(text) - - return text.encode(encoding, errors=errors) - - def get_encoding(self, encoding): - # resolve the encoding alias - return encoding.lower() - -code_pages = CodePages() - class Encoder(object): """Takes a list of available code spaces. Picks the right one for a given character. - Note: To determine the codespace, it needs to do the conversion, and + 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. @@ -94,36 +42,32 @@ class Encoder(object): 100000000 loops, best of 3: 0.0141 usec per loop """ - def __init__(self, codepages): - self.codepages = codepages - self.reverse = {v:k for k, v in codepages.items()} - self.available_encodings = set(codepages.values()) + def __init__(self, codepage_map): + self.codepages = codepage_map + self.available_encodings = set(codepage_map.keys()) self.used_encodings = set() def get_sequence(self, encoding): - return self.reverse[encoding] + return int(self.codepages[encoding]) def get_encoding(self, encoding): - """resolve aliases + """Given an encoding provided by the user, will return a + canonical encoding name; and also validate that the encoding + is supported. - check that the profile allows this encoding + TOOD: Support encoding aliases. """ - encoding = code_pages.get_encoding(encoding) - if not encoding in self.available_encodings: - raise ValueError('This encoding cannot be used for the current profile') + encoding = CodePages.get_encoding(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_encodings(self): - """ - - remove the ones not supported - - order by used first, then others - - do not use a cache, because encode already is so fast - """ - return self.available_encodings - def can_encode(self, encoding, char): try: - encoded = code_pages.encode(char, encoding) + encoded = CodePages.encode(char, encoding) assert type(encoded) is bytes return encoded except LookupError: @@ -134,7 +78,7 @@ class Encoder(object): return True - def find_suitable_codespace(self, char): + 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 @@ -150,9 +94,16 @@ class Encoder(object): that the code page we pick for this character is actually supported. - # XXX actually do speed up the search + # TODO actually do speed up the search """ - for encoding in self.get_encodings(): + """ + - remove the ones not supported + - order by used first, then others + - do not use a cache, because encode already is so fast + """ + sorted_encodings = self.codepages.keys() + + 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) @@ -160,14 +111,20 @@ class Encoder(object): class MagicEncode(object): - """ Magic Encode Class + """A helper that helps us to automatically switch to the right + code page to encode any given Unicode character. - It tries to automatically encode utf-8 input into the right coding. When encoding is impossible a configurable - symbol will be inserted. + 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. - 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. + 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): @@ -175,7 +132,7 @@ class MagicEncode(object): raise Error('If you disable magic encode, you need to define an encoding!') self.driver = driver - self.encoder = encoder or Encoder(get_encodings_from_profile(driver.profile)) + self.encoder = encoder or Encoder(driver.profile.get_code_pages()) self.encoding = self.encoder.get_encoding(encoding) if encoding else None self.defaultsymbol = defaultsymbol @@ -219,12 +176,12 @@ class MagicEncode(object): # We have to find another way to print this character. # See if any of the code pages that the printer profile supports # can encode this character. - codespace = self.encoder.find_suitable_codespace(char) - if not codespace: + encoding = self.encoder.find_suitable_encoding(char) + if not encoding: self._handle_character_failed(char) continue - self.write_with_encoding(codespace, char) + self.write_with_encoding(encoding, char) def _handle_character_failed(self, char): """Called when no codepage was found to render a character. @@ -239,8 +196,6 @@ class MagicEncode(object): type=type(text) )) - encoding = self.encoder.get_encoding(encoding) - # We always know the current code page; if the new codepage # is different, emit a change command. if encoding != self.encoding: @@ -251,78 +206,4 @@ class MagicEncode(object): )) if text: - self.driver._raw(code_pages.encode(text, encoding, errors="replace")) - - -# todo emoticons mit charmap encoden -# todo Escpos liste von unterdrückten charcodes mitgeben -# TODO Sichtbarkeit der Methode anpassen (Eigentlich braucht man nur die set_encode und die encode_text) - -TXT_ENC_KATAKANA_MAP = { - # Maps UTF-8 Katakana symbols to KATAKANA Page Codes - - # 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', -} + self.driver._raw(CodePages.encode(text, encoding, errors="replace")) diff --git a/test/test_function_text.py b/test/test_function_text.py index 5aac224..d4de426 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -12,23 +12,29 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals +import pytest import mock -from hypothesis import given +from hypothesis import given, assume import hypothesis.strategies as st from escpos.printer import Dummy +def get_printer(): + return Dummy(magic_encode_args={'disabled': True, 'encoding': 'cp437'}) + + @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 = Dummy() - instance.magic.encode_text = mock.Mock() +def test_text(text): + """Test that text() calls the MagicEncode object. + """ + instance = get_printer() + instance.magic.write = mock.Mock() instance.text(text) - instance.magic.encode_text.assert_called_with(txt=text) + instance.magic.write.assert_called_with(text) def test_block_text(): - printer = Dummy() + printer = get_printer() printer.block_text( "All the presidents men were eating falafel for breakfast.", font='a') assert printer.output == \ diff --git a/test/test_magicencode.py b/test/test_magicencode.py index 24fb0db..60a5a66 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -17,7 +17,8 @@ import pytest from nose.tools import raises, assert_raises from hypothesis import given, example import hypothesis.strategies as st -from escpos.magicencode import MagicEncode, Encoder, encode_katakana +from escpos.magicencode import MagicEncode, Encoder +from escpos.katakana import encode_katakana from escpos.exceptions import CharCodeError, Error @@ -25,13 +26,13 @@ from escpos.exceptions import CharCodeError, Error class TestEncoder: def test_can_encode(self): - assert not Encoder({1: 'cp437'}).can_encode('cp437', u'€') - assert Encoder({1: 'cp437'}).can_encode('cp437', u'á') - assert not Encoder({1: 'foobar'}).can_encode('foobar', 'a') + assert not Encoder({'cp437': 1}).can_encode('cp437', u'€') + assert Encoder({'cp437': 1}).can_encode('cp437', u'á') + assert not Encoder({'foobar': 1}).can_encode('foobar', 'a') def test_find_suitable_encoding(self): - assert not Encoder({1: 'cp437'}).find_suitable_codespace(u'€') - assert Encoder({1: 'cp858'}).find_suitable_codespace(u'€') == 'cp858' + assert not Encoder({'cp437': 1}).find_suitable_encoding(u'€') + assert Encoder({'cp858': 1}).find_suitable_encoding(u'€') == 'cp858' @raises(ValueError) def test_get_encoding(self): @@ -51,12 +52,12 @@ class TestMagicEncode: def test_init_from_none(self, driver): encode = MagicEncode(driver, encoding=None) encode.write_with_encoding('cp858', '€ ist teuro.') - assert driver.output == b'\x1bt\xd5 ist teuro.' + assert driver.output == b'\x1bt\x13\xd5 ist teuro.' def test_change_from_another(self, driver): encode = MagicEncode(driver, encoding='cp437') encode.write_with_encoding('cp858', '€ ist teuro.') - assert driver.output == b'\x1bt\xd5 ist teuro.' + assert driver.output == b'\x1bt\x13\xd5 ist teuro.' def test_no_change(self, driver): encode = MagicEncode(driver, encoding='cp858') @@ -68,7 +69,7 @@ class TestMagicEncode: def test_write(self, driver): encode = MagicEncode(driver) encode.write('€ ist teuro.') - assert driver.output == b'\x1bt\xa4 ist teuro.' + assert driver.output == b'\x1bt\x0f\xa4 ist teuro.' def test_write_disabled(self, driver): encode = MagicEncode(driver, encoding='cp437', disabled=True) @@ -77,7 +78,7 @@ class TestMagicEncode: def test_write_no_codepage(self, driver): encode = MagicEncode( - driver, defaultsymbol="_", encoder=Encoder({1: 'cp437'}), + driver, defaultsymbol="_", encoder=Encoder({'cp437': 1}), encoding='cp437') encode.write(u'€ ist teuro.') assert driver.output == b'_ ist teuro.' @@ -87,10 +88,10 @@ class TestMagicEncode: def test(self, driver): encode = MagicEncode(driver) encode.force_encoding('cp437') - assert driver.output == b'\x1bt' + assert driver.output == b'\x1bt\x00' encode.write('€ ist teuro.') - assert driver.output == b'\x1bt? ist teuro.' + assert driver.output == b'\x1bt\x00? ist teuro.' class TestKatakana: From 9aa1335fd28af92b8d15c84bf4da48b76d8b6f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 17:13:05 +0200 Subject: [PATCH 12/15] Improve codepage selection logic. --- src/escpos/magicencode.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 8cfecf9..3d62dab 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -55,7 +55,7 @@ class Encoder(object): canonical encoding name; and also validate that the encoding is supported. - TOOD: Support encoding aliases. + TODO: Support encoding aliases: pc437 instead of cp437. """ encoding = CodePages.get_encoding(encoding) if not encoding in self.codepages: @@ -78,6 +78,14 @@ class Encoder(object): return True + 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: @@ -93,17 +101,12 @@ class Encoder(object): 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) - # TODO actually do speed up the search - """ - """ - - remove the ones not supported - - order by used first, then others - - do not use a cache, because encode already is so fast - """ - sorted_encodings = self.codepages.keys() - - for encoding in sorted_encodings: + 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) From 73ef8c4c0a6d36a9c38263a80a152e1248c1ca49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 17:39:26 +0200 Subject: [PATCH 13/15] Write as many characters as possible at once. --- src/escpos/magicencode.py | 50 +++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 3d62dab..23fa9e1 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -85,7 +85,6 @@ class Encoder(object): index ) - def find_suitable_encoding(self, char): """The order of our search is a specific one: @@ -113,6 +112,21 @@ class Encoder(object): 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. @@ -161,30 +175,24 @@ class MagicEncode(object): self.write_with_encoding(self.encoding, text) return - # TODO: Currently this very simple loop means we send every - # character individually to the printer. We can probably - # improve performace by searching the text for the first - # character that cannot be rendered using the current code - # page, and then sending all of those characters at once. - # Or, should a lower-level buffer be responsible for that? + # 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) - for char in text: - # See if the current code page works for this character. - # The encoder object will use a cache to be able to answer - # this question fairly easily. - if self.encoding and self.encoder.can_encode(self.encoding, char): - self.write_with_encoding(self.encoding, char) - continue - - # We have to find another way to print this character. - # See if any of the code pages that the printer profile supports - # can encode this character. - encoding = self.encoder.find_suitable_encoding(char) + 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(char) + self._handle_character_failed(text[0]) + text = text[1:] continue - self.write_with_encoding(encoding, char) + # 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. From ddc93d7369d80413d840df6b2683f97b74549728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 18:06:34 +0200 Subject: [PATCH 14/15] Fix byte format() on Python 3. --- src/escpos/magicencode.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 23fa9e1..b36b844 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -211,10 +211,9 @@ class MagicEncode(object): # is different, emit a change command. if encoding != self.encoding: self.encoding = encoding - self.driver._raw(b'{}{}'.format( - CODEPAGE_CHANGE, - six.int2byte(self.encoder.get_sequence(encoding)) - )) + self.driver._raw( + CODEPAGE_CHANGE + + six.int2byte(self.encoder.get_sequence(encoding))) if text: self.driver._raw(CodePages.encode(text, encoding, errors="replace")) From a435b66006994f1826c745947b108e5ee3e2719a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 18:07:56 +0200 Subject: [PATCH 15/15] jcconf not available on Python 3. --- test/test_magicencode.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/test_magicencode.py b/test/test_magicencode.py index 60a5a66..d3d3121 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -94,6 +94,13 @@ class TestMagicEncode: assert driver.output == b'\x1bt\x00? ist teuro.' +try: + import jcconv +except ImportError: + jcconv = None + + +@pytest.mark.skipif(not jcconv, reason="jcconv not installed") class TestKatakana: @given(st.text()) @example("カタカナ")