From 3546e0c4bb5169aff242d73fe964d5dded6fd6f5 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sat, 23 Jul 2016 22:09:35 +0200 Subject: [PATCH 01/84] 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/84] 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/84] 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/84] 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/84] 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/84] =?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/84] 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 ed3077f00f68588c04fdef4c7081166566aea9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Thu, 25 Aug 2016 16:49:40 +0200 Subject: [PATCH 08/84] Define a capability format in YAML. --- src/escpos/capabilities.yml | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/escpos/capabilities.yml diff --git a/src/escpos/capabilities.yml b/src/escpos/capabilities.yml new file mode 100644 index 0000000..55f9212 --- /dev/null +++ b/src/escpos/capabilities.yml @@ -0,0 +1,68 @@ +# Many recent Epson-branded thermal receipt printers. +default: + columns: 42 + + barcodeB: true + bitImage: true + graphics: true + starCommands: false + qrCode: true + codePages: + - cp437 + # ... + # + +# Designed for non-Epson printers sold online. Without knowing +# their character encoding table, only CP437 output is assumed, +# and graphics() calls will be disabled, as it usually prints junk +# on these models. +simple: + codePages: + - cp437 + graphics: false + + +# Profile for Star-branded printers. +star: + inherits: default + starCommands: true + + +epson: + inherits: default + manufacturer: "Epson" + + +"P-822D": + inherits: default + graphics: false + + +"TM-T88IIIP": + inherits: epson + columns: + a: 42 + b: 56 + +"TM-P80": + inherits: epson + defaultColumnConfig: default + columnConfigs: + default: {'a': 48, 'b': 64, 'kanji': 24} + '42_emulation': {'a': 42, 'b': 60, 'kanji': 21} + +"TM-P60II 2": + inherits: epson + columnConfigs: + '58mm_paper': {'a': 35, 'b': 42, 'c': 52} + '60mm_paper': {'a': 36, 'b': 43, 'c': 54} + +"TM-P20 2": + inherits: epson + # Has 5 fonts! + +"TM-T90": + inherits: epson + colors: + - black + - red \ No newline at end of file From a8574ad9d7b7b933bb70fa47f84b3e396d058033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Thu, 25 Aug 2016 17:30:05 +0200 Subject: [PATCH 09/84] Support loading capabilites YAML into Python classes. --- src/escpos/capabilities.py | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/escpos/capabilities.py diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py new file mode 100644 index 0000000..2b08083 --- /dev/null +++ b/src/escpos/capabilities.py @@ -0,0 +1,83 @@ +import re +from os import path +import yaml + + +with open(path.join(path.dirname(__file__), 'capabilities.yml')) as f: + PROFILES = yaml.load(f) + + +class Profile(object): + + profile_data = {} + + def __init__(self, columns=None): + self.default_columns = columns + + def __getattr__(self, name): + return self.profile_data[name] + + def get_columns(self, font): + """ Return the number of columns for the given font. + """ + if self.default_columns: + return self.default_columns + + if 'columnConfigs' in self.profile_data: + columns_def = self.columnConfigs[self.defaultColumnConfig] + + elif 'columns' in self.profile_data: + columns_def = self.columns + + if isinstance(columns_def, int): + return columns_def + return columns_def[font] + + +def get_profile(name=None, **kwargs): + if isinstance(name, Profile): + return name + + clazz = get_profile_class(name or 'default') + return clazz(**kwargs) + + + +CLASS_CACHE = {} + + +def get_profile_class(name): + if not name in CLASS_CACHE: + profile_data = resolve_profile_data(name) + class_name = '%sProfile' % clean(name) + new_class = type(class_name, (Profile,), {'profile_data': profile_data}) + CLASS_CACHE[name] = new_class + + return CLASS_CACHE[name] + + +def clean(s): + # Remove invalid characters + s = re.sub('[^0-9a-zA-Z_]', '', s) + # Remove leading characters until we find a letter or underscore + s = re.sub('^[^a-zA-Z_]+', '', s) + return str(s) + + +def resolve_profile_data(name): + data = PROFILES[name] + inherits = data.get('inherits') + if not inherits: + return data + + if not isinstance(inherits, (tuple, list)): + inherits = [inherits] + + merged = {} + for base in reversed(inherits): + base_data = resolve_profile_data(base) + merged.update(base_data) + merged.update(data) + return merged + + From 8b5bc9cf8a595b4232afb96dc845ea9dbc0d7951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Thu, 25 Aug 2016 17:30:20 +0200 Subject: [PATCH 10/84] Make the Escpos class accept a profile. This is now used for the block_text function. --- src/escpos/escpos.py | 11 ++++++----- test/test_function_text.py | 9 +++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index c0df537..683688c 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -23,6 +23,7 @@ from .exceptions import * from abc import ABCMeta, abstractmethod # abstract base class support from escpos.image import EscposImage +from escpos.capabilities import get_profile @six.add_metaclass(ABCMeta) @@ -35,11 +36,11 @@ class Escpos(object): device = None codepage = None - def __init__(self, columns=32): + def __init__(self, profile=None): """ Initialize ESCPOS Printer - :param columns: Text columns used by the printer. Defaults to 32.""" - self.columns = columns + :param profile: Printer profile""" + self.profile = get_profile(profile) def __del__(self): """ call self.close upon deletion """ @@ -439,7 +440,7 @@ class Escpos(object): # TODO: why is it problematic to print an empty string? raise TextError() - def block_text(self, txt, columns=None): + def block_text(self, txt, font=None, columns=None): """ Text is printed wrapped to specified columns Text has to be encoded in unicode. @@ -448,7 +449,7 @@ class Escpos(object): :param columns: amount of columns :return: None """ - col_count = self.columns if columns is None else columns + col_count = self.profile.get_columns(font) if columns is None else columns self.text(textwrap.fill(txt, col_count)) def set(self, align='left', font='a', text_type='normal', width=1, height=1, density=9, invert=False, smooth=False, diff --git a/test/test_function_text.py b/test/test_function_text.py index b0b1ca1..973ddfc 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -15,6 +15,7 @@ from __future__ import unicode_literals from nose.tools import with_setup import escpos.printer as printer +from escpos.printer import Dummy import os import filecmp @@ -43,3 +44,11 @@ def test_function_text_dies_ist_ein_test_lf(): instance.text('Dies ist ein Test.\n') instance.flush() assert(filecmp.cmp('test/Dies ist ein Test.LF.txt', devfile)) + + +def test_block_text(): + printer = Dummy() + printer.block_text( + "All the presidents men were eating falafel for breakfast.", font='a') + assert printer.output == \ + 'All the presidents men were eating falafel\nfor breakfast.' 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 11/84] 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 3fd1a3de5d2824d8c60544ce4b967c5d8391179b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Fri, 26 Aug 2016 15:14:28 +0200 Subject: [PATCH 12/84] A suggested format for defining the code pages. --- src/escpos/capabilities.yml | 149 ++++++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 5 deletions(-) diff --git a/src/escpos/capabilities.yml b/src/escpos/capabilities.yml index 55f9212..e105687 100644 --- a/src/escpos/capabilities.yml +++ b/src/escpos/capabilities.yml @@ -1,3 +1,24 @@ +# Description of the format +abstract: + # Defines non-standard code pages that the printer supports, but + # that we won't find in Python's encoding system. If you define one + # here, don't forget to add it to codePageMap to assign it to a slot. + customCodePages: + sample: + + # This maps the indexed code page slots to code page names. + # Often, the slot assignment is the same, but the device only + # supports a subset. + codePageMap: + 0: "CP437" + 1: "CP932" + 3: "sample" + + # Maybe not all of the codepages in the map are supported. This + # is for subprofiles to select which ones the device knows. + codePages: [sample, cp932] + + # Many recent Epson-branded thermal receipt printers. default: columns: 42 @@ -7,10 +28,96 @@ default: graphics: true starCommands: false qrCode: true - codePages: - - cp437 - # ... - # + + customCodePages: + TCVN-3-1: [ + " ", + " ", + " ăâêôơưđ ", + " àảãáạ ằẳẵắ ", + " ặầẩẫấậè ẻẽ", + "éẹềểễếệìỉ ĩíịò", + " ỏõóọồổỗốộờởỡớợù", + " ủũúụừửữứựỳỷỹýỵ ", + ] + TCVN-3-2: [ + " ", + " ", + " ĂÂ Ð ÊÔƠƯ ", + " ÀẢÃÁẠ ẰẲẴẮ ", + " ẶẦẨẪẤẬÈ ẺẼ", + "ÉẸỀỂỄẾỆÌỈ ĨÍỊÒ", + " ỎÕÓỌỒỔỖỐỘỜỞỠỚỢÙ", + " ỦŨÚỤỪỬỮỨỰỲỶỸÝỴ " + ] + + + # Commented-out slots are TODO (might just need uncomment, might + # need verification/research) + codePageMap: + 0: "CP437" + 1: "CP932" + 2: "CP850" + 3: "CP860" + 4: "CP863" + 5: "CP865" + #6: // Hiragana + #7: // One-pass printing Kanji characters + #8: // Page 8 [One-pass printing Kanji characters] + 11: "CP851" + 12: "CP853" + 13: "CP857" + 14: "CP737" + 15: "ISO8859_7" + 16: "CP1252" + 17: "CP866" + 18: "CP852" + 19: "CP858" + #20: // Thai Character Code 42 + #21: // Thai Character Code 1" + #22: // Thai Character Code 13 + #23: // Thai Character Code 14 + #24: // Thai Character Code 16 + #25: // Thai Character Code 17 + #26: // Thai Character Code 18 + 30: 'TCVN-3-1', # TCVN-3: Vietnamese + 31: 'TCVN-3-2', # TCVN-3: Vietnamese + 32: "CP720" + 33: "CP775" + 34: "CP855" + 35: "CP861" + 36: "CP862" + 37: "CP864" + 38: "CP869" + 39: "ISO8859_2" + 40: "ISO8859_15" + 41: "CP1098" + 42: "CP774" + 43: "CP772" + 44: "CP1125" + 45: "CP1250" + 46: "CP1251" + 47: "CP1253" + 48: "CP1254" + 49: "CP1255" + 50: "CP1256" + 51: "CP1257" + 52: "CP1258" + 53: "RK1048" + #66: // Devanagari + #67: // Bengali + #68: // Tamil + #69: // Telugu + #70: // Assamese + #71: // Oriya + #72: // Kannada + #73: // Malayalam + #74: // Gujarati + #75: // Punjabi + #82: // Marathi + #254: + #255: + # Designed for non-Epson printers sold online. Without knowing # their character encoding table, only CP437 output is assumed, @@ -38,11 +145,41 @@ epson: graphics: false -"TM-T88IIIP": +# http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf +"TM-T88II": inherits: epson columns: a: 42 b: 56 + codePages: + - PC437 # 0 + - Katakana # 1 + - PC850 # 2 + - PC860 # 3 + - PC863 # 4 + - PC865 # 5 + - PC858 # 19 + - blank + +# http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf +"TM-T88III": + inherits: epson + columns: + a: 42 + b: 56 + codePages: + - PC437 # 0 + - Katakana # 1 + - PC850 # 2 + - PC860 # 3 + - PC863 # 4 + - PC865 # 5 + - WPC1252 # 16 + - PC866 # 17 + - PC852 # 18 + - PC858 # 19 + - blank + "TM-P80": inherits: epson @@ -51,12 +188,14 @@ epson: default: {'a': 48, 'b': 64, 'kanji': 24} '42_emulation': {'a': 42, 'b': 60, 'kanji': 21} + "TM-P60II 2": inherits: epson columnConfigs: '58mm_paper': {'a': 35, 'b': 42, 'c': 52} '60mm_paper': {'a': 36, 'b': 43, 'c': 54} + "TM-P20 2": inherits: epson # Has 5 fonts! 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 13/84] 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 216184f43f2080520df7c6ab541f80d382471842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Sat, 27 Aug 2016 12:17:35 +0200 Subject: [PATCH 14/84] Rework capabilities format based on Mike's ideas. --- src/escpos/capabilities.yml | 423 ++++++++++++++++++++---------------- 1 file changed, 234 insertions(+), 189 deletions(-) diff --git a/src/escpos/capabilities.yml b/src/escpos/capabilities.yml index e105687..561479d 100644 --- a/src/escpos/capabilities.yml +++ b/src/escpos/capabilities.yml @@ -1,207 +1,252 @@ -# Description of the format -abstract: - # Defines non-standard code pages that the printer supports, but - # that we won't find in Python's encoding system. If you define one - # here, don't forget to add it to codePageMap to assign it to a slot. - customCodePages: - sample: - - # This maps the indexed code page slots to code page names. - # Often, the slot assignment is the same, but the device only - # supports a subset. - codePageMap: - 0: "CP437" - 1: "CP932" - 3: "sample" - - # Maybe not all of the codepages in the map are supported. This - # is for subprofiles to select which ones the device knows. - codePages: [sample, cp932] +# Define code pages that implementors are likely not to find in iconv etc. +codepages: + blank: [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ] + TCVN-3-1: [ + " ", + " ", + " ăâêôơưđ ", + " àảãáạ ằẳẵắ ", + " ặầẩẫấậè ẻẽ", + "éẹềểễếệìỉ ĩíịò", + " ỏõóọồổỗốộờởỡớợù", + " ủũúụừửữứựỳỷỹýỵ ", + ] + TCVN-3-2: [ + " ", + " ", + " ĂÂ Ð ÊÔƠƯ ", + " ÀẢÃÁẠ ẰẲẴẮ ", + " ẶẦẨẪẤẬÈ ẺẼ", + "ÉẸỀỂỄẾỆÌỈ ĨÍỊÒ", + " ỎÕÓỌỒỔỖỐỘỜỞỠỚỢÙ", + " ỦŨÚỤỪỬỮỨỰỲỶỸÝỴ " + ] -# Many recent Epson-branded thermal receipt printers. -default: - columns: 42 - - barcodeB: true - bitImage: true - graphics: true - starCommands: false - qrCode: true - - customCodePages: - TCVN-3-1: [ - " ", - " ", - " ăâêôơưđ ", - " àảãáạ ằẳẵắ ", - " ặầẩẫấậè ẻẽ", - "éẹềểễếệìỉ ĩíịò", - " ỏõóọồổỗốộờởỡớợù", - " ủũúụừửữứựỳỷỹýỵ ", - ] - TCVN-3-2: [ - " ", - " ", - " ĂÂ Ð ÊÔƠƯ ", - " ÀẢÃÁẠ ẰẲẴẮ ", - " ẶẦẨẪẤẬÈ ẺẼ", - "ÉẸỀỂỄẾỆÌỈ ĨÍỊÒ", - " ỎÕÓỌỒỔỖỐỘỜỞỠỚỢÙ", - " ỦŨÚỤỪỬỮỨỰỲỶỸÝỴ " - ] +commands: + LineFeed: + name: Line feed + FeedAndCut: + name: Feed and cut + SetAbsolutePrintPos: + name: Set absolute print position + GraphicsData: + name: Graphics data - # Commented-out slots are TODO (might just need uncomment, might - # need verification/research) - codePageMap: - 0: "CP437" - 1: "CP932" - 2: "CP850" - 3: "CP860" - 4: "CP863" - 5: "CP865" - #6: // Hiragana - #7: // One-pass printing Kanji characters - #8: // Page 8 [One-pass printing Kanji characters] - 11: "CP851" - 12: "CP853" - 13: "CP857" - 14: "CP737" - 15: "ISO8859_7" - 16: "CP1252" - 17: "CP866" - 18: "CP852" - 19: "CP858" - #20: // Thai Character Code 42 - #21: // Thai Character Code 1" - #22: // Thai Character Code 13 - #23: // Thai Character Code 14 - #24: // Thai Character Code 16 - #25: // Thai Character Code 17 - #26: // Thai Character Code 18 - 30: 'TCVN-3-1', # TCVN-3: Vietnamese - 31: 'TCVN-3-2', # TCVN-3: Vietnamese - 32: "CP720" - 33: "CP775" - 34: "CP855" - 35: "CP861" - 36: "CP862" - 37: "CP864" - 38: "CP869" - 39: "ISO8859_2" - 40: "ISO8859_15" - 41: "CP1098" - 42: "CP774" - 43: "CP772" - 44: "CP1125" - 45: "CP1250" - 46: "CP1251" - 47: "CP1253" - 48: "CP1254" - 49: "CP1255" - 50: "CP1256" - 51: "CP1257" - 52: "CP1258" - 53: "RK1048" - #66: // Devanagari - #67: // Bengali - #68: // Tamil - #69: // Telugu - #70: // Assamese - #71: // Oriya - #72: // Kannada - #73: // Malayalam - #74: // Gujarati - #75: // Punjabi - #82: // Marathi - #254: - #255: +profiles: + default: + name: Default profile + description: Many recent Epson-branded thermal receipt printers + commands: + BarcodeB: true + BitImage: true + GraphicsData: true + QrCode: true + features: + starCommands: false + font: + 0: + columns: 40 + 1: + columns: 50 + colors: + - black + # Commented-out slots are TODO (might just need uncomment, might + # need verification/research) + codepages: + 0: CP437 + 1: CP932 + 2: CP850 + 3: CP860 + 4: CP863 + 5: CP865 + #6: // Hiragana + #7: // One-pass printing Kanji characters + #8: // Page 8 [One-pass printing Kanji characters] + 11: CP851 + 12: CP853 + 13: CP857 + 14: CP737 + 15: ISO8859_7 + 16: CP1252 + 17: CP866 + 18: CP852 + 19: CP858 + #20: // Thai Character Code 42 + #21: // Thai Character Code 1 + #22: // Thai Character Code 13 + #23: // Thai Character Code 14 + #24: // Thai Character Code 16 + #25: // Thai Character Code 17 + #26: // Thai Character Code 18 + 30: 'TCVN-3-1' # TCVN-3: Vietnamese + 31: 'TCVN-3-2' # TCVN-3: Vietnamese + 32: CP720 + 33: CP775 + 34: CP855 + 35: CP861 + 36: CP862 + 37: CP864 + 38: CP869 + 39: ISO8859_2 + 40: ISO8859_15 + 41: CP1098 + 42: CP774 + 43: CP772 + 44: CP1125 + 45: CP1250 + 46: CP1251 + 47: CP1253 + 48: CP1254 + 49: CP1255 + 50: CP1256 + 51: CP1257 + 52: CP1258 + 53: RK1048 + #66: // Devanagari + #67: // Bengali + #68: // Tamil + #69: // Telugu + #70: // Assamese + #71: // Oriya + #72: // Kannada + #73: // Malayalam + #74: // Gujarati + #75: // Punjabi + #82: // Marathi + #254: + #255: -# Designed for non-Epson printers sold online. Without knowing -# their character encoding table, only CP437 output is assumed, -# and graphics() calls will be disabled, as it usually prints junk -# on these models. -simple: - codePages: - - cp437 - graphics: false + # Designed for non-Epson printers sold online. Without knowing + # their character encoding table, only CP437 output is assumed, + # and graphics() calls will be disabled, as it usually prints junk + # on these models. + simple: + inherits: default + name: Simple profile + codePages: + 0: CP437 + commands: + graphicsData: false -# Profile for Star-branded printers. -star: - inherits: default - starCommands: true + star: + name: Star-branded printers + inherits: default + features: + starCommands: false -epson: - inherits: default - manufacturer: "Epson" +printers: + P-822D: + inherits: default + manufacturer: "Epson" + commands: + graphicsData: false + # http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf + TM-T88II: + inherits: default + manufacturer: "Epson" + fonts: + a: + columns: 42 + b: + columns: 56 + codePages: + 0: PC437 + 1: Katakana + 2: PC850 + 3: PC860 + 4: PC863 + 5: PC865 + 19: PC858 + 255: blank -"P-822D": - inherits: default - graphics: false + # http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf + TM-T88III: + inherits: default + manufacturer: "Epson" + fonts: + a: + columns: 42 + b: + columns: 56 + codePages: + - PC437 # 0 + - Katakana # 1 + - PC850 # 2 + - PC860 # 3 + - PC863 # 4 + - PC865 # 5 + - WPC1252 # 16 + - PC866 # 17 + - PC852 # 18 + - PC858 # 19 + - blank + TM-P80: + inherits: default + manufacturer: "Epson" + fonts: + a: + columns: 48 + b: + columns: 64 + kanji: + columns: 24 -# http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf -"TM-T88II": - inherits: epson - columns: - a: 42 - b: 56 - codePages: - - PC437 # 0 - - Katakana # 1 - - PC850 # 2 - - PC860 # 3 - - PC863 # 4 - - PC865 # 5 - - PC858 # 19 - - blank + TM-P80 (42 column emulation mode): + inherits: TM-P80 + fonts: + a: + columns: 42 + b: + columns: 60 + kanji: + columns: 21 -# http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf -"TM-T88III": - inherits: epson - columns: - a: 42 - b: 56 - codePages: - - PC437 # 0 - - Katakana # 1 - - PC850 # 2 - - PC860 # 3 - - PC863 # 4 - - PC865 # 5 - - WPC1252 # 16 - - PC866 # 17 - - PC852 # 18 - - PC858 # 19 - - blank + TM-P60II 2 (58mm): + inherits: default + manufacturer: "Epson" + media: + width: + mm: 58 + fonts: + a: {columns: 35} + b: {columns: 42} + c: {columns: 52} + TM-P60II 2 (60mm): + inherits: default + manufacturer: "Epson" + media: + width: + mm: 60 + fonts: + a: {columns: 36} + b: {columns: 43} + c: {columns: 54} -"TM-P80": - inherits: epson - defaultColumnConfig: default - columnConfigs: - default: {'a': 48, 'b': 64, 'kanji': 24} - '42_emulation': {'a': 42, 'b': 60, 'kanji': 21} + TM-P20 2: + inherits: default + manufacturer: "Epson" + fonts: [a, b c, d, e, f] - -"TM-P60II 2": - inherits: epson - columnConfigs: - '58mm_paper': {'a': 35, 'b': 42, 'c': 52} - '60mm_paper': {'a': 36, 'b': 43, 'c': 54} - - -"TM-P20 2": - inherits: epson - # Has 5 fonts! - -"TM-T90": - inherits: epson - colors: - - black - - red \ No newline at end of file + TM-T90: + inherits: default + manufacturer: "Epson" + colors: + - black + - red \ No newline at end of file From a07f84a5bc6a36e05e8a02cdd2204cc14f6cc722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 12:26:09 +0200 Subject: [PATCH 15/84] Match the current printer-db format. --- setup.py | 3 +- src/escpos/capabilities.json | 1 + src/escpos/capabilities.py | 87 +++++++----- src/escpos/capabilities.yml | 252 ----------------------------------- src/escpos/constants.py | 7 +- src/escpos/escpos.py | 14 +- 6 files changed, 67 insertions(+), 297 deletions(-) create mode 100644 src/escpos/capabilities.json delete mode 100644 src/escpos/capabilities.yml diff --git a/setup.py b/setup.py index 0e3eea5..6a95e63 100755 --- a/setup.py +++ b/setup.py @@ -83,7 +83,8 @@ setup( platforms='any', package_dir={"": "src"}, packages=find_packages(where="src", exclude=["tests", "tests.*"]), - package_data={'': ['COPYING']}, + package_data={'': ['COPYING', 'src/escpos/capabilities.json']}, + include_package_data=True, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', diff --git a/src/escpos/capabilities.json b/src/escpos/capabilities.json new file mode 100644 index 0000000..02ada55 --- /dev/null +++ b/src/escpos/capabilities.json @@ -0,0 +1 @@ +{"profiles": {"TM-T88III": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP862", "1": "Unknown", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "255": "Unknown"}, "name": "TM-T88III"}, "TM-P80": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": 72, "pixels": 576}}, "notes": "Portable printer (48-column mode)", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}, "2": {"name": "Kanji", "columns": 24}}, "colors": {"0": "black"}, "codePages": {"51": "CP1257", "48": "CP1254", "50": "CP1256", "82": "Unknown", "49": "CP1255", "66": "Unknown", "67": "Unknown", "68": "Unknown", "69": "Unknown", "52": "CP1258", "53": "RK1048", "254": "Unknown", "255": "Unknown", "24": "Unknown", "25": "Unknown", "26": "Unknown", "20": "Unknown", "21": "CP874", "22": "Unknown", "23": "Unknown", "46": "CP1251", "47": "CP1253", "44": "CP1125", "45": "CP1250", "42": "CP774", "43": "CP772", "40": "ISO_8859-15", "41": "CP1098", "1": "CP932", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "8": "Unknown", "39": "ISO_8859-2", "75": "Unknown", "38": "CP869", "73": "Unknown", "72": "Unknown", "71": "Unknown", "70": "Unknown", "11": "CP851", "13": "CP857", "12": "CP853", "15": "ISO_8859-7", "14": "CP737", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "TCVN-3-2", "30": "TCVN-3-1", "37": "CP864", "36": "CP862", "35": "CP861", "34": "CP855", "33": "CP775", "74": "Unknown", "32": "CP720"}, "name": "TM-P80"}, "P822D": {"vendor": "PBM", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": false, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"56": "CP861", "81": "CP3848", "54": "CP852", "60": "CP855", "61": "CP857", "62": "CP862", "63": "CP864", "64": "CP737", "53": "CP858", "66": "CP869", "67": "CP928", "68": "CP772", "69": "CP774", "80": "CP3847", "52": "CP437", "86": "CP3011", "87": "CP3012", "84": "CP3001", "85": "CP3002", "24": "CP747", "25": "CP1257", "27": "Unknown", "20": "Unknown", "21": "Unknown", "22": "Unknown", "23": "Unknown", "82": "CP1001", "28": "CP864", "29": "CP1001", "1": "Unknown", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "9": "Unknown", "8": "Unknown", "255": "Unknown", "83": "CP2001", "77": "CP3844", "76": "CP3843", "75": "CP3841", "74": "CP3840", "73": "CP1251", "72": "CP1250", "71": "CP1252", "70": "CP874", "79": "CP3846", "78": "CP3845", "10": "Unknown", "59": "CP866", "58": "CP865", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "Unknown", "30": "Unknown", "51": "Unknown", "50": "CP437", "35": "CP1257", "34": "CP1256", "33": "CP720", "55": "CP860", "89": "CP3041", "88": "CP3021", "32": "CP1255", "57": "CP863", "65": "CP851"}, "name": "P822D"}, "TEP-200M": {"vendor": "EPOS", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"51": "CP1257", "48": "CP1254", "50": "CP1256", "82": "Unknown", "49": "CP1255", "66": "Unknown", "67": "Unknown", "68": "Unknown", "69": "Unknown", "52": "CP1258", "53": "RK1048", "254": "Unknown", "255": "Unknown", "24": "Unknown", "25": "Unknown", "26": "Unknown", "20": "Unknown", "21": "CP874", "22": "Unknown", "23": "Unknown", "46": "CP1251", "47": "CP1253", "44": "CP1125", "45": "CP1250", "42": "CP774", "43": "CP772", "40": "ISO_8859-15", "41": "CP1098", "1": "CP932", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "8": "Unknown", "39": "ISO_8859-2", "75": "Unknown", "38": "CP869", "73": "Unknown", "72": "Unknown", "71": "Unknown", "70": "Unknown", "11": "CP851", "13": "CP857", "12": "CP853", "15": "ISO_8859-7", "14": "CP737", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "TCVN-3-2", "30": "TCVN-3-1", "37": "CP864", "36": "CP862", "35": "CP861", "34": "CP855", "33": "CP775", "74": "Unknown", "32": "CP720"}, "name": "TEP200M Series"}, "TM-T88II": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"16": "CP1252", "1": "Unknown", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "255": "Unknown"}, "name": "TM-T88II"}, "TM-P80-42col": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": 63.6, "pixels": 546}}, "notes": "Portable printer (42-column mode)", "fonts": {"1": {"name": "Font B", "columns": 60}, "0": {"name": "Font A", "columns": 42}, "2": {"name": "Kanji", "columns": 21}}, "colors": {"0": "black"}, "codePages": {"51": "CP1257", "48": "CP1254", "50": "CP1256", "82": "Unknown", "49": "CP1255", "66": "Unknown", "67": "Unknown", "68": "Unknown", "69": "Unknown", "52": "CP1258", "53": "RK1048", "254": "Unknown", "255": "Unknown", "24": "Unknown", "25": "Unknown", "26": "Unknown", "20": "Unknown", "21": "CP874", "22": "Unknown", "23": "Unknown", "46": "CP1251", "47": "CP1253", "44": "CP1125", "45": "CP1250", "42": "CP774", "43": "CP772", "40": "ISO_8859-15", "41": "CP1098", "1": "CP932", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "8": "Unknown", "39": "ISO_8859-2", "75": "Unknown", "38": "CP869", "73": "Unknown", "72": "Unknown", "71": "Unknown", "70": "Unknown", "11": "CP851", "13": "CP857", "12": "CP853", "15": "ISO_8859-7", "14": "CP737", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "TCVN-3-2", "30": "TCVN-3-1", "37": "CP864", "36": "CP862", "35": "CP861", "34": "CP855", "33": "CP775", "74": "Unknown", "32": "CP720"}, "name": "TM-P80 (42 column mode)"}, "TSP600": {"vendor": "Star Micronics", "features": {"starCommands": true, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "Star TSP600 thermal printer series with ESC/POS emulation enabled", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"98": "Unknown", "64": "CP3840", "65": "CP3841", "66": "CP3843", "67": "CP3844", "68": "CP3845", "69": "CP3846", "255": "Unknown", "20": "CP774", "21": "CP874", "1": "CP437", "0": "CP437", "3": "CP437", "2": "CP932", "5": "CP852", "4": "CP858", "7": "CP861", "6": "CP860", "9": "CP865", "8": "CP863", "96": "Unknown", "97": "Unknown", "77": "CP3012", "76": "CP3011", "75": "CP3002", "74": "CP3001", "73": "CP2001", "72": "CP1001", "71": "CP3848", "70": "CP3847", "102": "Unknown", "100": "Unknown", "101": "Unknown", "79": "CP3041", "78": "CP3021", "11": "CP855", "10": "CP866", "13": "CP862", "12": "CP857", "15": "CP737", "14": "CP864", "17": "CP869", "16": "CP851", "19": "CP772", "18": "CP928", "34": "CP1251", "99": "Unknown", "33": "CP1250", "32": "CP1252"}, "name": "TSP600 Series"}, "default": {"vendor": "Generic", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "Default ESC/POS profile, suitable for standards-compliant or Epson-branded printers. This profile allows the use of standard ESC/POS features, and can encode a variety of code pages.\n", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"51": "CP1257", "48": "CP1254", "50": "CP1256", "82": "Unknown", "49": "CP1255", "66": "Unknown", "67": "Unknown", "68": "Unknown", "69": "Unknown", "52": "CP1258", "53": "RK1048", "254": "Unknown", "255": "Unknown", "24": "Unknown", "25": "Unknown", "26": "Unknown", "20": "Unknown", "21": "CP874", "22": "Unknown", "23": "Unknown", "46": "CP1251", "47": "CP1253", "44": "CP1125", "45": "CP1250", "42": "CP774", "43": "CP772", "40": "ISO_8859-15", "41": "CP1098", "1": "CP932", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "8": "Unknown", "39": "ISO_8859-2", "75": "Unknown", "38": "CP869", "73": "Unknown", "72": "Unknown", "71": "Unknown", "70": "Unknown", "11": "CP851", "13": "CP857", "12": "CP853", "15": "ISO_8859-7", "14": "CP737", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "TCVN-3-2", "30": "TCVN-3-1", "37": "CP864", "36": "CP862", "35": "CP861", "34": "CP855", "33": "CP775", "74": "Unknown", "32": "CP720"}, "name": "Default"}, "TUP500": {"vendor": "Star Micronics", "features": {"starCommands": true, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "Star TUP500 thermal printer series with ESC/POS emulation enabled", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"98": "Unknown", "64": "CP3840", "65": "CP3841", "66": "CP3843", "67": "CP3844", "68": "CP3845", "69": "CP3846", "255": "Unknown", "20": "CP774", "21": "CP874", "1": "CP437", "0": "CP437", "3": "CP437", "2": "CP932", "5": "CP852", "4": "CP858", "7": "CP861", "6": "CP860", "9": "CP865", "8": "CP863", "96": "Unknown", "97": "Unknown", "77": "CP3012", "76": "CP3011", "75": "CP3002", "74": "CP3001", "73": "CP2001", "72": "CP1001", "71": "CP3848", "70": "CP3847", "102": "Unknown", "100": "Unknown", "101": "Unknown", "79": "CP3041", "78": "CP3021", "11": "CP855", "10": "CP866", "13": "CP862", "12": "CP857", "15": "CP737", "14": "CP864", "17": "CP869", "16": "CP851", "19": "CP772", "18": "CP928", "34": "CP1251", "99": "Unknown", "33": "CP1250", "32": "CP1252"}, "name": "TUP500 Series"}, "TM-U220": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": false, "barcodeB": false, "bitImageColumn": true, "graphics": false, "qrCode": false, "bitImageRaster": false}, "media": {"width": {"mm": 80, "pixels": "Unknown"}}, "notes": "Two-color impact printer with 80mm output", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"1": "alternate", "0": "black"}, "codePages": {"0": "CP437"}, "name": "TM-U220"}, "simple": {"vendor": "Generic", "features": {"starCommands": false, "highDensity": true, "barcodeB": false, "bitImageColumn": false, "graphics": false, "qrCode": false, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "A profile for use in printers with unknown or poor compatibility. This profile indicates that a small number of features are supported, so that commands are not sent a printer that is unlikely to understand them.\n", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"0": "CP437"}, "name": "Simple"}, "SP2000": {"vendor": "Star Micronics", "features": {"starCommands": true, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "Star SP2000 impact printer series with ESC/POS emulation enabled", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"98": "Unknown", "64": "CP3840", "65": "CP3841", "66": "CP3843", "67": "CP3844", "68": "CP3845", "69": "CP3846", "255": "Unknown", "20": "CP774", "21": "CP874", "1": "CP437", "0": "CP437", "3": "CP437", "2": "CP932", "5": "CP852", "4": "CP858", "7": "CP861", "6": "CP860", "9": "CP865", "8": "CP863", "96": "Unknown", "97": "Unknown", "77": "CP3012", "76": "CP3011", "75": "CP3002", "74": "CP3001", "73": "CP2001", "72": "CP1001", "71": "CP3848", "70": "CP3847", "102": "Unknown", "100": "Unknown", "101": "Unknown", "79": "CP3041", "78": "CP3021", "11": "CP855", "10": "CP866", "13": "CP862", "12": "CP857", "15": "CP737", "14": "CP864", "17": "CP869", "16": "CP851", "19": "CP772", "18": "CP928", "34": "CP1251", "99": "Unknown", "33": "CP1250", "32": "CP1252"}, "name": "SP2000 Series"}}, "encodings": {"CP3041": {"name": "Unimplemented Star-specific CP3041"}, "RK1048": {"iconv": "RK1048", "name": "RK1048"}, "Unknown": {"notes": "Code page that has not yet been identified.", "name": "Unknown"}, "CP1252": {"iconv": "CP1252", "python_encode": "cp1252", "name": "CP1252"}, "CP1255": {"iconv": "CP1255", "python_encode": "cp1255", "name": "CP1255"}, "CP1254": {"iconv": "CP1254", "python_encode": "cp1254", "name": "CP1254"}, "CP1257": {"iconv": "CP1257", "python_encode": "cp1257", "name": "CP1257"}, "CP1256": {"iconv": "CP1256", "python_encode": "cp1256", "name": "CP1256"}, "CP1251": {"iconv": "CP1251", "python_encode": "cp1251", "name": "CP1251"}, "CP1250": {"iconv": "CP1250", "python_encode": "cp1250", "name": "CP1250"}, "CP1253": {"iconv": "CP1253", "python_encode": "cp1253", "name": "CP1253"}, "CP1098": {"name": "CP1098"}, "CP437": {"iconv": "CP437", "python_encode": "cp437", "name": "CP437"}, "CP3021": {"name": "Unimplemented Star-specific CP3021"}, "CP3002": {"name": "Unimplemented Star-specific CP3002"}, "CP1258": {"iconv": "CP1258", "python_encode": "cp1258", "name": "CP1258"}, "CP3001": {"name": "Unimplemented Star-specific CP3001"}, "CP928": {"name": "CP928"}, "TCVN-3-1": {"data": [" ", " ", " \u0103\u00e2\u00ea\u00f4\u01a1\u01b0\u0111 ", " \u00e0\u1ea3\u00e3\u00e1\u1ea1 \u1eb1\u1eb3\u1eb5\u1eaf ", " \u1eb7\u1ea7\u1ea9\u1eab\u1ea5\u1ead\u00e8 \u1ebb\u1ebd", "\u00e9\u1eb9\u1ec1\u1ec3\u1ec5\u1ebf\u1ec7\u00ec\u1ec9 \u0129\u00ed\u1ecb\u00f2", " \u1ecf\u00f5\u00f3\u1ecd\u1ed3\u1ed5\u1ed7\u1ed1\u1ed9\u1edd\u1edf\u1ee1\u1edb\u1ee3\u00f9", " \u1ee7\u0169\u00fa\u1ee5\u1eeb\u1eed\u1eef\u1ee9\u1ef1\u1ef3\u1ef7\u1ef9\u00fd\u1ef5 "], "name": "Vietnamese TCVN-3 1"}, "TCVN-3-2": {"data": [" ", " ", " \u0102\u00c2 \u00d0 \u00ca\u00d4\u01a0\u01af ", " \u00c0\u1ea2\u00c3\u00c1\u1ea0 \u1eb0\u1eb2\u1eb4\u1eae ", " \u1eb6\u1ea6\u1ea8\u1eaa\u1ea4\u1eac\u00c8 \u1eba\u1ebc", "\u00c9\u1eb8\u1ec0\u1ec2\u1ec4\u1ebe\u1ec6\u00cc\u1ec8 \u0128\u00cd\u1eca\u00d2", " \u1ece\u00d5\u00d3\u1ecc\u1ed2\u1ed4\u1ed6\u1ed0\u1ed8\u1edc\u1ede\u1ee0\u1eda\u1ee2\u00d9", " \u1ee6\u0168\u00da\u1ee4\u1eea\u1eec\u1eee\u1ee8\u1ef0\u1ef2\u1ef6\u1ef8\u00dd\u1ef4 "], "name": "Vietnamese TCVN-3 1"}, "CP737": {"iconv": "CP737", "python_encode": "cp737", "name": "CP737"}, "CP747": {"name": "CP747"}, "CP851": {"notes": "Not used, due to inconsistencies between implementations.", "name": "Greek CP851"}, "CP850": {"iconv": "CP850", "python_encode": "cp850", "name": "CP850"}, "CP853": {"name": "CP853"}, "CP852": {"iconv": "CP852", "python_encode": "cp852", "name": "CP852"}, "CP855": {"iconv": "CP855", "python_encode": "cp855", "name": "CP855"}, "CP857": {"iconv": "CP857", "python_encode": "cp857", "name": "CP857"}, "CP861": {"iconv": "CP861", "python_encode": "cp861", "name": "CP861"}, "CP774": {"iconv": "CP774", "name": "CP774"}, "CP775": {"iconv": "CP775", "python_encode": "cp775", "name": "CP775"}, "CP874": {"iconv": "CP874", "python_encode": "cp874", "name": "CP874"}, "CP866": {"iconv": "CP866", "python_encode": "cp866", "name": "CP866"}, "CP1125": {"iconv": "CP1125", "python_encode": "cp1125", "name": "CP1125"}, "CP1001": {"name": "Unimplemented Star-specific CP1001"}, "CP858": {"python_encode": "cp858", "name": "CP858"}, "CP932": {"iconv": "CP932", "python_encode": "cp932", "name": "CP932"}, "CP3011": {"data": ["\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5", "\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00a2\u00a3\u00a5\u20a7\u0192", "\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u2310\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb", "\u2591\u2592\u2593\u2502\u2524\u0100\u2562\u0146\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510", "\u2514\u2534\u252c\u251c\u2500\u253c\u0101\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567", "\u0160\u2564\u010d\u010c\u2558\u2552\u0123\u012a\u012b\u2518\u250c\u2588\u2584\u016b\u016a\u2580", "\u03b1\u00df\u0393\u03c0\u03a3\u03c3\u00b5\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u03c6\u03b5\u2229", "\u0112\u0113\u0122\u0137\u0136\u013c\u013b\u017e\u017d\u2219\u00b7\u221a\u0145\u0161\u25a0 "], "name": "CP3011 Latvian"}, "CP862": {"iconv": "CP862", "python_encode": "cp862", "name": "CP862"}, "CP3012": {"data": ["\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0417\u0418\u0419\u041a\u041b\u041c\u041d\u041e\u041f", "\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\u042a\u042b\u042c\u042d\u042e\u042f", "\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0437\u0438\u0439\u043a\u043b\u043c\u043d\u043e\u043f", "\u2591\u2592\u2593\u2502\u2524\u0100\u2562\u0146\u2555\u2563\u2551\u2557\u255d\u014c\u255b\u2510", "\u2514\u2534\u252c\u251c\u2500\u253c\u0101\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567", "\u0160\u2564\u010d\u010c\u2558\u2552\u0123\u012a\u012b\u2518\u250c\u2588\u2584\u016b\u016a\u2580", "\u0440\u0441\u0442\u0443\u0444\u0445\u0446\u0447\u0448\u0449\u044a\u044b\u044c\u044d\u044e\u044f", "\u0112\u0113\u0122\u0137\u0136\u013c\u013b\u017e\u017d\u2219\u00b7\u221a\u0145\u0161\u25a0 "], "name": "CP3012 Cyrillic"}, "ISO_8859-7": {"iconv": "ISO_8859-7", "python_encode": "iso8859_7", "name": "ISO_8859-7"}, "CP863": {"iconv": "CP863", "python_encode": "cp863", "name": "CP863"}, "CP720": {"python_encode": "cp720", "name": "CP720"}, "CP2001": {"name": "Unimplemented Star-specific CP2001"}, "CP864": {"iconv": "CP864", "python_encode": "cp864", "name": "CP864"}, "ISO_8859-2": {"iconv": "ISO_8859-2", "python_encode": "iso8859_2", "name": "ISO_8859-2"}, "CP865": {"iconv": "CP865", "python_encode": "cp865", "name": "CP865"}, "CP869": {"iconv": "CP869", "python_encode": "cp869", "name": "CP869"}, "CP3848": {"name": "Unimplemented Star-specific CP3848"}, "ISO_8859-15": {"iconv": "ISO_8859-15", "python_encode": "iso8859-15", "name": "ISO_8859-15"}, "CP772": {"iconv": "CP772", "name": "CP772"}, "CP860": {"iconv": "CP860", "python_encode": "cp860", "name": "CP860"}, "CP3843": {"name": "Unimplemented Star-specific CP3843"}, "CP3840": {"name": "Unimplemented Star-specific CP3840"}, "CP3841": {"name": "Unimplemented Star-specific CP3841"}, "CP3846": {"name": "Unimplemented Star-specific CP3846"}, "CP3847": {"name": "Unimplemented Star-specific CP3847"}, "CP3844": {"name": "Unimplemented Star-specific CP3844"}, "CP3845": {"name": "Unimplemented Star-specific CP3845"}}} diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index 2b08083..816c6e9 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -1,40 +1,53 @@ import re +import six from os import path import yaml -with open(path.join(path.dirname(__file__), 'capabilities.yml')) as f: - PROFILES = yaml.load(f) +# Load external printer database +with open(path.join(path.dirname(__file__), 'capabilities.json')) as f: + CAPABILITIES = yaml.load(f) +PROFILES = CAPABILITIES['profiles'] +ENCODINGS = CAPABILITIES['encodings'] -class Profile(object): +class NotSupported(Exception): + pass + + +class BaseProfile(object): + """This respresents a printer profile. + + A printer profile knows about the number of columns, supported + features, colors and more. + """ profile_data = {} - def __init__(self, columns=None): - self.default_columns = columns - def __getattr__(self, name): return self.profile_data[name] + def get_font(self, font): + """Return the escpos index for `font`. Makes sure that + the requested `font` is valid. + """ + font = {'a': 0, 'b': 1}.get(font, font) + if not six.text_type(font) in self.fonts: + raise NotSupported( + '"%s" is not a valid font in the current profile' % font) + return font + def get_columns(self, font): """ Return the number of columns for the given font. """ - if self.default_columns: - return self.default_columns - - if 'columnConfigs' in self.profile_data: - columns_def = self.columnConfigs[self.defaultColumnConfig] - - elif 'columns' in self.profile_data: - columns_def = self.columns - - if isinstance(columns_def, int): - return columns_def - return columns_def[font] + font = self.get_font(font) + return self.fonts[six.text_type(font)]['columns'] def get_profile(name=None, **kwargs): + """Get the profile by name; if no name is given, return the + default profile. + """ if isinstance(name, Profile): return name @@ -42,15 +55,19 @@ def get_profile(name=None, **kwargs): return clazz(**kwargs) - CLASS_CACHE = {} def get_profile_class(name): + """For the given profile name, load the data from the external + database, then generate dynamically a class. + """ if not name in CLASS_CACHE: - profile_data = resolve_profile_data(name) - class_name = '%sProfile' % clean(name) - new_class = type(class_name, (Profile,), {'profile_data': profile_data}) + profile_data = PROFILES[name] + profile_name = clean(name) + class_name = '{}{}Profile'.format( + profile_name[0].upper(), profile_name[1:]) + new_class = type(class_name, (BaseProfile,), {'profile_data': profile_data}) CLASS_CACHE[name] = new_class return CLASS_CACHE[name] @@ -64,20 +81,20 @@ def clean(s): return str(s) -def resolve_profile_data(name): - data = PROFILES[name] - inherits = data.get('inherits') - if not inherits: - return data +# For users, who want to provide their profile +class Profile(get_profile_class('default')): + + def __init__(self, columns=None): + super(Profile, self).__init() + + self.columns = columns + + def get_columns(self, font): + if self.columns is not None: + return columns + + return super(Profile, self).get_columns(font) - if not isinstance(inherits, (tuple, list)): - inherits = [inherits] - merged = {} - for base in reversed(inherits): - base_data = resolve_profile_data(base) - merged.update(base_data) - merged.update(data) - return merged diff --git a/src/escpos/capabilities.yml b/src/escpos/capabilities.yml deleted file mode 100644 index 561479d..0000000 --- a/src/escpos/capabilities.yml +++ /dev/null @@ -1,252 +0,0 @@ -# Define code pages that implementors are likely not to find in iconv etc. -codepages: - blank: [ - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - ] - TCVN-3-1: [ - " ", - " ", - " ăâêôơưđ ", - " àảãáạ ằẳẵắ ", - " ặầẩẫấậè ẻẽ", - "éẹềểễếệìỉ ĩíịò", - " ỏõóọồổỗốộờởỡớợù", - " ủũúụừửữứựỳỷỹýỵ ", - ] - TCVN-3-2: [ - " ", - " ", - " ĂÂ Ð ÊÔƠƯ ", - " ÀẢÃÁẠ ẰẲẴẮ ", - " ẶẦẨẪẤẬÈ ẺẼ", - "ÉẸỀỂỄẾỆÌỈ ĨÍỊÒ", - " ỎÕÓỌỒỔỖỐỘỜỞỠỚỢÙ", - " ỦŨÚỤỪỬỮỨỰỲỶỸÝỴ " - ] - - -commands: - LineFeed: - name: Line feed - FeedAndCut: - name: Feed and cut - SetAbsolutePrintPos: - name: Set absolute print position - GraphicsData: - name: Graphics data - - -profiles: - default: - name: Default profile - description: Many recent Epson-branded thermal receipt printers - commands: - BarcodeB: true - BitImage: true - GraphicsData: true - QrCode: true - features: - starCommands: false - font: - 0: - columns: 40 - 1: - columns: 50 - colors: - - black - # Commented-out slots are TODO (might just need uncomment, might - # need verification/research) - codepages: - 0: CP437 - 1: CP932 - 2: CP850 - 3: CP860 - 4: CP863 - 5: CP865 - #6: // Hiragana - #7: // One-pass printing Kanji characters - #8: // Page 8 [One-pass printing Kanji characters] - 11: CP851 - 12: CP853 - 13: CP857 - 14: CP737 - 15: ISO8859_7 - 16: CP1252 - 17: CP866 - 18: CP852 - 19: CP858 - #20: // Thai Character Code 42 - #21: // Thai Character Code 1 - #22: // Thai Character Code 13 - #23: // Thai Character Code 14 - #24: // Thai Character Code 16 - #25: // Thai Character Code 17 - #26: // Thai Character Code 18 - 30: 'TCVN-3-1' # TCVN-3: Vietnamese - 31: 'TCVN-3-2' # TCVN-3: Vietnamese - 32: CP720 - 33: CP775 - 34: CP855 - 35: CP861 - 36: CP862 - 37: CP864 - 38: CP869 - 39: ISO8859_2 - 40: ISO8859_15 - 41: CP1098 - 42: CP774 - 43: CP772 - 44: CP1125 - 45: CP1250 - 46: CP1251 - 47: CP1253 - 48: CP1254 - 49: CP1255 - 50: CP1256 - 51: CP1257 - 52: CP1258 - 53: RK1048 - #66: // Devanagari - #67: // Bengali - #68: // Tamil - #69: // Telugu - #70: // Assamese - #71: // Oriya - #72: // Kannada - #73: // Malayalam - #74: // Gujarati - #75: // Punjabi - #82: // Marathi - #254: - #255: - - - # Designed for non-Epson printers sold online. Without knowing - # their character encoding table, only CP437 output is assumed, - # and graphics() calls will be disabled, as it usually prints junk - # on these models. - simple: - inherits: default - name: Simple profile - codePages: - 0: CP437 - commands: - graphicsData: false - - - star: - name: Star-branded printers - inherits: default - features: - starCommands: false - - -printers: - P-822D: - inherits: default - manufacturer: "Epson" - commands: - graphicsData: false - - # http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf - TM-T88II: - inherits: default - manufacturer: "Epson" - fonts: - a: - columns: 42 - b: - columns: 56 - codePages: - 0: PC437 - 1: Katakana - 2: PC850 - 3: PC860 - 4: PC863 - 5: PC865 - 19: PC858 - 255: blank - - # http://support.epostraders.co.uk/support-files/documents/3/l7O-TM-T88II_TechnicalRefGuide.pdf - TM-T88III: - inherits: default - manufacturer: "Epson" - fonts: - a: - columns: 42 - b: - columns: 56 - codePages: - - PC437 # 0 - - Katakana # 1 - - PC850 # 2 - - PC860 # 3 - - PC863 # 4 - - PC865 # 5 - - WPC1252 # 16 - - PC866 # 17 - - PC852 # 18 - - PC858 # 19 - - blank - - TM-P80: - inherits: default - manufacturer: "Epson" - fonts: - a: - columns: 48 - b: - columns: 64 - kanji: - columns: 24 - - TM-P80 (42 column emulation mode): - inherits: TM-P80 - fonts: - a: - columns: 42 - b: - columns: 60 - kanji: - columns: 21 - - TM-P60II 2 (58mm): - inherits: default - manufacturer: "Epson" - media: - width: - mm: 58 - fonts: - a: {columns: 35} - b: {columns: 42} - c: {columns: 52} - - TM-P60II 2 (60mm): - inherits: default - manufacturer: "Epson" - media: - width: - mm: 60 - fonts: - a: {columns: 36} - b: {columns: 43} - c: {columns: 54} - - TM-P20 2: - inherits: default - manufacturer: "Epson" - fonts: [a, b c, d, e, f] - - TM-T90: - inherits: default - manufacturer: "Epson" - colors: - - black - - red \ No newline at end of file diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 93721cb..6087c80 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -93,14 +93,17 @@ TXT_UNDERL_ON = ESC + b'\x2d\x01' # Underline font 1-dot ON TXT_UNDERL2_ON = ESC + b'\x2d\x02' # Underline font 2-dot ON TXT_BOLD_OFF = ESC + b'\x45\x00' # Bold font OFF TXT_BOLD_ON = ESC + b'\x45\x01' # Bold font ON -TXT_FONT_A = ESC + b'\x4d\x00' # Font type A -TXT_FONT_B = ESC + b'\x4d\x01' # Font type B TXT_ALIGN_LT = ESC + b'\x61\x00' # Left justification TXT_ALIGN_CT = ESC + b'\x61\x01' # Centering TXT_ALIGN_RT = ESC + b'\x61\x02' # Right justification TXT_INVERT_ON = GS + b'\x42\x01' # Inverse Printing ON TXT_INVERT_OFF = GS + b'\x42\x00' # Inverse Printing OFF +# Fonts +SET_FONT = lambda n: ESC + b'\x4d' + n +TXT_FONT_A = SET_FONT(b'\x00') # Font type A +TXT_FONT_B = SET_FONT(b'\x01') # Font type B + # Char code table CHARCODE_PC437 = ESC + b'\x74\x00' # USA: Standard Europe CHARCODE_JIS = ESC + b'\x74\x01' # Japanese Katakana diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 683688c..70e5ca1 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -452,8 +452,8 @@ class Escpos(object): col_count = self.profile.get_columns(font) if columns is None else columns self.text(textwrap.fill(txt, col_count)) - def set(self, align='left', font='a', text_type='normal', width=1, height=1, density=9, invert=False, smooth=False, - flip=False): + def set(self, align='left', font='a', text_type='normal', width=1, + height=1, density=9, invert=False, smooth=False, flip=False): """ Set text properties by sending them to the printer :param align: horizontal position for text, possible values are: @@ -463,7 +463,9 @@ class Escpos(object): * RIGHT *default*: LEFT - :param font: font type, possible values are A or B, *default*: A + + :param font: font given as an index, a name, or one of the + special values 'a' or 'b', refering to fonts 0 and 1. :param text_type: text type, possible values are: * B for bold @@ -528,10 +530,8 @@ class Escpos(object): self._raw(TXT_BOLD_OFF) self._raw(TXT_UNDERL_OFF) # Font - if font.upper() == "B": - self._raw(TXT_FONT_B) - else: # DEFAULT FONT: A - self._raw(TXT_FONT_A) + self._raw(SET_FONT(six.int2byte(self.profile.get_font(font)))) + # Align if align.upper() == "CENTER": self._raw(TXT_ALIGN_CT) From 5fa89ff685aef81bd12fb0370d6ff1bda814a05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 12:53:31 +0200 Subject: [PATCH 16/84] Automatically choose correct barcode function. Tests for barcode function. --- src/escpos/capabilities.py | 13 +++++++++-- src/escpos/escpos.py | 43 +++++++++++++++++++++++++---------- test/test_function_barcode.py | 34 +++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 test/test_function_barcode.py diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index 816c6e9..6e375bc 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -15,6 +15,9 @@ class NotSupported(Exception): pass +BARCODE_B = 'barcodeB' + + class BaseProfile(object): """This respresents a printer profile. @@ -43,6 +46,11 @@ class BaseProfile(object): font = self.get_font(font) return self.fonts[six.text_type(font)]['columns'] + def supports(self, feature): + """Return true/false for the given feature. + """ + return self.features.get(feature) + def get_profile(name=None, **kwargs): """Get the profile by name; if no name is given, return the @@ -84,10 +92,11 @@ def clean(s): # For users, who want to provide their profile class Profile(get_profile_class('default')): - def __init__(self, columns=None): - super(Profile, self).__init() + def __init__(self, columns=None, features={}): + super(Profile, self).__init__() self.columns = columns + self.features = features def get_columns(self, font): if self.columns is not None: diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 70e5ca1..50e59f8 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -23,7 +23,7 @@ from .exceptions import * from abc import ABCMeta, abstractmethod # abstract base class support from escpos.image import EscposImage -from escpos.capabilities import get_profile +from escpos.capabilities import get_profile, BARCODE_B @six.add_metaclass(ABCMeta) @@ -293,7 +293,8 @@ class Escpos(object): else: raise CharCodeError() - def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True, function_type="A"): + def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", + align_ct=True, function_type=None): """ Print Barcode This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to @@ -364,14 +365,40 @@ class Escpos(object): issued. :type align_ct: bool - :param function_type: Choose between ESCPOS function type A or B, depending on printer support and desired - barcode. + :param function_type: Choose between ESCPOS function type A or B, + depending on printer support and desired barcode. If not given, + the printer will attempt to automatically choose the correct + function based on the current profile. *default*: A :raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`, :py:exc:`~escpos.exceptions.BarcodeTypeError`, :py:exc:`~escpos.exceptions.BarcodeCodeError` """ + if function_type is None: + # Choose the function type automatically. + if bc in BARCODE_TYPES['A']: + function_type = 'A' + else: + if bc in BARCODE_TYPES['B']: + if not self.profile.supports(BARCODE_B): + raise BarcodeTypeError(( + "Barcode type '{bc} not supported for " + "the current printer profile").format(bc=bc)) + function_type = 'B' + else: + raise BarcodeTypeError(( + "Barcode type '{bc} is not valid").format(bc=bc)) + + bc_types = BARCODE_TYPES[function_type.upper()] + if bc.upper() not in bc_types.keys(): + raise BarcodeTypeError(( + "Barcode type '{bc}' not valid for barcode function type " + "{function_type}").format( + bc=bc, + function_type=function_type, + )) + # Align Bar Code() if align_ct: self._raw(TXT_ALIGN_CT) @@ -400,14 +427,6 @@ class Escpos(object): else: # DEFAULT POSITION: BELOW self._raw(BARCODE_TXT_BLW) - bc_types = BARCODE_TYPES[function_type.upper()] - if bc.upper() not in bc_types.keys(): - # TODO: Raise a better error, or fix the message of this error type - raise BarcodeTypeError("Barcode type {bc} not valid for barcode function type {function_type}".format( - bc=bc, - function_type=function_type, - )) - self._raw(bc_types[bc.upper()]) if function_type.upper() == "B": diff --git a/test/test_function_barcode.py b/test/test_function_barcode.py new file mode 100644 index 0000000..4205eba --- /dev/null +++ b/test/test_function_barcode.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import escpos.printer as printer +from escpos.constants import BARCODE_TYPE_A, BARCODE_TYPE_B +from escpos.capabilities import Profile, BARCODE_B +from escpos.exceptions import BarcodeTypeError +import pytest + + +@pytest.mark.parametrize("bctype,data,expected", [ + ('EAN13', '4006381333931', + b'\x1ba\x01\x1dh@\x1dw\x03\x1df\x00\x1dH\x02\x1dk\x024006381333931\x00') +]) +def test_barcode(bctype, data, expected): + instance = printer.Dummy() + instance.barcode(data, bctype) + assert instance.output == expected + + +@pytest.mark.parametrize("bctype,supports_b", [ + ('invalid', True), + ('CODE128', False), +]) +def test_lacks_support(bctype, supports_b): + profile = Profile(features={BARCODE_B: supports_b}) + instance = printer.Dummy(profile=profile) + with pytest.raises(BarcodeTypeError): + instance.barcode('test', bctype) + + assert instance.output == '' \ No newline at end of file From 3d8626d17e8192943691b3b8de525c9e3a239239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 13:08:23 +0200 Subject: [PATCH 17/84] Update text() test to use dummy printer. --- test/Dies ist ein Test.LF.txt | 1 - test/test_function_text.py | 28 ++-------------------------- 2 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 test/Dies ist ein Test.LF.txt 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_function_text.py b/test/test_function_text.py index 973ddfc..dc25842 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -12,38 +12,14 @@ 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 from escpos.printer import Dummy -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(): """test the text printing function with simple string and compare output""" - instance = printer.File(devfile=devfile) + instance = Dummy() instance.text('Dies ist ein Test.\n') - instance.flush() - assert(filecmp.cmp('test/Dies ist ein Test.LF.txt', devfile)) + assert instance.output == 'Dies ist ein Test.\n' def test_block_text(): From 4496ea91bd9e8969b0e0b9d45d96448f24a170af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 13:09:29 +0200 Subject: [PATCH 18/84] Make pytest the test runner. --- setup.py | 2 +- tox.ini | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 219ee2c..ebc785d 100755 --- a/setup.py +++ b/setup.py @@ -117,7 +117,7 @@ setup( setup_requires=[ 'setuptools_scm', ], - tests_require=['tox', 'nose', 'scripttest', 'mock', 'hypothesis'], + tests_require=['tox', 'pytest', 'pytest-cov', 'nose', 'scripttest', 'mock', 'hypothesis'], cmdclass={'test': Tox}, entry_points={ 'console_scripts': [ diff --git a/tox.ini b/tox.ini index 0968270..5412ac3 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,10 @@ deps = nose coverage scripttest mock + pytest + pytest-cov hypothesis -commands = nosetests --with-coverage --cover-erase --cover-branches +commands = py.test [testenv:docs] basepython = python From 3681c5c7bfcaff98adf388e84cc1cdab1923c054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 13:20:42 +0200 Subject: [PATCH 19/84] Fix tests for Python 3. --- test/test_function_barcode.py | 2 +- test/test_function_text.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_function_barcode.py b/test/test_function_barcode.py index 4205eba..c6ec958 100644 --- a/test/test_function_barcode.py +++ b/test/test_function_barcode.py @@ -31,4 +31,4 @@ def test_lacks_support(bctype, supports_b): with pytest.raises(BarcodeTypeError): instance.barcode('test', bctype) - assert instance.output == '' \ No newline at end of file + assert instance.output == b'' \ No newline at end of file diff --git a/test/test_function_text.py b/test/test_function_text.py index dc25842..7f91c95 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -19,7 +19,7 @@ def test_function_text_dies_ist_ein_test_lf(): """test the text printing function with simple string and compare output""" instance = Dummy() instance.text('Dies ist ein Test.\n') - assert instance.output == 'Dies ist ein Test.\n' + assert instance.output == b'Dies ist ein Test.\n' def test_block_text(): @@ -27,4 +27,4 @@ def test_block_text(): printer.block_text( "All the presidents men were eating falafel for breakfast.", font='a') assert printer.output == \ - 'All the presidents men were eating falafel\nfor breakfast.' + b'All the presidents men were eating falafel\nfor breakfast.' From b92eeed50b1947fdf3a5dbac496814d962a4601b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 13:27:48 +0200 Subject: [PATCH 20/84] Add tests for the profile. --- src/escpos/capabilities.py | 2 +- test/test_profile.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 test/test_profile.py diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index 6e375bc..9ae0e79 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -100,7 +100,7 @@ class Profile(get_profile_class('default')): def get_columns(self, font): if self.columns is not None: - return columns + return self.columns return super(Profile, self).get_columns(font) diff --git a/test/test_profile.py b/test/test_profile.py new file mode 100644 index 0000000..c9418d5 --- /dev/null +++ b/test/test_profile.py @@ -0,0 +1,34 @@ +import pytest +from escpos.capabilities import get_profile, NotSupported, BARCODE_B, Profile + + +@pytest.fixture +def profile(): + return get_profile('default') + + +class TestBaseProfile: + + def test_get_font(self, profile): + with pytest.raises(NotSupported): + assert profile.get_font('3') + assert profile.get_font(1) == 1 + assert profile.get_font('a') == 0 + + def test_supports(self, profile): + assert not profile.supports('asdf asdf') + assert profile.supports(BARCODE_B) + + def test_get_columns(self, profile): + assert profile.get_columns('a') > 5 + with pytest.raises(NotSupported): + assert profile.get_columns('asdfasdf') + + +class TestCustomProfile: + + def test_columns(self): + assert Profile(columns=10).get_columns('sdfasdf') == 10 + + def test_features(self): + assert Profile(features={'foo': True}).supports('foo') \ No newline at end of file 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 21/84] 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 68c17f118199e2ca7be4549ddd890a2c60901233 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 22/84] Generate coverage reports. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5412ac3..362f2a6 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ deps = nose pytest pytest-cov hypothesis -commands = py.test +commands = py.test --cov escpos [testenv:docs] basepython = python From 58ea206c36e42a30435ea63fe860d1a357a6948c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 16:13:38 +0200 Subject: [PATCH 23/84] Avoid mutable default argumet. --- src/escpos/capabilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index 9ae0e79..3dd5c32 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -92,11 +92,11 @@ def clean(s): # For users, who want to provide their profile class Profile(get_profile_class('default')): - def __init__(self, columns=None, features={}): + def __init__(self, columns=None, features=None): super(Profile, self).__init__() self.columns = columns - self.features = features + self.features = features or {} def get_columns(self, font): if self.columns is not None: 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 24/84] 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 25/84] 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 26/84] 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 1bd53697b99b728515adb36e06b2358d013e7593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 17:47:09 +0200 Subject: [PATCH 27/84] Fix the CLI test inside pytest runner. --- test/test_cli.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/test/test_cli.py b/test/test_cli.py index b9aebc3..14df358 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -34,27 +34,22 @@ class TestCLI(): """ Contains setups, teardowns, and tests for CLI """ - def __init__(self): - """ Initalize the tests. - Just define some vars here since most of them get set during - setup_class and teardown_class - """ - self.env = None - self.default_args = None - - @staticmethod - def setup_class(): + @classmethod + def setup_class(cls): """ Create a config file to read from """ with open(CONFIGFILE, 'w') as config: config.write(CONFIG_YAML) - @staticmethod - def teardown_class(): + @classmethod + def teardown_class(cls): """ Remove config file """ os.remove(CONFIGFILE) def setup(self): """ Create a file to print to and set up env""" + self.env = None + self.default_args = None + self.env = TestFileEnvironment( base_path=TEST_DIR, cwd=os.getcwd(), From c850a726cbcf8b4c6d6a09931e3927bf04cb04ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 17:55:58 +0200 Subject: [PATCH 28/84] Fix docstring warnings from QuantifedCode. --- src/escpos/capabilities.py | 5 ++++- test/test_function_barcode.py | 4 ++++ test/test_profile.py | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index 3dd5c32..15416ed 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -12,6 +12,9 @@ ENCODINGS = CAPABILITIES['encodings'] class NotSupported(Exception): + """Raised if a requested feature is not suppored by the + printer profile. + """ pass @@ -37,7 +40,7 @@ class BaseProfile(object): font = {'a': 0, 'b': 1}.get(font, font) if not six.text_type(font) in self.fonts: raise NotSupported( - '"%s" is not a valid font in the current profile' % font) + '"{}" is not a valid font in the current profile'.format(font)) return font def get_columns(self, font): diff --git a/test/test_function_barcode.py b/test/test_function_barcode.py index c6ec958..2edd712 100644 --- a/test/test_function_barcode.py +++ b/test/test_function_barcode.py @@ -16,6 +16,8 @@ import pytest b'\x1ba\x01\x1dh@\x1dw\x03\x1df\x00\x1dH\x02\x1dk\x024006381333931\x00') ]) def test_barcode(bctype, data, expected): + """should generate different barcode types correctly. + """ instance = printer.Dummy() instance.barcode(data, bctype) assert instance.output == expected @@ -26,6 +28,8 @@ def test_barcode(bctype, data, expected): ('CODE128', False), ]) def test_lacks_support(bctype, supports_b): + """should raise an error if the barcode type is not supported. + """ profile = Profile(features={BARCODE_B: supports_b}) instance = printer.Dummy(profile=profile) with pytest.raises(BarcodeTypeError): diff --git a/test/test_profile.py b/test/test_profile.py index c9418d5..d75c88c 100644 --- a/test/test_profile.py +++ b/test/test_profile.py @@ -8,6 +8,8 @@ def profile(): class TestBaseProfile: + """Test the `BaseProfile` class. + """ def test_get_font(self, profile): with pytest.raises(NotSupported): @@ -26,6 +28,8 @@ class TestBaseProfile: class TestCustomProfile: + """Test custom profile options with the `Profile` class. + """ def test_columns(self): assert Profile(columns=10).get_columns('sdfasdf') == 10 From b543ecea5886eb4cbc4b82046071b930df80e8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 30 Aug 2016 18:00:06 +0200 Subject: [PATCH 29/84] Enable branch coverage. --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..398ff08 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +branch = True 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 30/84] 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 31/84] 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("カタカナ") From b29ef6df69401778a9c51ad7383bcb24c9e603c4 Mon Sep 17 00:00:00 2001 From: Curtis // mashedkeyboard Date: Thu, 1 Sep 2016 11:57:58 +0100 Subject: [PATCH 32/84] Corrected set() command documentation The documentation currently says that `printer.set(type="B")` is the way to bold text. It won't work - you need to use `printer.set(text_type="B")`. --- doc/user/methods.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/methods.rst b/doc/user/methods.rst index 12a076c..3edf37b 100644 --- a/doc/user/methods.rst +++ b/doc/user/methods.rst @@ -84,7 +84,7 @@ text("text") Prints raw text. Raises ``TextError`` exception. -set("align", "font", "type", width, height, invert, smooth, flip) +set("align", "font", "text_type", width, height, invert, smooth, flip) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Set text properties. @@ -96,7 +96,7 @@ Set text properties. * RIGHT > > *Default:* left * ``font`` type could be ``A`` or ``B``. *Default:* A -* ``type`` type could be ``B`` (Bold), ``U`` (Underline) or ``normal``. *Default:* normal +* ``text_type`` type could be ``B`` (Bold), ``U`` (Underline) or ``normal``. *Default:* normal * ``width`` is a numeric value, 1 is for regular size, and 2 is twice the standard size. *Default*: 1 * ``height`` is a numeric value, 1 is for regular size and 2 is twice the standard size. *Default*: 1 * ``invert`` is a boolean value, True enables white on black printing. *Default*: False From f5c706db34f3330293e7fba7a61236bfa5c5afd6 Mon Sep 17 00:00:00 2001 From: Curtis // mashedkeyboard Date: Thu, 1 Sep 2016 12:10:35 +0100 Subject: [PATCH 33/84] Extended underline to fit the wording of the new subtitle --- doc/user/methods.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/methods.rst b/doc/user/methods.rst index 3edf37b..228170f 100644 --- a/doc/user/methods.rst +++ b/doc/user/methods.rst @@ -85,7 +85,7 @@ text("text") Prints raw text. Raises ``TextError`` exception. set("align", "font", "text_type", width, height, invert, smooth, flip) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Set text properties. From f467cacdd81053f11ef765db88851c6a35ee3677 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Fri, 2 Sep 2016 16:56:12 +0200 Subject: [PATCH 34/84] improve import capabilities from a submodule The capabilities are now imported as a submodule from mike42/escpos-printer-db fixes #174 --- .gitignore | 1 + .gitmodules | 3 +++ MANIFEST.in | 1 + capabilities-data | 1 + setup.py | 2 +- src/escpos/capabilities.json | 2 +- 6 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 160000 capabilities-data mode change 100644 => 120000 src/escpos/capabilities.json diff --git a/.gitignore b/.gitignore index 944dcb3..932ff9b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ $~ .idea/ .directory +.cache/ # temporary data temp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d474ac2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "capabilities-data"] + path = capabilities-data + url = https://github.com/mike42/escpos-printer-db.git diff --git a/MANIFEST.in b/MANIFEST.in index 997ee7b..eeed97e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ include *.txt include COPYING include INSTALL include tox.ini +include capabilities-data/dist/capabilities.json recursive-include doc *.bat recursive-include doc *.ico recursive-include doc *.py diff --git a/capabilities-data b/capabilities-data new file mode 160000 index 0000000..8744f93 --- /dev/null +++ b/capabilities-data @@ -0,0 +1 @@ +Subproject commit 8744f9397ef6b58aee502aa75ac1efad31c9f5d7 diff --git a/setup.py b/setup.py index ebc785d..d29d7b9 100755 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ setup( author='Manuel F Martinez and others', author_email='manpaz@bashlinux.com', maintainer='Patrick Kanzler', - maintainer_email='patrick.kanzler@fablab.fau.de', + maintainer_email='dev@pkanzler.de', keywords=[ 'ESC/POS', 'thermoprinter', diff --git a/src/escpos/capabilities.json b/src/escpos/capabilities.json deleted file mode 100644 index 02ada55..0000000 --- a/src/escpos/capabilities.json +++ /dev/null @@ -1 +0,0 @@ -{"profiles": {"TM-T88III": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP862", "1": "Unknown", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "255": "Unknown"}, "name": "TM-T88III"}, "TM-P80": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": 72, "pixels": 576}}, "notes": "Portable printer (48-column mode)", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}, "2": {"name": "Kanji", "columns": 24}}, "colors": {"0": "black"}, "codePages": {"51": "CP1257", "48": "CP1254", "50": "CP1256", "82": "Unknown", "49": "CP1255", "66": "Unknown", "67": "Unknown", "68": "Unknown", "69": "Unknown", "52": "CP1258", "53": "RK1048", "254": "Unknown", "255": "Unknown", "24": "Unknown", "25": "Unknown", "26": "Unknown", "20": "Unknown", "21": "CP874", "22": "Unknown", "23": "Unknown", "46": "CP1251", "47": "CP1253", "44": "CP1125", "45": "CP1250", "42": "CP774", "43": "CP772", "40": "ISO_8859-15", "41": "CP1098", "1": "CP932", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "8": "Unknown", "39": "ISO_8859-2", "75": "Unknown", "38": "CP869", "73": "Unknown", "72": "Unknown", "71": "Unknown", "70": "Unknown", "11": "CP851", "13": "CP857", "12": "CP853", "15": "ISO_8859-7", "14": "CP737", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "TCVN-3-2", "30": "TCVN-3-1", "37": "CP864", "36": "CP862", "35": "CP861", "34": "CP855", "33": "CP775", "74": "Unknown", "32": "CP720"}, "name": "TM-P80"}, "P822D": {"vendor": "PBM", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": false, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"56": "CP861", "81": "CP3848", "54": "CP852", "60": "CP855", "61": "CP857", "62": "CP862", "63": "CP864", "64": "CP737", "53": "CP858", "66": "CP869", "67": "CP928", "68": "CP772", "69": "CP774", "80": "CP3847", "52": "CP437", "86": "CP3011", "87": "CP3012", "84": "CP3001", "85": "CP3002", "24": "CP747", "25": "CP1257", "27": "Unknown", "20": "Unknown", "21": "Unknown", "22": "Unknown", "23": "Unknown", "82": "CP1001", "28": "CP864", "29": "CP1001", "1": "Unknown", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "9": "Unknown", "8": "Unknown", "255": "Unknown", "83": "CP2001", "77": "CP3844", "76": "CP3843", "75": "CP3841", "74": "CP3840", "73": "CP1251", "72": "CP1250", "71": "CP1252", "70": "CP874", "79": "CP3846", "78": "CP3845", "10": "Unknown", "59": "CP866", "58": "CP865", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "Unknown", "30": "Unknown", "51": "Unknown", "50": "CP437", "35": "CP1257", "34": "CP1256", "33": "CP720", "55": "CP860", "89": "CP3041", "88": "CP3021", "32": "CP1255", "57": "CP863", "65": "CP851"}, "name": "P822D"}, "TEP-200M": {"vendor": "EPOS", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"51": "CP1257", "48": "CP1254", "50": "CP1256", "82": "Unknown", "49": "CP1255", "66": "Unknown", "67": "Unknown", "68": "Unknown", "69": "Unknown", "52": "CP1258", "53": "RK1048", "254": "Unknown", "255": "Unknown", "24": "Unknown", "25": "Unknown", "26": "Unknown", "20": "Unknown", "21": "CP874", "22": "Unknown", "23": "Unknown", "46": "CP1251", "47": "CP1253", "44": "CP1125", "45": "CP1250", "42": "CP774", "43": "CP772", "40": "ISO_8859-15", "41": "CP1098", "1": "CP932", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "8": "Unknown", "39": "ISO_8859-2", "75": "Unknown", "38": "CP869", "73": "Unknown", "72": "Unknown", "71": "Unknown", "70": "Unknown", "11": "CP851", "13": "CP857", "12": "CP853", "15": "ISO_8859-7", "14": "CP737", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "TCVN-3-2", "30": "TCVN-3-1", "37": "CP864", "36": "CP862", "35": "CP861", "34": "CP855", "33": "CP775", "74": "Unknown", "32": "CP720"}, "name": "TEP200M Series"}, "TM-T88II": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"16": "CP1252", "1": "Unknown", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "255": "Unknown"}, "name": "TM-T88II"}, "TM-P80-42col": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": 63.6, "pixels": 546}}, "notes": "Portable printer (42-column mode)", "fonts": {"1": {"name": "Font B", "columns": 60}, "0": {"name": "Font A", "columns": 42}, "2": {"name": "Kanji", "columns": 21}}, "colors": {"0": "black"}, "codePages": {"51": "CP1257", "48": "CP1254", "50": "CP1256", "82": "Unknown", "49": "CP1255", "66": "Unknown", "67": "Unknown", "68": "Unknown", "69": "Unknown", "52": "CP1258", "53": "RK1048", "254": "Unknown", "255": "Unknown", "24": "Unknown", "25": "Unknown", "26": "Unknown", "20": "Unknown", "21": "CP874", "22": "Unknown", "23": "Unknown", "46": "CP1251", "47": "CP1253", "44": "CP1125", "45": "CP1250", "42": "CP774", "43": "CP772", "40": "ISO_8859-15", "41": "CP1098", "1": "CP932", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "8": "Unknown", "39": "ISO_8859-2", "75": "Unknown", "38": "CP869", "73": "Unknown", "72": "Unknown", "71": "Unknown", "70": "Unknown", "11": "CP851", "13": "CP857", "12": "CP853", "15": "ISO_8859-7", "14": "CP737", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "TCVN-3-2", "30": "TCVN-3-1", "37": "CP864", "36": "CP862", "35": "CP861", "34": "CP855", "33": "CP775", "74": "Unknown", "32": "CP720"}, "name": "TM-P80 (42 column mode)"}, "TSP600": {"vendor": "Star Micronics", "features": {"starCommands": true, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "Star TSP600 thermal printer series with ESC/POS emulation enabled", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"98": "Unknown", "64": "CP3840", "65": "CP3841", "66": "CP3843", "67": "CP3844", "68": "CP3845", "69": "CP3846", "255": "Unknown", "20": "CP774", "21": "CP874", "1": "CP437", "0": "CP437", "3": "CP437", "2": "CP932", "5": "CP852", "4": "CP858", "7": "CP861", "6": "CP860", "9": "CP865", "8": "CP863", "96": "Unknown", "97": "Unknown", "77": "CP3012", "76": "CP3011", "75": "CP3002", "74": "CP3001", "73": "CP2001", "72": "CP1001", "71": "CP3848", "70": "CP3847", "102": "Unknown", "100": "Unknown", "101": "Unknown", "79": "CP3041", "78": "CP3021", "11": "CP855", "10": "CP866", "13": "CP862", "12": "CP857", "15": "CP737", "14": "CP864", "17": "CP869", "16": "CP851", "19": "CP772", "18": "CP928", "34": "CP1251", "99": "Unknown", "33": "CP1250", "32": "CP1252"}, "name": "TSP600 Series"}, "default": {"vendor": "Generic", "features": {"starCommands": false, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "Default ESC/POS profile, suitable for standards-compliant or Epson-branded printers. This profile allows the use of standard ESC/POS features, and can encode a variety of code pages.\n", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"51": "CP1257", "48": "CP1254", "50": "CP1256", "82": "Unknown", "49": "CP1255", "66": "Unknown", "67": "Unknown", "68": "Unknown", "69": "Unknown", "52": "CP1258", "53": "RK1048", "254": "Unknown", "255": "Unknown", "24": "Unknown", "25": "Unknown", "26": "Unknown", "20": "Unknown", "21": "CP874", "22": "Unknown", "23": "Unknown", "46": "CP1251", "47": "CP1253", "44": "CP1125", "45": "CP1250", "42": "CP774", "43": "CP772", "40": "ISO_8859-15", "41": "CP1098", "1": "CP932", "0": "CP437", "3": "CP860", "2": "CP850", "5": "CP865", "4": "CP863", "7": "Unknown", "6": "Unknown", "8": "Unknown", "39": "ISO_8859-2", "75": "Unknown", "38": "CP869", "73": "Unknown", "72": "Unknown", "71": "Unknown", "70": "Unknown", "11": "CP851", "13": "CP857", "12": "CP853", "15": "ISO_8859-7", "14": "CP737", "17": "CP866", "16": "CP1252", "19": "CP858", "18": "CP852", "31": "TCVN-3-2", "30": "TCVN-3-1", "37": "CP864", "36": "CP862", "35": "CP861", "34": "CP855", "33": "CP775", "74": "Unknown", "32": "CP720"}, "name": "Default"}, "TUP500": {"vendor": "Star Micronics", "features": {"starCommands": true, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "Star TUP500 thermal printer series with ESC/POS emulation enabled", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"98": "Unknown", "64": "CP3840", "65": "CP3841", "66": "CP3843", "67": "CP3844", "68": "CP3845", "69": "CP3846", "255": "Unknown", "20": "CP774", "21": "CP874", "1": "CP437", "0": "CP437", "3": "CP437", "2": "CP932", "5": "CP852", "4": "CP858", "7": "CP861", "6": "CP860", "9": "CP865", "8": "CP863", "96": "Unknown", "97": "Unknown", "77": "CP3012", "76": "CP3011", "75": "CP3002", "74": "CP3001", "73": "CP2001", "72": "CP1001", "71": "CP3848", "70": "CP3847", "102": "Unknown", "100": "Unknown", "101": "Unknown", "79": "CP3041", "78": "CP3021", "11": "CP855", "10": "CP866", "13": "CP862", "12": "CP857", "15": "CP737", "14": "CP864", "17": "CP869", "16": "CP851", "19": "CP772", "18": "CP928", "34": "CP1251", "99": "Unknown", "33": "CP1250", "32": "CP1252"}, "name": "TUP500 Series"}, "TM-U220": {"vendor": "Epson", "features": {"starCommands": false, "highDensity": false, "barcodeB": false, "bitImageColumn": true, "graphics": false, "qrCode": false, "bitImageRaster": false}, "media": {"width": {"mm": 80, "pixels": "Unknown"}}, "notes": "Two-color impact printer with 80mm output", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"1": "alternate", "0": "black"}, "codePages": {"0": "CP437"}, "name": "TM-U220"}, "simple": {"vendor": "Generic", "features": {"starCommands": false, "highDensity": true, "barcodeB": false, "bitImageColumn": false, "graphics": false, "qrCode": false, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "A profile for use in printers with unknown or poor compatibility. This profile indicates that a small number of features are supported, so that commands are not sent a printer that is unlikely to understand them.\n", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"0": "CP437"}, "name": "Simple"}, "SP2000": {"vendor": "Star Micronics", "features": {"starCommands": true, "highDensity": true, "barcodeB": true, "bitImageColumn": true, "graphics": true, "qrCode": true, "bitImageRaster": true}, "media": {"width": {"mm": "Unknown", "pixels": "Unknown"}}, "notes": "Star SP2000 impact printer series with ESC/POS emulation enabled", "fonts": {"1": {"name": "Font B", "columns": 56}, "0": {"name": "Font A", "columns": 42}}, "colors": {"0": "black"}, "codePages": {"98": "Unknown", "64": "CP3840", "65": "CP3841", "66": "CP3843", "67": "CP3844", "68": "CP3845", "69": "CP3846", "255": "Unknown", "20": "CP774", "21": "CP874", "1": "CP437", "0": "CP437", "3": "CP437", "2": "CP932", "5": "CP852", "4": "CP858", "7": "CP861", "6": "CP860", "9": "CP865", "8": "CP863", "96": "Unknown", "97": "Unknown", "77": "CP3012", "76": "CP3011", "75": "CP3002", "74": "CP3001", "73": "CP2001", "72": "CP1001", "71": "CP3848", "70": "CP3847", "102": "Unknown", "100": "Unknown", "101": "Unknown", "79": "CP3041", "78": "CP3021", "11": "CP855", "10": "CP866", "13": "CP862", "12": "CP857", "15": "CP737", "14": "CP864", "17": "CP869", "16": "CP851", "19": "CP772", "18": "CP928", "34": "CP1251", "99": "Unknown", "33": "CP1250", "32": "CP1252"}, "name": "SP2000 Series"}}, "encodings": {"CP3041": {"name": "Unimplemented Star-specific CP3041"}, "RK1048": {"iconv": "RK1048", "name": "RK1048"}, "Unknown": {"notes": "Code page that has not yet been identified.", "name": "Unknown"}, "CP1252": {"iconv": "CP1252", "python_encode": "cp1252", "name": "CP1252"}, "CP1255": {"iconv": "CP1255", "python_encode": "cp1255", "name": "CP1255"}, "CP1254": {"iconv": "CP1254", "python_encode": "cp1254", "name": "CP1254"}, "CP1257": {"iconv": "CP1257", "python_encode": "cp1257", "name": "CP1257"}, "CP1256": {"iconv": "CP1256", "python_encode": "cp1256", "name": "CP1256"}, "CP1251": {"iconv": "CP1251", "python_encode": "cp1251", "name": "CP1251"}, "CP1250": {"iconv": "CP1250", "python_encode": "cp1250", "name": "CP1250"}, "CP1253": {"iconv": "CP1253", "python_encode": "cp1253", "name": "CP1253"}, "CP1098": {"name": "CP1098"}, "CP437": {"iconv": "CP437", "python_encode": "cp437", "name": "CP437"}, "CP3021": {"name": "Unimplemented Star-specific CP3021"}, "CP3002": {"name": "Unimplemented Star-specific CP3002"}, "CP1258": {"iconv": "CP1258", "python_encode": "cp1258", "name": "CP1258"}, "CP3001": {"name": "Unimplemented Star-specific CP3001"}, "CP928": {"name": "CP928"}, "TCVN-3-1": {"data": [" ", " ", " \u0103\u00e2\u00ea\u00f4\u01a1\u01b0\u0111 ", " \u00e0\u1ea3\u00e3\u00e1\u1ea1 \u1eb1\u1eb3\u1eb5\u1eaf ", " \u1eb7\u1ea7\u1ea9\u1eab\u1ea5\u1ead\u00e8 \u1ebb\u1ebd", "\u00e9\u1eb9\u1ec1\u1ec3\u1ec5\u1ebf\u1ec7\u00ec\u1ec9 \u0129\u00ed\u1ecb\u00f2", " \u1ecf\u00f5\u00f3\u1ecd\u1ed3\u1ed5\u1ed7\u1ed1\u1ed9\u1edd\u1edf\u1ee1\u1edb\u1ee3\u00f9", " \u1ee7\u0169\u00fa\u1ee5\u1eeb\u1eed\u1eef\u1ee9\u1ef1\u1ef3\u1ef7\u1ef9\u00fd\u1ef5 "], "name": "Vietnamese TCVN-3 1"}, "TCVN-3-2": {"data": [" ", " ", " \u0102\u00c2 \u00d0 \u00ca\u00d4\u01a0\u01af ", " \u00c0\u1ea2\u00c3\u00c1\u1ea0 \u1eb0\u1eb2\u1eb4\u1eae ", " \u1eb6\u1ea6\u1ea8\u1eaa\u1ea4\u1eac\u00c8 \u1eba\u1ebc", "\u00c9\u1eb8\u1ec0\u1ec2\u1ec4\u1ebe\u1ec6\u00cc\u1ec8 \u0128\u00cd\u1eca\u00d2", " \u1ece\u00d5\u00d3\u1ecc\u1ed2\u1ed4\u1ed6\u1ed0\u1ed8\u1edc\u1ede\u1ee0\u1eda\u1ee2\u00d9", " \u1ee6\u0168\u00da\u1ee4\u1eea\u1eec\u1eee\u1ee8\u1ef0\u1ef2\u1ef6\u1ef8\u00dd\u1ef4 "], "name": "Vietnamese TCVN-3 1"}, "CP737": {"iconv": "CP737", "python_encode": "cp737", "name": "CP737"}, "CP747": {"name": "CP747"}, "CP851": {"notes": "Not used, due to inconsistencies between implementations.", "name": "Greek CP851"}, "CP850": {"iconv": "CP850", "python_encode": "cp850", "name": "CP850"}, "CP853": {"name": "CP853"}, "CP852": {"iconv": "CP852", "python_encode": "cp852", "name": "CP852"}, "CP855": {"iconv": "CP855", "python_encode": "cp855", "name": "CP855"}, "CP857": {"iconv": "CP857", "python_encode": "cp857", "name": "CP857"}, "CP861": {"iconv": "CP861", "python_encode": "cp861", "name": "CP861"}, "CP774": {"iconv": "CP774", "name": "CP774"}, "CP775": {"iconv": "CP775", "python_encode": "cp775", "name": "CP775"}, "CP874": {"iconv": "CP874", "python_encode": "cp874", "name": "CP874"}, "CP866": {"iconv": "CP866", "python_encode": "cp866", "name": "CP866"}, "CP1125": {"iconv": "CP1125", "python_encode": "cp1125", "name": "CP1125"}, "CP1001": {"name": "Unimplemented Star-specific CP1001"}, "CP858": {"python_encode": "cp858", "name": "CP858"}, "CP932": {"iconv": "CP932", "python_encode": "cp932", "name": "CP932"}, "CP3011": {"data": ["\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5", "\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00a2\u00a3\u00a5\u20a7\u0192", "\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u2310\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb", "\u2591\u2592\u2593\u2502\u2524\u0100\u2562\u0146\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510", "\u2514\u2534\u252c\u251c\u2500\u253c\u0101\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567", "\u0160\u2564\u010d\u010c\u2558\u2552\u0123\u012a\u012b\u2518\u250c\u2588\u2584\u016b\u016a\u2580", "\u03b1\u00df\u0393\u03c0\u03a3\u03c3\u00b5\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u03c6\u03b5\u2229", "\u0112\u0113\u0122\u0137\u0136\u013c\u013b\u017e\u017d\u2219\u00b7\u221a\u0145\u0161\u25a0 "], "name": "CP3011 Latvian"}, "CP862": {"iconv": "CP862", "python_encode": "cp862", "name": "CP862"}, "CP3012": {"data": ["\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0417\u0418\u0419\u041a\u041b\u041c\u041d\u041e\u041f", "\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\u042a\u042b\u042c\u042d\u042e\u042f", "\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0437\u0438\u0439\u043a\u043b\u043c\u043d\u043e\u043f", "\u2591\u2592\u2593\u2502\u2524\u0100\u2562\u0146\u2555\u2563\u2551\u2557\u255d\u014c\u255b\u2510", "\u2514\u2534\u252c\u251c\u2500\u253c\u0101\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567", "\u0160\u2564\u010d\u010c\u2558\u2552\u0123\u012a\u012b\u2518\u250c\u2588\u2584\u016b\u016a\u2580", "\u0440\u0441\u0442\u0443\u0444\u0445\u0446\u0447\u0448\u0449\u044a\u044b\u044c\u044d\u044e\u044f", "\u0112\u0113\u0122\u0137\u0136\u013c\u013b\u017e\u017d\u2219\u00b7\u221a\u0145\u0161\u25a0 "], "name": "CP3012 Cyrillic"}, "ISO_8859-7": {"iconv": "ISO_8859-7", "python_encode": "iso8859_7", "name": "ISO_8859-7"}, "CP863": {"iconv": "CP863", "python_encode": "cp863", "name": "CP863"}, "CP720": {"python_encode": "cp720", "name": "CP720"}, "CP2001": {"name": "Unimplemented Star-specific CP2001"}, "CP864": {"iconv": "CP864", "python_encode": "cp864", "name": "CP864"}, "ISO_8859-2": {"iconv": "ISO_8859-2", "python_encode": "iso8859_2", "name": "ISO_8859-2"}, "CP865": {"iconv": "CP865", "python_encode": "cp865", "name": "CP865"}, "CP869": {"iconv": "CP869", "python_encode": "cp869", "name": "CP869"}, "CP3848": {"name": "Unimplemented Star-specific CP3848"}, "ISO_8859-15": {"iconv": "ISO_8859-15", "python_encode": "iso8859-15", "name": "ISO_8859-15"}, "CP772": {"iconv": "CP772", "name": "CP772"}, "CP860": {"iconv": "CP860", "python_encode": "cp860", "name": "CP860"}, "CP3843": {"name": "Unimplemented Star-specific CP3843"}, "CP3840": {"name": "Unimplemented Star-specific CP3840"}, "CP3841": {"name": "Unimplemented Star-specific CP3841"}, "CP3846": {"name": "Unimplemented Star-specific CP3846"}, "CP3847": {"name": "Unimplemented Star-specific CP3847"}, "CP3844": {"name": "Unimplemented Star-specific CP3844"}, "CP3845": {"name": "Unimplemented Star-specific CP3845"}}} diff --git a/src/escpos/capabilities.json b/src/escpos/capabilities.json new file mode 120000 index 0000000..fd2b9f4 --- /dev/null +++ b/src/escpos/capabilities.json @@ -0,0 +1 @@ +../../capabilities-data/dist/capabilities.json \ No newline at end of file From 57ed77e33251b0d4e3f1cb0e82bb8459b9af8ed1 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 6 Sep 2016 23:15:54 +0200 Subject: [PATCH 35/84] update contributing.rst on resolving issues --- CONTRIBUTING.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e68360f..9d5de8b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -29,7 +29,7 @@ of every file of code: from __future__ import division from __future__ import print_function from __future__ import unicode_literals - + Furthermore please be aware of the differences between Python 2 and 3. For example `this guide `_ is helpful. Special care has to be taken when dealing with strings and byte-strings. Please note @@ -45,8 +45,7 @@ The checks by Landscape and QuantifiedCode that run on every PR will provide you GIT ^^^ The master-branch contains code that has been released to PyPi. A release is marked with a tag -corresponding to the version. Issues are closed when they have been resolved in a released version -of the package. +corresponding to the version. Issues are closed when they have been resolved in the development-branch. When you have a change to make, begin by creating a new branch from the HEAD of `python-escpos/development`. Name your branch to indicate what you are trying to achieve. Good branch names might From 95a84d3673b303fe5a3a52184a3f0abe25874d48 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 9 Sep 2016 18:26:25 +1000 Subject: [PATCH 36/84] update printer DB submodule path --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index d474ac2..8c16a8a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "capabilities-data"] path = capabilities-data - url = https://github.com/mike42/escpos-printer-db.git + url = https://github.com/receipt-print-hq/escpos-printer-db.git From b5bf1125dba6d4e5e13ff2892b5ff736eb50f7ce Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 11 Sep 2016 17:17:22 +1000 Subject: [PATCH 37/84] reverse the lookup order to correct encoding issues --- src/escpos/magicencode.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index b36b844..3a545b7 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -17,6 +17,7 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals +from builtins import bytes, chr from .constants import CODEPAGE_CHANGE from .exceptions import CharCodeError, Error from .capabilities import get_profile @@ -66,17 +67,22 @@ class Encoder(object): return encoding def can_encode(self, encoding, char): - try: - encoded = CodePages.encode(char, encoding) - assert type(encoded) is bytes - return encoded - except LookupError: - # We don't have this encoding - return False - except UnicodeEncodeError: - return False + # Compute the encodable characters in the upper half of this code page + encodable_chars = [u" "] * 128 + for i in range(0, 128): + codepoint = i + 128 + try: + encodable_chars[i] = bytes([codepoint]).decode(encoding) + except UnicodeDecodeError: + # Non-encodable character + pass + except LookupError: + # We don't have this encoding + return False - return True + # Decide whether this character is encodeable in this code page + is_ascii = ord(char) < 128 + return is_ascii or char in encodable_chars def __encoding_sort_func(self, item): key, index = item From d9a6960f070d4f8aadb82ea9041e63fd5d665c01 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 11 Sep 2016 20:21:30 +1000 Subject: [PATCH 38/84] efficiency improvements for backwards encoding --- src/escpos/magicencode.py | 51 +++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 3a545b7..5ecaf6c 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -17,7 +17,7 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals -from builtins import bytes, chr +from builtins import bytes from .constants import CODEPAGE_CHANGE from .exceptions import CharCodeError, Error from .capabilities import get_profile @@ -46,6 +46,7 @@ class Encoder(object): 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): @@ -66,8 +67,15 @@ class Encoder(object): ).format(encoding, ','.join(self.codepages.keys()))) return encoding - def can_encode(self, encoding, char): - # Compute the encodable characters in the upper half of this code page + 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 be a valid python encoding. + """ + # Compute the encodable characters as an array (this is the format + # that for non-standard codings come in) encodable_chars = [u" "] * 128 for i in range(0, 128): codepoint = i + 128 @@ -76,13 +84,42 @@ class Encoder(object): except UnicodeDecodeError: # Non-encodable character pass - except LookupError: - # We don't have this encoding - return False + return encodable_chars + + 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 - return is_ascii or char in encodable_chars + is_encodable = char in available_map + return is_ascii or is_encodable def __encoding_sort_func(self, item): key, index = item From 9a65945fcd0c7a2d7a2a81f2212c0e138623640f Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 11 Sep 2016 21:03:55 +1000 Subject: [PATCH 39/84] re-work encoder to consult the capabilities database and use custom code pages or python encoder as necessary --- src/escpos/capabilities.py | 2 +- src/escpos/codepages.py | 18 +++--------- src/escpos/magicencode.py | 60 ++++++++++++++++++++++++++++---------- test/test_magicencode.py | 2 +- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index e569ccc..4fa9664 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -58,7 +58,7 @@ class BaseProfile(object): 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()} + return {v: k for k, v in self.codePages.items()} diff --git a/src/escpos/codepages.py b/src/escpos/codepages.py index 9666fb2..05d5852 100644 --- a/src/escpos/codepages.py +++ b/src/escpos/codepages.py @@ -12,21 +12,11 @@ class CodePageManager: 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_name(self, encoding): + # TODO resolve the encoding alias + return encoding.upper() def get_encoding(self, encoding): - # resolve the encoding alias - return encoding.lower() - + return self.data[encoding] CodePages = CodePageManager(CAPABILITIES['encodings']) \ No newline at end of file diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 5ecaf6c..764b05f 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -52,14 +52,14 @@ class Encoder(object): def get_sequence(self, encoding): return int(self.codepages[encoding]) - def get_encoding(self, 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(encoding) + encoding = CodePages.get_encoding_name(encoding) if not encoding in self.codepages: raise ValueError(( 'Encoding "{}" cannot be used for the current profile. ' @@ -72,19 +72,24 @@ class Encoder(object): Gets characters 128-255 for a given code page, as an array. - :param encoding: The name of the encoding. This must be a valid python encoding. + :param encoding: The name of the encoding. This must appear in the CodePage list """ - # Compute the encodable characters as an array (this is the format - # that for non-standard codings come in) - encodable_chars = [u" "] * 128 - for i in range(0, 128): - codepoint = i + 128 - try: - encodable_chars[i] = bytes([codepoint]).decode(encoding) - except UnicodeDecodeError: - # Non-encodable character - pass - return encodable_chars + 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 @@ -121,6 +126,29 @@ class Encoder(object): is_encodable = char in available_map return is_ascii or is_encodable + def _encode_char(self, char, charmap): + """ 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 char in charmap: + return charmap[char] + if ord(char) < 128: + return ord(char) + return ord('?') + + 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.available_characters[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 ( @@ -194,7 +222,7 @@ class MagicEncode(object): self.driver = driver self.encoder = encoder or Encoder(driver.profile.get_code_pages()) - self.encoding = self.encoder.get_encoding(encoding) if encoding else None + self.encoding = self.encoder.get_encoding_name(encoding) if encoding else None self.defaultsymbol = defaultsymbol self.disabled = disabled @@ -259,4 +287,4 @@ class MagicEncode(object): six.int2byte(self.encoder.get_sequence(encoding))) if text: - self.driver._raw(CodePages.encode(text, encoding, errors="replace")) + self.driver._raw(self.encoder.encode(text, encoding)) diff --git a/test/test_magicencode.py b/test/test_magicencode.py index d3d3121..665369e 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -36,7 +36,7 @@ class TestEncoder: @raises(ValueError) def test_get_encoding(self): - Encoder({}).get_encoding('latin1') + Encoder({}).get_encoding_name('latin1') class TestMagicEncode: From 83f926758cce83b081fc6b14026bf8bcd0baeeaf Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 11 Sep 2016 21:06:44 +1000 Subject: [PATCH 40/84] adjust order in _encode_char to prioritise ASCII; ' ' is used in the character list to flag characters with no known UTF-8 code, better not encode as these --- src/escpos/magicencode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 764b05f..d48d59d 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -132,10 +132,10 @@ class Encoder(object): :param char: char to encode :param charmap: dictionary for mapping characters in this code page """ - if char in charmap: - return charmap[char] if ord(char) < 128: return ord(char) + if char in charmap: + return charmap[char] return ord('?') def encode(self, text, encoding, defaultchar='?'): From 7a7ea23628c99790ba73c223906e49f0c2f42cc1 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sun, 11 Sep 2016 21:08:04 +1000 Subject: [PATCH 41/84] fixes to arguments for _encode_char --- src/escpos/magicencode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index d48d59d..ded60b8 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -126,7 +126,7 @@ class Encoder(object): is_encodable = char in available_map return is_ascii or is_encodable - def _encode_char(self, char, charmap): + def _encode_char(self, char, charmap, defaultchar): """ Encode a single character with the given encoding map :param char: char to encode @@ -136,7 +136,7 @@ class Encoder(object): return ord(char) if char in charmap: return charmap[char] - return ord('?') + return ord(defaultchar) def encode(self, text, encoding, defaultchar='?'): """ Encode text under the given encoding From 7b68d97f5f4176bfbf24181ab96906874021f1b9 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Tue, 13 Sep 2016 20:28:54 +1000 Subject: [PATCH 42/84] test fixes - just case-changes to match code page names, seems to need 'future' pip module --- setup.py | 1 + src/escpos/magicencode.py | 2 +- test/test_function_text.py | 2 +- test/test_magicencode.py | 26 +++++++++++++------------- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index d29d7b9..ef7b5b1 100755 --- a/setup.py +++ b/setup.py @@ -113,6 +113,7 @@ setup( 'pyyaml', 'argparse', 'argcomplete', + 'future' ], setup_requires=[ 'setuptools_scm', diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index ded60b8..770703c 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -145,7 +145,7 @@ class Encoder(object): :param encoding: Encoding name to use (must be defined in capabilities) :param defaultchar: Fallback for non-encodable characters """ - codepage_char_map = self.available_characters[encoding] + 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 diff --git a/test/test_function_text.py b/test/test_function_text.py index d4de426..7f3869c 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -20,7 +20,7 @@ from escpos.printer import Dummy def get_printer(): - return Dummy(magic_encode_args={'disabled': True, 'encoding': 'cp437'}) + return Dummy(magic_encode_args={'disabled': True, 'encoding': 'CP437'}) @given(text=st.text()) diff --git a/test/test_magicencode.py b/test/test_magicencode.py index 665369e..45b0d55 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -26,13 +26,13 @@ from escpos.exceptions import CharCodeError, Error class TestEncoder: def test_can_encode(self): - assert not Encoder({'cp437': 1}).can_encode('cp437', u'€') - assert Encoder({'cp437': 1}).can_encode('cp437', u'á') + 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({'cp437': 1}).find_suitable_encoding(u'€') - assert Encoder({'cp858': 1}).find_suitable_encoding(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,17 +51,17 @@ class TestMagicEncode: def test_init_from_none(self, driver): encode = MagicEncode(driver, encoding=None) - encode.write_with_encoding('cp858', '€ ist teuro.') + encode.write_with_encoding('CP858', '€ 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.') + encode = MagicEncode(driver, encoding='CP437') + encode.write_with_encoding('CP858', '€ ist teuro.') assert driver.output == b'\x1bt\x13\xd5 ist teuro.' def test_no_change(self, driver): - encode = MagicEncode(driver, encoding='cp858') - encode.write_with_encoding('cp858', '€ ist teuro.') + encode = MagicEncode(driver, encoding='CP858') + encode.write_with_encoding('CP858', '€ ist teuro.') assert driver.output == b'\xd5 ist teuro.' class TestWrite: @@ -72,14 +72,14 @@ class TestMagicEncode: assert driver.output == b'\x1bt\x0f\xa4 ist teuro.' def test_write_disabled(self, driver): - encode = MagicEncode(driver, encoding='cp437', disabled=True) + 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({'cp437': 1}), - encoding='cp437') + driver, defaultsymbol="_", encoder=Encoder({'CP437': 1}), + encoding='CP437') encode.write(u'€ ist teuro.') assert driver.output == b'_ ist teuro.' @@ -87,7 +87,7 @@ class TestMagicEncode: def test(self, driver): encode = MagicEncode(driver) - encode.force_encoding('cp437') + encode.force_encoding('CP437') assert driver.output == b'\x1bt\x00' encode.write('€ ist teuro.') From a82fefb301f98ff03316d41049694b73d7368e16 Mon Sep 17 00:00:00 2001 From: belono Date: Thu, 15 Sep 2016 22:54:35 +0200 Subject: [PATCH 43/84] Add support for custom cash drawer kick sequence --- src/escpos/constants.py | 1 + src/escpos/escpos.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/escpos/constants.py b/src/escpos/constants.py index d606ecc..370fd74 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -47,6 +47,7 @@ HW_RESET = ESC + b'\x3f\x0a\x00' # Reset printer hardware # Cash Drawer (ESC p ) _CASH_DRAWER = lambda m, t1='', t2='': ESC + b'p' + m + six.int2byte(t1) + six.int2byte(t2) +CD_KICK_DEC_SEQUENCE = lambda esc, p, m, t1=50, t2=50: six.int2byte(esc) + six.int2byte(p) + six.int2byte(m) + six.int2byte(t1) + six.int2byte(t2) CD_KICK_2 = _CASH_DRAWER(b'\x00', 50, 50) # Sends a pulse to pin 2 [] CD_KICK_5 = _CASH_DRAWER(b'\x01', 50, 50) # Sends a pulse to pin 5 [] diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 9a18fec..12702d3 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -636,9 +636,10 @@ class Escpos(object): def cashdraw(self, pin): """ Send pulse to kick the cash drawer - Kick cash drawer on pin 2 or pin 5 according to parameter. + Kick cash drawer on pin 2 or pin 5 according to default parameter. + For non default parameter send a decimal sequence i.e. [27,112,48] or [27,112,0,25,255] - :param pin: pin number, 2 or 5 + :param pin: pin number, 2 or 5 or list of decimals :raises: :py:exc:`~escpos.exceptions.CashDrawerError` """ if pin == 2: @@ -646,7 +647,10 @@ class Escpos(object): elif pin == 5: self._raw(CD_KICK_5) else: - raise CashDrawerError() + try: + self._raw(CD_KICK_DEC_SEQUENCE(*pin)) + except: + raise CashDrawerError() def hw(self, hw): """ Hardware operations From 4f2f1cf520fb447615af85a5e5da14d46252028b Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sat, 17 Sep 2016 18:59:31 +1000 Subject: [PATCH 44/84] Update escpos-printer-db to include TM-T88II fixes --- capabilities-data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capabilities-data b/capabilities-data index 8744f93..31d2269 160000 --- a/capabilities-data +++ b/capabilities-data @@ -1 +1 @@ -Subproject commit 8744f9397ef6b58aee502aa75ac1efad31c9f5d7 +Subproject commit 31d2269651d4d10ca51f59799ee4d05b4c4a1625 From 915adf8fd3ebe630f9fc95c6bbb33ab8ea0e9dda Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 27 Sep 2016 19:50:33 +0200 Subject: [PATCH 45/84] fix file-printer-tests by using pytest-mock --- setup.py | 2 +- test/test_printer_file.py | 24 +++++++++++++----------- tox.ini | 1 + 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index ef7b5b1..d136f95 100755 --- a/setup.py +++ b/setup.py @@ -118,7 +118,7 @@ setup( setup_requires=[ 'setuptools_scm', ], - tests_require=['tox', 'pytest', 'pytest-cov', 'nose', 'scripttest', 'mock', 'hypothesis'], + tests_require=['tox', 'pytest', 'pytest-cov', 'pytest-mock', 'nose', 'scripttest', 'mock', 'hypothesis'], cmdclass={'test': Tox}, entry_points={ 'console_scripts': [ diff --git a/test/test_printer_file.py b/test/test_printer_file.py index bce9a0e..4d1b188 100644 --- a/test/test_printer_file.py +++ b/test/test_printer_file.py @@ -15,7 +15,7 @@ from __future__ import unicode_literals import six -import mock +import pytest from hypothesis import given from hypothesis.strategies import text @@ -27,21 +27,22 @@ else: mock_open_call = '__builtin__.open' @given(path=text()) -@mock.patch(mock_open_call) -@mock.patch('escpos.escpos.Escpos.__init__') -def test_load_file_printer(mock_escpos, mock_open, path): +def test_load_file_printer(mocker, path): """test the loading of the file-printer""" + mock_escpos = mocker.patch('escpos.escpos.Escpos.__init__') + mock_open = mocker.patch(mock_open_call) printer.File(devfile=path) assert mock_escpos.called mock_open.assert_called_with(path, "wb") @given(txt=text()) -@mock.patch.object(printer.File, 'device') -@mock.patch(mock_open_call) -@mock.patch('escpos.escpos.Escpos.__init__') -def test_auto_flush(mock_escpos, mock_open, mock_device, txt): +def test_auto_flush(mocker, txt): """test auto_flush in file-printer""" + mock_escpos = mocker.patch('escpos.escpos.Escpos.__init__') + mock_open = mocker.patch(mock_open_call) + mock_device = mocker.patch.object(printer.File, 'device') + p = printer.File(auto_flush=False) # inject the mocked device-object p.device = mock_device @@ -56,10 +57,11 @@ def test_auto_flush(mock_escpos, mock_open, mock_device, txt): @given(txt=text()) -@mock.patch.object(printer.File, 'device') -@mock.patch(mock_open_call) -def test_flush_on_close(mock_open, mock_device, txt): +def test_flush_on_close(mocker, txt): """test flush on close in file-printer""" + mock_open = mocker.patch(mock_open_call) + mock_device = mocker.patch.object(printer.File, 'device') + p = printer.File(auto_flush=False) # inject the mocked device-object p.device = mock_device diff --git a/tox.ini b/tox.ini index 362f2a6..bed3686 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ deps = nose mock pytest pytest-cov + pytest-mock hypothesis commands = py.test --cov escpos From b795c02dd4ca651aef7cf967e689a129a59b2d3c Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 27 Sep 2016 20:02:15 +0200 Subject: [PATCH 46/84] DOC remove incode TODOs I will add them as issues where applicable. --- test/test_magicencode.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/test_magicencode.py b/test/test_magicencode.py index 45b0d55..3f0dbdb 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -112,14 +112,3 @@ class TestKatakana: 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 -# 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 -# 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 1b2f5097588e7bf194fae28e236ce4b8c1aae46a Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 27 Sep 2016 20:26:22 +0200 Subject: [PATCH 47/84] use jaconv instead of jcconv for japanese chars jaconv is available for more Python-versions and seems to be more professional. Apart from that I added jaconv to the test-requirements but not the requirements. (If the katakana-stuff really works we can later add it as a real dependency) --- setup.py | 2 +- src/escpos/katakana.py | 14 +++++++++----- test/test_magicencode.py | 6 +++--- tox.ini | 1 + 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index d136f95..9d00261 100755 --- a/setup.py +++ b/setup.py @@ -118,7 +118,7 @@ setup( setup_requires=[ 'setuptools_scm', ], - tests_require=['tox', 'pytest', 'pytest-cov', 'pytest-mock', 'nose', 'scripttest', 'mock', 'hypothesis'], + tests_require=['jaconv', 'tox', 'pytest', 'pytest-cov', 'pytest-mock', 'nose', 'scripttest', 'mock', 'hypothesis'], cmdclass={'test': Tox}, entry_points={ 'console_scripts': [ diff --git a/src/escpos/katakana.py b/src/escpos/katakana.py index 7c2e2c7..14b2e61 100644 --- a/src/escpos/katakana.py +++ b/src/escpos/katakana.py @@ -11,24 +11,27 @@ from __future__ import unicode_literals try: - import jcconv + import jaconv except ImportError: - jcconv = None + jaconv = None def encode_katakana(text): """I don't think this quite works yet.""" encoded = [] for char in text: - if jcconv: + if jaconv: # try to convert japanese text to half-katakanas - char = jcconv.kata2half(jcconv.hira2kata(char)) + char = jaconv.z2h(jaconv.hira2kata(char)) # TODO: "the conversion may result in multiple characters" - # When? What should we do about it? + # 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) @@ -36,6 +39,7 @@ def encode_katakana(text): 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', diff --git a/test/test_magicencode.py b/test/test_magicencode.py index 3f0dbdb..fc7a31e 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -95,12 +95,12 @@ class TestMagicEncode: try: - import jcconv + import jaconv except ImportError: - jcconv = None + jaconv = None -@pytest.mark.skipif(not jcconv, reason="jcconv not installed") +@pytest.mark.skipif(not jaconv, reason="jaconv not installed") class TestKatakana: @given(st.text()) @example("カタカナ") diff --git a/tox.ini b/tox.ini index bed3686..7fbd3b8 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py27, py34, py35, docs [testenv] deps = nose + jaconv coverage scripttest mock From 10c589ae8d25b468915ce1cba0afdc930ddcf434 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 4 Oct 2016 14:01:26 +1100 Subject: [PATCH 48/84] swap default fragment height to 960 --- src/escpos/escpos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 32b62fe..1249c10 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -59,7 +59,7 @@ class Escpos(object): pass def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster", - fragment_height=1024): + fragment_height=960): """ Print an image You can select whether the printer should print in high density or not. The default value is high density. From 94e1944d160f242ebebe9614395bb795e5074d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Fri, 26 Aug 2016 11:49:42 +0200 Subject: [PATCH 49/84] Add script to output codepage tables. --- examples/codepage_tables.py | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/codepage_tables.py diff --git a/examples/codepage_tables.py b/examples/codepage_tables.py new file mode 100644 index 0000000..fb535c7 --- /dev/null +++ b/examples/codepage_tables.py @@ -0,0 +1,59 @@ +"""Prints code page tables. +""" + +import six +import sys + +from escpos import printer +from escpos.constants import * + + +def main(): + dummy = printer.Dummy() + + dummy.hw('init') + + for codepage in sys.argv[1:] or ['USA']: + dummy.set(height=2, width=2) + dummy._raw(codepage+"\n\n\n") + print_codepage(dummy, codepage) + dummy._raw("\n\n") + + dummy.cut() + + print dummy.output + + +def print_codepage(printer, codepage): + if codepage.isdigit(): + codepage = int(codepage) + printer._raw(CODEPAGE_CHANGE + six.int2byte(codepage)) + printer._raw("after") + else: + printer.charcode(codepage) + + sep = "" + + # Table header + printer.set(text_type='B') + printer._raw(" %s\n" % sep.join(map(lambda s: hex(s)[2:], range(0,16)))) + printer.set() + + # The table + for x in range(0,16): + # First column + printer.set(text_type='B') + printer._raw("%s " % hex(x)[2:]) + printer.set() + + for y in range(0,16): + byte = six.int2byte(x*16+y) + + if byte in (ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT): + byte = ' ' + + printer._raw(byte) + printer._raw(sep) + printer._raw('\n') + +main() \ No newline at end of file From da0d49c7875f36416b44d1405c7997852e9b3ce5 Mon Sep 17 00:00:00 2001 From: ysuolmai Date: Fri, 25 Nov 2016 17:20:36 +0800 Subject: [PATCH 50/84] Update raspi.rst --- doc/user/raspi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/raspi.rst b/doc/user/raspi.rst index 387c648..93cfa29 100644 --- a/doc/user/raspi.rst +++ b/doc/user/raspi.rst @@ -64,7 +64,7 @@ If you have installed pyusb for libusb-1.0 then you need to: :: - # git clone https://github.com/manpaz/python-escpos.git + # git clone --recursive https://github.com/manpaz/python-escpos.git # cd python-escpos # python setup.py build # sudo python setup.py install From bd57c01794f55a4f2c9d4c7373692a4ac0fdeb07 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Wed, 7 Dec 2016 20:31:49 +0100 Subject: [PATCH 51/84] update doc-page on Raspi --- doc/user/raspi.rst | 59 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/doc/user/raspi.rst b/doc/user/raspi.rst index 93cfa29..ea807a4 100644 --- a/doc/user/raspi.rst +++ b/doc/user/raspi.rst @@ -2,16 +2,28 @@ Raspberry Pi ************ -This instructions were tested on Raspbian. +:Last Reviewed: 2016-12-07 -Unless you have done any distro with libusb-1.0 on the Raspberry Pi, the -following instructions should works fine on your raspberry distro. +This instructions were tested on Raspbian. .. warning:: You should **never** directly connect an printer with RS232-interface (serial port) directly to a Raspberry PI or similar interface (e.g. those simple USB-sticks without encasing). Those interfaces are based on 5V- or 3,3V-logic (the latter in the case of Raspberry PI). Classical RS232 uses 12V-logic and would **thus destroy your interface**. Connect both systems with an appropriate *level shifter*. +Normal installation +------------------- +Normally you should be able to install the library just like on any other +Linux distribution. Please be reminded that the newer versions are not in Debian +and thus not in Raspbian. +However, you can just install with `pip`. For more details on this +check the :doc:`installation-manual `. + +If this works for you, you don't need to continue reading this part. +If not, please check if the rest might help you. +Should you have any problems, do not hesitate to contact the maintainer, since you +might have found a bug in the code or documentation. + Dependencies ------------ @@ -19,13 +31,15 @@ First, install the packages available on Raspbian. :: - # apt-get install python-imaging python-serial python-setuptools + apt-get install python-imaging python-serial python-setuptools PyUSB ^^^^^ +.. todo:: The freshness of this part is not verified. Please take it with a grain of salt or help updating it. + PyUSB 1.0 is not available on Ubuntu, so you have to download and -install it manually +install it manually. 1. Download the latest tarball from `Sourceforge `__ @@ -35,23 +49,32 @@ install it manually :: # wget ... - # unzip pyusb*.zip - # cd pyusb* - # python setup.py build - # sudo python setup.py install + unzip pyusb*.zip + cd pyusb* + python setup.py build + sudo python setup.py install python-qrcode ^^^^^^^^^^^^^ +You can install qrcode just from pip with + + :: + + sudo pip install qrcode + +Otherwise you can install it manually. If in doubt please check the documentation of the +`python-qrcode`-project. + 1. Checkout the code from github 2. Install the library :: - # git clone https://github.com/lincolnloop/python-qrcode - # cd python-qrcode - # python setup.py build - # sudo python setup.py install + git clone https://github.com/lincolnloop/python-qrcode + cd python-qrcode + python setup.py build + sudo python setup.py install Installation ------------ @@ -64,10 +87,10 @@ If you have installed pyusb for libusb-1.0 then you need to: :: - # git clone --recursive https://github.com/manpaz/python-escpos.git - # cd python-escpos - # python setup.py build - # sudo python setup.py install + git clone --recursive https://github.com/python-escpos/python-escpos.git + cd python-escpos + python setup.py build + sudo python setup.py install Now you can attach your printer and and test it with the example code in -the project's `home `__ +the project's set of examples. You can find that in the `project-repository `__. From 6158ba344fbd4eb0623288c5fe89ad98dd7d6f20 Mon Sep 17 00:00:00 2001 From: Thijs Triemstra Date: Fri, 9 Dec 2016 16:42:23 +0100 Subject: [PATCH 52/84] fix typos --- doc/user/methods.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/user/methods.rst b/doc/user/methods.rst index 228170f..09b6df5 100644 --- a/doc/user/methods.rst +++ b/doc/user/methods.rst @@ -8,13 +8,13 @@ Methods Escpos class ------------ -Escpos inherits its methods to the printers. the following methods are +Escpos inherits its methods to the printers. The following methods are defined: image("image\_name.ext") ^^^^^^^^^^^^^^^^^^^^^^^^ -Prints an image. Its adjust the size in order to print it. +Prints an image. It adjusts the size in order to print it. * ``image_name.ext`` is the complete file name and location of any image type (jpg, gif, png, bmp) @@ -72,7 +72,7 @@ Prints a barcode. * A * B > *Default:* A -* ``fuction_type`` chooses between ESCPOS function type A or B. A is default, B has more barcode options. Choose which one based upon your printer support and require barcode. +* ``function_type`` chooses between ESCPOS function type A or B. A is default, B has more barcode options. Choose which one based upon your printer support and required barcode. * A * B > *Default* A @@ -115,7 +115,7 @@ Cut paper. cashdraw(pin) ^^^^^^^^^^^^^ -Sends a pulse to the cash drawer in the specified pin. +Sends a pulse to the cash drawer on the specified pin. * ``pin`` is a numeric value which defines the pin to be used to send the pulse, it could be 2 or 5. Raises ``CashDrawerError()`` From 24731f433e98b5d643867f165dbb6f38580f8338 Mon Sep 17 00:00:00 2001 From: Thijs Triemstra Date: Fri, 9 Dec 2016 16:56:23 +0100 Subject: [PATCH 53/84] fix typos --- doc/user/usage.rst | 52 ++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/doc/user/usage.rst b/doc/user/usage.rst index e507cb5..4ef0439 100644 --- a/doc/user/usage.rst +++ b/doc/user/usage.rst @@ -8,12 +8,11 @@ Define your printer USB printer ^^^^^^^^^^^ -Before start creating your Python ESC/POS printer instance, you must see -at your system for the printer parameters. This is done with the 'lsusb' -command. +Before creating your Python ESC/POS printer instance, consult the system to obtain +the printer parameters. This is done with the 'lsusb' command. -First run the command to look for the "Vendor ID" and "Product ID", then -write down the values, these values are displayed just before the name +Run the command and look for the "Vendor ID" and "Product ID" and write +down the values. These values are displayed just before the name of the device with the following format: :: @@ -37,7 +36,7 @@ so you can get the "Interface" number and "End Point" # lsusb -vvv -d xxxx:xxxx | grep bEndpointAddress | grep OUT bEndpointAddress 0x01 EP 1 OUT -The first command will yields the "Interface" number that must be handy +The first command will yield the "Interface" number that must be handy to have and the second yields the "Output Endpoint" address. **USB Printer initialization** @@ -47,9 +46,9 @@ to have and the second yields the "Output Endpoint" address. Epson = printer.Usb(0x04b8,0x0202) By default the "Interface" number is "0" and the "Output Endpoint" -address is "0x01", if you have other values then you can define with +address is "0x01". If you have other values then you can define them on your instance. So, assuming that we have another printer where in\_ep is -on 0x81 and out\_ep=0x02, then the printer definition should looks like: +on 0x81 and out\_ep=0x02, then the printer definition should look like: **Generic USB Printer initialization** @@ -72,10 +71,10 @@ IP by DHCP or you set it manually. Serial printer ^^^^^^^^^^^^^^ -Must of the default values set by the DIP switches for the serial +Most of the default values set by the DIP switches for the serial printers, have been set as default on the serial printer class, so the -only thing you need to know is which serial port the printer is hooked -up. +only thing you need to know is which serial port the printer is connected +to. **Serial printer initialization** @@ -86,9 +85,9 @@ up. Other printers ^^^^^^^^^^^^^^ -Some printers under /dev can't be used or initialized with any of the +Some printers under `/dev` can't be used or initialized with any of the methods described above. Usually, those are printers used by printcap, -however, if you know the device name, you could try the initialize +however, if you know the device name, you could try to initialize by passing the device node name. :: @@ -101,8 +100,8 @@ node, then you don't necessary need to pass the node name. Define your instance -------------------- -The following example demonstrate how to initialize the Epson TM-TI88IV -on USB interface +The following example demonstrates how to initialize the Epson TM-TI88IV +on a USB interface. :: @@ -125,9 +124,9 @@ Configuration File You can create a configuration file for python-escpos. This will allow you to use the CLI, and skip some setup when using the library -programically. +programmatically. -The default configuration file is named ``config.yaml``. It's in the YAML +The default configuration file is named ``config.yaml`` and uses the YAML format. For windows it is probably at:: %appdata%/python-escpos/config.yaml @@ -143,11 +142,10 @@ If you aren't sure, run:: c.load() If it can't find the configuration file in the default location, it will tell -you where it's looking. You can always pass a path or a list of paths to -search to the ``load()`` method. +you where it's looking. You can always pass a path, or a list of paths, to +the ``load()`` method. - -To load the configured pritner, run:: +To load the configured printer, run:: from escpos import config c = config.Config() @@ -180,15 +178,15 @@ And for a network printer:: 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. +from ``__future__`` if you are on Python 2. On Python 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. +the right codepage and then send the encoded data to the printer. If this feature does not work, please try to +isolate the error and then create an issue on the Github project page. -I you want or need to you can manually set the codepage. For this please use the ``charcode()``-function. You can set +If 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 +After you have manually set the codepage the printer won't change it anymore. You can revert to normal behaviour by setting charcode to ``AUTO``. Advanced Usage: Print from binary blob @@ -244,7 +242,7 @@ This is probably best explained by an example: # send code to printer p._raw(d.output) -This way you could also store the code in a file and print later. +This way you could also store the code in a file and print it later. You could then for example print the code from another process than your main-program and thus reduce the waiting time. (Of course this will not make the printer print faster.) From f8b7238801c82f7858c64a60162cbe36aa8fe332 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Fri, 9 Dec 2016 21:14:16 +0100 Subject: [PATCH 54/84] downgrade docutils version 0.13 of docutils seems to have problems. See sphinx-doc/sphinx#3212 --- .travis.yml | 2 +- doc/requirements.txt | 1 + tox.ini | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cce6180..5e3daa3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ matrix: - python: nightly - python: pypy3 before_install: - - pip install tox codecov + - pip install tox codecov docutils==0.12 script: - tox - codecov diff --git a/doc/requirements.txt b/doc/requirements.txt index acd8cd8..70b595d 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,3 +4,4 @@ qrcode>=4.0 pyserial sphinx-rtd-theme setuptools-scm +docutils==0.12 diff --git a/tox.ini b/tox.ini index 7fbd3b8..5e32ebe 100644 --- a/tox.ini +++ b/tox.ini @@ -18,4 +18,5 @@ basepython = python changedir = doc deps = sphinx setuptools_scm + docutils==0.12 commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html From 938f9890ab3c0dd76d09f8e236b71501c0283012 Mon Sep 17 00:00:00 2001 From: Asuki Kono Date: Fri, 6 Jan 2017 05:51:07 +0900 Subject: [PATCH 55/84] Update doc about installation for raspi (#194) * Update dec about installation for raspi * Rollback syntax about warning on raspi doc * Add a link of installation-manual on raspi doc * Update last reviewed date for raspi doc --- doc/user/raspi.rst | 90 ++++++++++------------------------------------ 1 file changed, 18 insertions(+), 72 deletions(-) diff --git a/doc/user/raspi.rst b/doc/user/raspi.rst index ea807a4..bb05f5e 100644 --- a/doc/user/raspi.rst +++ b/doc/user/raspi.rst @@ -2,95 +2,41 @@ Raspberry Pi ************ -:Last Reviewed: 2016-12-07 +:Last Reviewed: 2017-01-05 -This instructions were tested on Raspbian. +This instructions were tested on Raspbian Jessie. .. warning:: You should **never** directly connect an printer with RS232-interface (serial port) directly to a Raspberry PI or similar interface (e.g. those simple USB-sticks without encasing). Those interfaces are based on 5V- or 3,3V-logic (the latter in the case of Raspberry PI). Classical RS232 uses 12V-logic and would **thus destroy your interface**. Connect both systems with an appropriate *level shifter*. -Normal installation -------------------- -Normally you should be able to install the library just like on any other -Linux distribution. Please be reminded that the newer versions are not in Debian -and thus not in Raspbian. -However, you can just install with `pip`. For more details on this -check the :doc:`installation-manual `. - -If this works for you, you don't need to continue reading this part. -If not, please check if the rest might help you. -Should you have any problems, do not hesitate to contact the maintainer, since you -might have found a bug in the code or documentation. - Dependencies ------------ - First, install the packages available on Raspbian. :: - apt-get install python-imaging python-serial python-setuptools - -PyUSB -^^^^^ - -.. todo:: The freshness of this part is not verified. Please take it with a grain of salt or help updating it. - -PyUSB 1.0 is not available on Ubuntu, so you have to download and -install it manually. - -1. Download the latest tarball from - `Sourceforge `__ -2. Decompress the zip file -3. Install the library - - :: - - # wget ... - unzip pyusb*.zip - cd pyusb* - python setup.py build - sudo python setup.py install - -python-qrcode -^^^^^^^^^^^^^ - -You can install qrcode just from pip with - - :: - - sudo pip install qrcode - -Otherwise you can install it manually. If in doubt please check the documentation of the -`python-qrcode`-project. - -1. Checkout the code from github -2. Install the library - - :: - - git clone https://github.com/lincolnloop/python-qrcode - cd python-qrcode - python setup.py build - sudo python setup.py install + sudo apt-get install python3 python3-setuptools python3-pip libjpeg8-dev Installation ------------ - -If you have installed pyusb for libusb-1.0 then you need to: - -1. Download the latest file -2. Decompress the file -3. Install the library +You can install by using pip3. :: - git clone --recursive https://github.com/python-escpos/python-escpos.git - cd python-escpos - python setup.py build - sudo python setup.py install + sudo pip3 install --upgrade pip + sudo pip3 install python-escpos -Now you can attach your printer and and test it with the example code in -the project's set of examples. You can find that in the `project-repository `__. +Run +--- +You need sudo and python3 to run your program. + +:: + + sudo python3 your-program.py + +Now you can attach your printer and and test it with the example code in the project's set of examples. +You can find that in the `project-repository `__. + +For more details on this check the :doc:`installation-manual `. From a15d02b50c09ad84a97c9326964c18bb89026544 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Fri, 6 Jan 2017 16:54:36 +0100 Subject: [PATCH 56/84] change dependency to docutils to newer version of sphinx This is a revert to f8b7238801c82f7858c64a60162cbe36aa8fe332 --- .travis.yml | 2 +- doc/requirements.txt | 2 +- tox.ini | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e3daa3..0932316 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ matrix: - python: nightly - python: pypy3 before_install: - - pip install tox codecov docutils==0.12 + - pip install tox codecov 'sphinx>=1.5.1' script: - tox - codecov diff --git a/doc/requirements.txt b/doc/requirements.txt index 70b595d..682316c 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,4 +4,4 @@ qrcode>=4.0 pyserial sphinx-rtd-theme setuptools-scm -docutils==0.12 +docutils>=0.12 diff --git a/tox.ini b/tox.ini index 5e32ebe..7c9e8ca 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,6 @@ commands = py.test --cov escpos [testenv:docs] basepython = python changedir = doc -deps = sphinx +deps = sphinx>=1.5.1 setuptools_scm - docutils==0.12 commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html From 7bf6a1791be03764a38f3cc88f65e8c4d201cf39 Mon Sep 17 00:00:00 2001 From: Thijs Triemstra Date: Wed, 25 Jan 2017 23:56:06 +0100 Subject: [PATCH 57/84] Correct fragment_height documentation default (#198) --- src/escpos/escpos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 1249c10..7fce107 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -79,7 +79,7 @@ class Escpos(object): :param high_density_vertical: print in high density in vertical direction *default:* True :param high_density_horizontal: print in high density in horizontal direction *default:* True :param impl: choose image printing mode between `bitImageRaster`, `graphics` or `bitImageColumn` - :param fragment_height: Images larger than this will be split into multiple fragments *default:* 1024 + :param fragment_height: Images larger than this will be split into multiple fragments *default:* 960 """ im = EscposImage(img_source) From e595bc21508505fff4566c094f759fa928fcc6f8 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Thu, 26 Jan 2017 00:23:50 +0100 Subject: [PATCH 58/84] doc update methods and printers and use autodoc --- doc/user/methods.rst | 143 +++--------------------------------------- doc/user/printers.rst | 89 ++++++++++++++++---------- src/escpos/printer.py | 6 +- 3 files changed, 66 insertions(+), 172 deletions(-) diff --git a/doc/user/methods.rst b/doc/user/methods.rst index 09b6df5..5a150ad 100644 --- a/doc/user/methods.rst +++ b/doc/user/methods.rst @@ -1,144 +1,17 @@ ******* Methods ******* - -.. note:: **TODO** Merge this page with the API-description. (Make the API-description more pretty and then - replace this with the API-description.) +:Last Reviewed: 2017-01-25 Escpos class ------------ -Escpos inherits its methods to the printers. The following methods are -defined: +The core part of this libraries API is the Escpos class. +You use it by instantiating a :doc:`printer ` which is a child of Escpos. +The following methods are available: -image("image\_name.ext") -^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: escpos.escpos.Escpos + :members: + :member-order: bysource + :noindex: -Prints an image. It adjusts the size in order to print it. - -* ``image_name.ext`` is the complete file name and location of any image type (jpg, gif, png, bmp) - -Raises ``ImageSizeError`` exception. - -qr("text") -^^^^^^^^^^ - -Prints a QR code. The size has been adjusted to Version 4, so it can be -enough small to be printed but also enough big to be read by a smart -phone. - -* ``text`` Any text that needs to be QR encoded. It could be a slogan, - salutation, url, etc. - -barcode("code", "barcode\_type", width, height, "position", "font") -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Prints a barcode. - -* ``code`` is an alphanumeric code to be printed as bar code -* ``barcode_type`` must be one of the following type of codes for function type A: - - * UPC-A - * UPC-E - * EAN13 - * EAN8 - * CODE39 - * ITF - * NW7 - - And for function type B: - - * Any type above - * CODE93 - * CODE128 - * GS1-128 - * GS1 DataBar Omnidirectional - * GS1 DataBar Truncated - * GS1 DataBar Limited - * GS1 DataBar Expanded - - -* ``width`` is a numeric value in the range between (1,255) *Default:* 64 -* ``height`` is a numeric value in the range between (2,6) *Default:* 3 -* ``position`` is where to place the code around the bars, could be one of the following values: - - * ABOVE - * BELOW - * BOTH - * OFF > *Default:* BELOW - -* ``font`` is one of the 2 type of fonts, values could be: - - * A - * B > *Default:* A - -* ``function_type`` chooses between ESCPOS function type A or B. A is default, B has more barcode options. Choose which one based upon your printer support and required barcode. - - * A - * B > *Default* A - -* Raises ``BarcodeTypeError``, ``BarcodeSizeError``, ``BarcodeCodeError`` exceptions. - -text("text") -^^^^^^^^^^^^ - -Prints raw text. Raises ``TextError`` exception. - -set("align", "font", "text_type", width, height, invert, smooth, flip) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Set text properties. - -* ``align`` set horizontal position for text, the possible values are: - - * CENTER - * LEFT - * RIGHT > > *Default:* left - -* ``font`` type could be ``A`` or ``B``. *Default:* A -* ``text_type`` type could be ``B`` (Bold), ``U`` (Underline) or ``normal``. *Default:* normal -* ``width`` is a numeric value, 1 is for regular size, and 2 is twice the standard size. *Default*: 1 -* ``height`` is a numeric value, 1 is for regular size and 2 is twice the standard size. *Default*: 1 -* ``invert`` is a boolean value, True enables white on black printing. *Default*: False -* ``smooth`` is a boolean value, True enables text smoothing. *Default*: False -* ``flip`` is a boolean value, True enables upside-down text. *Default*: False - -cut("mode") -^^^^^^^^^^^ - -Cut paper. - -* ``mode`` set a full or partial cut. *Default:* full - -**Partial cut is not implemented in all printers.** - -cashdraw(pin) -^^^^^^^^^^^^^ - -Sends a pulse to the cash drawer on the specified pin. - -* ``pin`` is a numeric value which defines the pin to be used to send the pulse, it could be 2 or 5. Raises ``CashDrawerError()`` - -hw("operation") -^^^^^^^^^^^^^^^ - -Hardware operations. - -* ``operation`` is any of the following options: - - * INIT - * SELECT - * RESET - -control("align") -^^^^^^^^^^^^^^^^ - -Carrier feed and tabs. - -* ``align`` is a string which takes any of the following values: - - * LF *for Line Feed* - * FF *for Form Feed* - * CR *for Carriage Return* - * HT *for Horizontal Tab* - * VT *for Vertical Tab* diff --git a/doc/user/printers.rst b/doc/user/printers.rst index 073aef3..1e3484b 100644 --- a/doc/user/printers.rst +++ b/doc/user/printers.rst @@ -1,56 +1,77 @@ ******** Printers ******** +:Last Reviewed: 2017-01-25 -.. note:: **TODO** Merge this page into the API-description. +As of now there are 5 different type of printer implementations. -There 3 different type of printers: +USB +--- +The USB-class uses pyusb and libusb to communicate with USB-based +printers. Note that this driver is not suited for USB-to-Serial-adapters +and similiar devices, but only for those implementing native USB. -USB(idVendor, idProduct, interface, in\_ep, out\_ep) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: escpos.printer.Usb + :members: + :special-members: + :member-order: bysource + :noindex: -Based on pyusb and libusb-1.0 +Serial +------ +This driver uses pyserial in order to communicate with serial devices. +If you are using an USB-based adapter to connect to the serial port, +then you should also use this driver. +The configuration is often based on DIP-switches that you can set on your +printer. For the hardware-configuration please refer to your printer's manual. -* ``idVendor`` is the Vendor ID -* ``idProduct`` is the Product ID -* ``interface`` is the USB device interface (default = 0) -* ``in_ep`` is the input end point (default = 0x82) -* ``out_ep`` is the output end point (default = 0x01) +.. autoclass:: escpos.printer.Serial + :members: + :special-members: + :member-order: bysource + :noindex: -Serial("devfile", baudrate, bytesize, timeout) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Network +------- -Based on pyserial, default values are based on the defaults set by -DIP\_SWITCH\_1 on the documentation(hardware side). +This driver is based on the socket class. -* ``devfile`` is an alphanumeric device file name under /dev filesystem (default = /ev/ttyS0) -* ``baudrate`` is the Baud rate for serial transmission (default = 9600) -* ``bytesize`` sets the serial buffer size (default = 8) -* ``timeout`` defines Read/Write timeout (default = 1) +.. autoclass:: escpos.printer.Network + :members: + :special-members: + :member-order: bysource + :noindex: -Network("host", port) -^^^^^^^^^^^^^^^^^^^^^ - -Based on socket - -* ``host`` is an alphanumeric host name, could be either DNS host name or IP address. -* ``port`` to write to (default = 9100) - -Troubleshooting: +Troubleshooting +^^^^^^^^^^^^^^^ Problems with a network-attached printer can have numerous causes. Make sure that your device has a proper IP address. Often you can check the IP address by triggering the self-test of the device. As a next step try to send text manually to the device. You could use for example: -.. :: + :: - echo "OK\n" | nc IPADDRESS 9100 - # the port number is often 9100 + echo "OK\n" | nc IPADDRESS 9100 + # the port number is often 9100 As a last resort try to reset the interface of the printer. This should be described in its manual. -File("file\_name") -^^^^^^^^^^^^^^^^^^ +File +---- +This printer "prints" just into a file-handle. Especially on \*nix-systems this comes very handy. -Printcap printers +.. autoclass:: escpos.printer.File + :members: + :special-members: + :member-order: bysource + :noindex: + +Dummy +----- +The Dummy-printer is mainly for testing- and debugging-purposes. It stores +all of the "output" as raw ESC/POS in a string and returns that. + +.. autoclass:: escpos.printer.Dummy + :members: + :member-order: bysource + :noindex: -* ``file_name`` is the full path to the device file name diff --git a/src/escpos/printer.py b/src/escpos/printer.py index ec936b5..1fe9cf1 100644 --- a/src/escpos/printer.py +++ b/src/escpos/printer.py @@ -182,9 +182,9 @@ class Network(Escpos): def __init__(self, host, port=9100, timeout=60, *args, **kwargs): """ - :param host : Printer's hostname or IP address - :param port : Port to write to - :param timeout : timeout in seconds for the socket-library + :param host: Printer's hostname or IP address + :param port: Port to write to + :param timeout: timeout in seconds for the socket-library """ Escpos.__init__(self, *args, **kwargs) self.host = host From d3f76a5f6d25c6f96dee1844dee11c35d7c0d1e3 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sun, 29 Jan 2017 22:33:15 +0100 Subject: [PATCH 59/84] doc update modules in autogeneration --- doc/api/capabilities.rst | 10 ++++++++++ doc/api/cli.rst | 10 ++++++++++ doc/api/codepages.rst | 10 ++++++++++ doc/api/katakana.rst | 10 ++++++++++ doc/api/magicencode.rst | 10 ++++++++++ doc/index.rst | 5 +++++ 6 files changed, 55 insertions(+) create mode 100644 doc/api/capabilities.rst create mode 100644 doc/api/cli.rst create mode 100644 doc/api/codepages.rst create mode 100644 doc/api/katakana.rst create mode 100644 doc/api/magicencode.rst diff --git a/doc/api/capabilities.rst b/doc/api/capabilities.rst new file mode 100644 index 0000000..f88b0e1 --- /dev/null +++ b/doc/api/capabilities.rst @@ -0,0 +1,10 @@ +Capabilities +------------ +Module :py:mod:`escpos.capabilities` + +.. automodule:: escpos.capabilities + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :member-order: bysource \ No newline at end of file diff --git a/doc/api/cli.rst b/doc/api/cli.rst new file mode 100644 index 0000000..dabbc10 --- /dev/null +++ b/doc/api/cli.rst @@ -0,0 +1,10 @@ +CLI +--- +Module :py:mod:`escpos.cli` + +.. automodule:: escpos.cli + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :member-order: bysource \ No newline at end of file diff --git a/doc/api/codepages.rst b/doc/api/codepages.rst new file mode 100644 index 0000000..c4e9183 --- /dev/null +++ b/doc/api/codepages.rst @@ -0,0 +1,10 @@ +Codepages +--------- +Module :py:mod:`escpos.codepages` + +.. automodule:: escpos.codepages + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :member-order: bysource \ No newline at end of file diff --git a/doc/api/katakana.rst b/doc/api/katakana.rst new file mode 100644 index 0000000..899b080 --- /dev/null +++ b/doc/api/katakana.rst @@ -0,0 +1,10 @@ +Katakana +-------- +Module :py:mod:`escpos.katakana` + +.. automodule:: escpos.katakana + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :member-order: bysource \ No newline at end of file diff --git a/doc/api/magicencode.rst b/doc/api/magicencode.rst new file mode 100644 index 0000000..715c218 --- /dev/null +++ b/doc/api/magicencode.rst @@ -0,0 +1,10 @@ +Magic Encode +------------ +Module :py:mod:`escpos.magicencode` + +.. automodule:: escpos.magicencode + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :member-order: bysource \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst index 060fc7f..4b5eea7 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -35,8 +35,13 @@ Content api/printer api/constants api/exceptions + api/capabilities api/config api/image + api/cli + api/magicencode + api/codepages + api/katakana Indices and tables ================== From efff2cbe43d1f998d56846ac6d98e0de8c8f8976 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sun, 29 Jan 2017 22:57:12 +0100 Subject: [PATCH 60/84] update changelog --- CHANGELOG.rst | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 41ceb3b..04b1e5b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,18 +2,31 @@ Changelog ********* -2016-08-?? - Version 2.?.? - "?" ------------------------------------------------- +2017-01-29 - Version 3.0a - "Grey Area" +--------------------------------------- +This release is the first alpha release of the new version 3.0. Please +be aware that the API will still change until v3.0 is released. changes ^^^^^^^ -- feature: the driver tries now to guess the appropriate codepage and sets it automatically +- feature: add "capabilities" which are shared with escpos-php +- feature: the driver tries now to guess the appropriate codepage and sets it automatically (called "magic encode") - as an alternative you can force the codepage with the old API +- updated and improved documentation +- changed constructor of main class due to introduction of capablities +- changed interface of method `blocktext`, changed behavior of multiple methods, for details refer to the documentation + on `python-escpos.readthedocs.io `_ contributors ^^^^^^^^^^^^ +- Michael Billington +- Michael Elsdörfer - Patrick Kanzler (with code by Frédéric Van der Essen) - +- Asuki Kono +- Benito López +- Curtis // mashedkeyboard +- Thijs Triemstra +- ysuolmai 2016-08-26 - Version 2.2.0 - "Fate Amenable To Change" ------------------------------------------------------ From e904500312e92e00fcecd95bb0c0f2bd788eca70 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sun, 29 Jan 2017 23:20:11 +0100 Subject: [PATCH 61/84] link to escpos-printer-db --- CHANGELOG.rst | 4 +++- README.rst | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04b1e5b..0c16632 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,13 +9,15 @@ be aware that the API will still change until v3.0 is released. changes ^^^^^^^ -- feature: add "capabilities" which are shared with escpos-php +- feature: add "capabilities" which are shared with escpos-php, capabilities are stored in + `escpos-printer-db `_ - feature: the driver tries now to guess the appropriate codepage and sets it automatically (called "magic encode") - as an alternative you can force the codepage with the old API - updated and improved documentation - changed constructor of main class due to introduction of capablities - changed interface of method `blocktext`, changed behavior of multiple methods, for details refer to the documentation on `python-escpos.readthedocs.io `_ +- add support for custom cash drawer sequence contributors ^^^^^^^^^^^^ diff --git a/README.rst b/README.rst index e79b279..1a10540 100644 --- a/README.rst +++ b/README.rst @@ -37,6 +37,11 @@ Text can be aligned/justified and fonts can be changed by size, type and weight. Also, this module handles some hardware functionalities like cutting paper, control characters, printer reset and similar functions. +Since supported commands differ from printer to printer the software tries to automatically apply the right +settings for the printer that you set. These settings are handled by +`escpos-printer-db `_ which is also used in +`escpos-php `_. + Dependencies ------------ @@ -56,11 +61,11 @@ The basic usage is: from escpos.printer import Usb - """ Seiko Epson Corp. Receipt Printer M129 Definitions (EPSON TM-T88IV) """ - p = Usb(0x04b8,0x0202,0) + """ Seiko Epson Corp. Receipt Printer (EPSON TM-T88III) """ + p = Usb(0x04b8, 0x0202, 0, profile="TM-T88III") p.text("Hello World\n") p.image("logo.gif") - p.barcode('1324354657687','EAN13',64,2,'','') + p.barcode('1324354657687', 'EAN13', 64, 2, '', '') p.cut() The full project-documentation is available on `Read the Docs `_. From e4a21e94fcfb5a00c01a318946aa391c5b08f38f Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sun, 29 Jan 2017 23:36:33 +0100 Subject: [PATCH 62/84] improve doc, improve codepage_tables.py --- examples/codepage_tables.py | 3 ++- src/escpos/capabilities.py | 5 +++-- src/escpos/magicencode.py | 16 +++++++++++----- test/test_magicencode.py | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/examples/codepage_tables.py b/examples/codepage_tables.py index fb535c7..e19ea3f 100644 --- a/examples/codepage_tables.py +++ b/examples/codepage_tables.py @@ -56,4 +56,5 @@ def print_codepage(printer, codepage): printer._raw(sep) printer._raw('\n') -main() \ No newline at end of file +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index 4fa9664..a3bfd3a 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -99,9 +99,10 @@ def clean(s): return str(s) -# For users, who want to provide their profile class Profile(get_profile_class('default')): - + """ + For users, who want to provide their profile + """ def __init__(self, columns=None, features=None): super(Profile, self).__init__() diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 770703c..3dbd256 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -208,14 +208,20 @@ class MagicEncode(object): 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): + """ + + :param driver: + :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. + :param disabled: + :param defaultsymbol: + :param encoder: + """ if disabled and not encoding: raise Error('If you disable magic encode, you need to define an encoding!') diff --git a/test/test_magicencode.py b/test/test_magicencode.py index fc7a31e..faaf66d 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -24,6 +24,9 @@ from escpos.exceptions import CharCodeError, Error class TestEncoder: + """ + Tests the single encoders. + """ def test_can_encode(self): assert not Encoder({'CP437': 1}).can_encode('CP437', u'€') @@ -40,10 +43,21 @@ class TestEncoder: class TestMagicEncode: + """ + Tests the magic encode functionality. + """ class TestInit: + """ + Test initialization. + """ def test_disabled_requires_encoding(self, driver): + """ + Test that disabled without encoder raises an error. + + :param driver: + """ with pytest.raises(Error): MagicEncode(driver, disabled=True) From 2ea8e69c664ee5d371416dbd74c81b90fa5a65a3 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Sun, 29 Jan 2017 23:39:26 +0100 Subject: [PATCH 63/84] style fixes --- examples/codepage_tables.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/codepage_tables.py b/examples/codepage_tables.py index e19ea3f..e638b3c 100644 --- a/examples/codepage_tables.py +++ b/examples/codepage_tables.py @@ -15,7 +15,7 @@ def main(): for codepage in sys.argv[1:] or ['USA']: dummy.set(height=2, width=2) - dummy._raw(codepage+"\n\n\n") + dummy._raw(codepage + "\n\n\n") print_codepage(dummy, codepage) dummy._raw("\n\n") @@ -36,14 +36,14 @@ def print_codepage(printer, codepage): # Table header printer.set(text_type='B') - printer._raw(" %s\n" % sep.join(map(lambda s: hex(s)[2:], range(0,16)))) + printer._raw(" {}\n".format(sep.join(map(lambda s: hex(s)[2:], range(0, 16))))) printer.set() # The table - for x in range(0,16): + for x in range(0, 16): # First column printer.set(text_type='B') - printer._raw("%s " % hex(x)[2:]) + printer._raw("{} ".format(hex(x)[2:])) printer.set() for y in range(0,16): From c2fc464c5514206f52af89c1bb755b1845619267 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 00:10:14 +0100 Subject: [PATCH 64/84] reformat PEP8 and similar issues --- examples/codepage_tables.py | 7 ++++--- src/escpos/capabilities.py | 18 ++++++------------ src/escpos/codepages.py | 5 +++-- src/escpos/escpos.py | 16 +++++++++------- src/escpos/katakana.py | 3 +-- src/escpos/magicencode.py | 6 ++++-- test/test_cli.py | 2 +- test/test_function_barcode.py | 2 +- test/test_functions.py | 8 ++++---- test/test_magicencode.py | 1 - test/test_printer_file.py | 1 + test/test_profile.py | 2 +- 12 files changed, 35 insertions(+), 36 deletions(-) diff --git a/examples/codepage_tables.py b/examples/codepage_tables.py index e638b3c..7d5c008 100644 --- a/examples/codepage_tables.py +++ b/examples/codepage_tables.py @@ -46,8 +46,8 @@ def print_codepage(printer, codepage): printer._raw("{} ".format(hex(x)[2:])) printer.set() - for y in range(0,16): - byte = six.int2byte(x*16+y) + for y in range(0, 16): + byte = six.int2byte(x * 16 + y) if byte in (ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT): byte = ' ' @@ -56,5 +56,6 @@ def print_codepage(printer, codepage): printer._raw(sep) printer._raw('\n') + if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index a3bfd3a..3ff9e20 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -3,7 +3,6 @@ import six from os import path import yaml - # Load external printer database with open(path.join(path.dirname(__file__), 'capabilities.json')) as f: CAPABILITIES = yaml.load(f) @@ -11,7 +10,6 @@ with open(path.join(path.dirname(__file__), 'capabilities.json')) as f: PROFILES = CAPABILITIES['profiles'] - class NotSupported(Exception): """Raised if a requested feature is not suppored by the printer profile. @@ -61,7 +59,6 @@ class BaseProfile(object): 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 default profile. @@ -92,17 +89,18 @@ def get_profile_class(name): def clean(s): - # Remove invalid characters - s = re.sub('[^0-9a-zA-Z_]', '', s) - # Remove leading characters until we find a letter or underscore - s = re.sub('^[^a-zA-Z_]+', '', s) - return str(s) + # Remove invalid characters + s = re.sub('[^0-9a-zA-Z_]', '', s) + # Remove leading characters until we find a letter or underscore + s = re.sub('^[^a-zA-Z_]+', '', s) + return str(s) class Profile(get_profile_class('default')): """ For users, who want to provide their profile """ + def __init__(self, columns=None, features=None): super(Profile, self).__init__() @@ -114,7 +112,3 @@ class Profile(get_profile_class('default')): return self.columns return super(Profile, self).get_columns(font) - - - - diff --git a/src/escpos/codepages.py b/src/escpos/codepages.py index 05d5852..e22e1d0 100644 --- a/src/escpos/codepages.py +++ b/src/escpos/codepages.py @@ -12,11 +12,12 @@ class CodePageManager: def get_all(self): return self.data.values() - def get_encoding_name(self, encoding): + @staticmethod + def get_encoding_name(encoding): # TODO resolve the encoding alias return encoding.upper() def get_encoding(self, encoding): return self.data[encoding] -CodePages = CodePageManager(CAPABILITIES['encodings']) \ No newline at end of file +CodePages = CodePageManager(CAPABILITIES['encodings']) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 7fce107..85898a5 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -81,7 +81,7 @@ class Escpos(object): :param impl: choose image printing mode between `bitImageRaster`, `graphics` or `bitImageColumn` :param fragment_height: Images larger than this will be split into multiple fragments *default:* 960 - """ + """ im = EscposImage(img_source) if im.height > fragment_height: @@ -93,13 +93,14 @@ class Escpos(object): impl=impl, fragment_height=fragment_height) return - + if impl == "bitImageRaster": # GS v 0, raster format bit image density_byte = (0 if high_density_horizontal else 1) + (0 if high_density_vertical else 2) - header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) + self._int_low_high(im.height, 2) + header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) +\ + self._int_low_high(im.height, 2) self._raw(header + im.to_raster_format()) - + if impl == "graphics": # GS ( L raster format graphics img_header = self._int_low_high(im.width, 2) + self._int_low_high(im.height, 2) @@ -111,7 +112,7 @@ class Escpos(object): raster_data = im.to_raster_format() self._image_send_graphics_data(b'0', b'p', header + raster_data) self._image_send_graphics_data(b'0', b'2', b'') - + if impl == "bitImageColumn": # ESC *, column format bit image density_byte = (1 if high_density_horizontal else 0) + (32 if high_density_vertical else 0) @@ -198,7 +199,7 @@ class Escpos(object): raise ValueError("cn and fn must be one byte each.") header = self._int_low_high(len(data) + len(m) + 2, 2) self._raw(GS + b'(k' + header + cn + fn + m + data) - + @staticmethod def _int_low_high(inp_number, out_bytes): """ Generate multiple bytes for a number: In lower and higher parts, or more parts as needed. @@ -398,6 +399,7 @@ class Escpos(object): Text has to be encoded in unicode. :param txt: text to be printed + :param font: font to be used, can be :code:`a` or :code`b` :param columns: amount of columns :return: None """ @@ -539,7 +541,7 @@ class Escpos(object): if divisor not in LINESPACING_FUNCS: raise ValueError("divisor must be either 360, 180 or 60") - if (divisor in [360, 180] \ + if (divisor in [360, 180] and (not(0 <= spacing <= 255))): raise ValueError("spacing must be a int between 0 and 255 when divisor is 360 or 180") if divisor == 60 and (not(0 <= spacing <= 85)): diff --git a/src/escpos/katakana.py b/src/escpos/katakana.py index 14b2e61..927a59b 100644 --- a/src/escpos/katakana.py +++ b/src/escpos/katakana.py @@ -30,13 +30,12 @@ def encode_katakana(text): 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 + # TODO doesn't this discard all that is not in the map? Can we be sure 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? diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 3dbd256..0179110 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -67,7 +67,8 @@ class Encoder(object): ).format(encoding, ','.join(self.codepages.keys()))) return encoding - def _get_codepage_char_list(self, encoding): + @staticmethod + def _get_codepage_char_list(encoding): """Get codepage character list Gets characters 128-255 for a given code page, as an array. @@ -126,7 +127,8 @@ class Encoder(object): is_encodable = char in available_map return is_ascii or is_encodable - def _encode_char(self, char, charmap, defaultchar): + @staticmethod + def _encode_char(char, charmap, defaultchar): """ Encode a single character with the given encoding map :param char: char to encode diff --git a/test/test_cli.py b/test/test_cli.py index c9b2189..6b45036 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -30,7 +30,7 @@ printer: ) -class TestCLI(): +class TestCLI: """ Contains setups, teardowns, and tests for CLI """ diff --git a/test/test_function_barcode.py b/test/test_function_barcode.py index 2edd712..710e5cd 100644 --- a/test/test_function_barcode.py +++ b/test/test_function_barcode.py @@ -35,4 +35,4 @@ def test_lacks_support(bctype, supports_b): with pytest.raises(BarcodeTypeError): instance.barcode('test', bctype) - assert instance.output == b'' \ No newline at end of file + assert instance.output == b'' diff --git a/test/test_functions.py b/test/test_functions.py index 22ee737..96b923a 100644 --- a/test/test_functions.py +++ b/test/test_functions.py @@ -17,10 +17,10 @@ def test_line_spacing_rest(): def test_line_spacing_error_handling(): printer = Dummy() with assert_raises(ValueError): - printer.line_spacing(99, divisor=44) + printer.line_spacing(99, divisor=44) with assert_raises(ValueError): - printer.line_spacing(divisor=80, spacing=86) + printer.line_spacing(divisor=80, spacing=86) with assert_raises(ValueError): - printer.line_spacing(divisor=360, spacing=256) + printer.line_spacing(divisor=360, spacing=256) with assert_raises(ValueError): - printer.line_spacing(divisor=180, spacing=256) \ No newline at end of file + printer.line_spacing(divisor=180, spacing=256) diff --git a/test/test_magicencode.py b/test/test_magicencode.py index faaf66d..fe0edcf 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -22,7 +22,6 @@ from escpos.katakana import encode_katakana from escpos.exceptions import CharCodeError, Error - class TestEncoder: """ Tests the single encoders. diff --git a/test/test_printer_file.py b/test/test_printer_file.py index 4d1b188..d234de8 100644 --- a/test/test_printer_file.py +++ b/test/test_printer_file.py @@ -26,6 +26,7 @@ if six.PY3: else: mock_open_call = '__builtin__.open' + @given(path=text()) def test_load_file_printer(mocker, path): """test the loading of the file-printer""" diff --git a/test/test_profile.py b/test/test_profile.py index d75c88c..5f4fa4b 100644 --- a/test/test_profile.py +++ b/test/test_profile.py @@ -35,4 +35,4 @@ class TestCustomProfile: assert Profile(columns=10).get_columns('sdfasdf') == 10 def test_features(self): - assert Profile(features={'foo': True}).supports('foo') \ No newline at end of file + assert Profile(features={'foo': True}).supports('foo') From 972c7a2238d6630da9fc5ef99418917bf4d9d8f9 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 00:44:54 +0100 Subject: [PATCH 65/84] use explicit import from constants --- examples/codepage_tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/codepage_tables.py b/examples/codepage_tables.py index 7d5c008..13deb4b 100644 --- a/examples/codepage_tables.py +++ b/examples/codepage_tables.py @@ -5,7 +5,7 @@ import six import sys from escpos import printer -from escpos.constants import * +from escpos.constants import CODEPAGE_CHANGE, ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT def main(): From c48a0bee5119b58715febafbfa263332dfdc1252 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 00:50:58 +0100 Subject: [PATCH 66/84] use not in instead of not ... in --- src/escpos/capabilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index 3ff9e20..e4f1a18 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -77,7 +77,7 @@ def get_profile_class(name): """For the given profile name, load the data from the external database, then generate dynamically a class. """ - if not name in CLASS_CACHE: + if name not in CLASS_CACHE: profile_data = PROFILES[name] profile_name = clean(name) class_name = '{}{}Profile'.format( From 43e30707be638e6f07742d1397f4a47f39331d83 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 01:26:50 +0100 Subject: [PATCH 67/84] add flake8 config --- setup.cfg | 4 ++++ setup.py | 13 ++++++++++++- tox.ini | 7 ++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 654c47d..e8da255 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,3 +5,7 @@ with-doctest=1 [bdist_wheel] # This flag says that the code is written to work on both Python 2 and Python 3. universal=1 + +[flake8] +exclude = .git,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test +max-line-length = 100 \ No newline at end of file diff --git a/setup.py b/setup.py index 9d00261..7fe0c78 100755 --- a/setup.py +++ b/setup.py @@ -118,7 +118,18 @@ setup( setup_requires=[ 'setuptools_scm', ], - tests_require=['jaconv', 'tox', 'pytest', 'pytest-cov', 'pytest-mock', 'nose', 'scripttest', 'mock', 'hypothesis'], + tests_require=[ + 'jaconv', + 'tox', + 'pytest', + 'pytest-cov', + 'pytest-mock', + 'nose', + 'scripttest', + 'mock', + 'hypothesis', + 'flake8' + ], cmdclass={'test': Tox}, entry_points={ 'console_scripts': [ diff --git a/tox.ini b/tox.ini index 7c9e8ca..f2d6389 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py34, py35, docs +envlist = py27, py34, py35, docs, flake8 [testenv] deps = nose @@ -19,3 +19,8 @@ changedir = doc deps = sphinx>=1.5.1 setuptools_scm commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html + +[testenv:flake8] +basepython = python +deps = flake8 +commands = flake8 From b4920aafe26f338a464e58a82ddb18b633d31b0d Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 01:41:01 +0100 Subject: [PATCH 68/84] increase allowed line-length, fix whitespace in cli.py --- setup.cfg | 2 +- src/escpos/cli.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index e8da255..ecf3bdb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,4 +8,4 @@ universal=1 [flake8] exclude = .git,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test -max-line-length = 100 \ No newline at end of file +max-line-length = 120 diff --git a/src/escpos/cli.py b/src/escpos/cli.py index 3e3311d..62742dd 100644 --- a/src/escpos/cli.py +++ b/src/escpos/cli.py @@ -244,13 +244,13 @@ ESCPOS_COMMANDS = [ 'option_strings': ('--high_density_horizontal',), 'help': 'Image density (horizontal)', 'type': str_to_bool, - }, + }, { 'option_strings': ('--high_density_vertical',), 'help': 'Image density (vertical)', 'type': str_to_bool, - }, - + }, + ], }, { From fc69754a211efd0de2294d036901139f4478a436 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 01:42:34 +0100 Subject: [PATCH 69/84] constants.py is custom-formatted --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ecf3bdb..95c89af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,5 +7,5 @@ with-doctest=1 universal=1 [flake8] -exclude = .git,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test +exclude = .git,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test,src/escpos/constants.py max-line-length = 120 From ca880dd8ec0f80999827a2f29a5f09ff66965485 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 01:44:15 +0100 Subject: [PATCH 70/84] remove trailing whitespace in version.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 7fe0c78..adcac2a 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,6 @@ setuptools_scm_template = """\ from __future__ import unicode_literals version = '{version}' - """ From ce94a1fc189116547b3faf1562fdfecd6f9bb0ac Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 01:50:27 +0100 Subject: [PATCH 71/84] normalize whitespace --- src/escpos/escpos.py | 8 ++++---- src/escpos/image.py | 6 +++--- src/escpos/magicencode.py | 16 ++++++++-------- src/escpos/printer.py | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 85898a5..7db7950 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -126,7 +126,7 @@ class Escpos(object): def _image_send_graphics_data(self, m, fn, data): """ Wrapper for GS ( L, to calculate and send correct data length. - + :param m: Modifier//variant for function. Usually '0' :param fn: Function number to use, as byte :param data: Data to send @@ -203,7 +203,7 @@ class Escpos(object): @staticmethod def _int_low_high(inp_number, out_bytes): """ Generate multiple bytes for a number: In lower and higher parts, or more parts as needed. - + :param inp_number: Input number :param out_bytes: The number of bytes to output (1 - 4). """ @@ -259,7 +259,7 @@ class Escpos(object): .. todo:: If further barcode-types are needed they could be rendered transparently as an image. (This could also be of help if the printer does not support types that others do.) - + :param code: alphanumeric data to be printed as bar code :param bc: barcode format, possible values are for type A are: @@ -555,7 +555,7 @@ class Escpos(object): Without any arguments the paper will be cut completely. With 'mode=PART' a partial cut will be attempted. Note however, that not all models can do a partial cut. See the documentation of your printer for details. - + .. todo:: Check this function on TM-T88II. :param mode: set to 'PART' for a partial cut diff --git a/src/escpos/image.py b/src/escpos/image.py index abf2e22..5165e5a 100644 --- a/src/escpos/image.py +++ b/src/escpos/image.py @@ -28,7 +28,7 @@ class EscposImage(object): def __init__(self, img_source): """ Load in an image - + :param img_source: PIL.Image, or filename to load one from. """ if isinstance(img_source, Image.Image): @@ -45,12 +45,12 @@ class EscposImage(object): im = Image.new("RGB", img_original.size, (255, 255, 255)) im.paste(img_original, mask=img_original.split()[3]) # Convert down to greyscale - im = im.convert("L") + im = im.convert("L") # Invert: Only works on 'L' images im = ImageOps.invert(im) # Pure black and white self._im = im.convert("1") - + @property def width(self): """ diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 0179110..554f293 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -70,9 +70,9 @@ class Encoder(object): @staticmethod def _get_codepage_char_list(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) @@ -94,12 +94,12 @@ class Encoder(object): 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 @@ -112,7 +112,7 @@ class Encoder(object): 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. """ @@ -130,7 +130,7 @@ class Encoder(object): @staticmethod def _encode_char(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 """ @@ -142,7 +142,7 @@ class Encoder(object): 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 diff --git a/src/escpos/printer.py b/src/escpos/printer.py index 1fe9cf1..55e2555 100644 --- a/src/escpos/printer.py +++ b/src/escpos/printer.py @@ -34,7 +34,7 @@ class Usb(Escpos): """ - def __init__(self, idVendor, idProduct, timeout=0, in_ep=0x82, out_ep=0x01, *args, **kwargs): + def __init__(self, idVendor, idProduct, timeout=0, in_ep=0x82, out_ep=0x01, *args, **kwargs): # noqa: N803 """ :param idVendor: Vendor ID :param idProduct: Product ID From 94a0f2b94b1e3826eaad423cd35c20b72b843dc5 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 01:57:36 +0100 Subject: [PATCH 72/84] normalize whitespace --- src/escpos/escpos.py | 4 ++-- src/escpos/magicencode.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 7db7950..48c06d7 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -98,7 +98,7 @@ class Escpos(object): # GS v 0, raster format bit image density_byte = (0 if high_density_horizontal else 1) + (0 if high_density_vertical else 2) header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) +\ - self._int_low_high(im.height, 2) + self._int_low_high(im.height, 2) self._raw(header + im.to_raster_format()) if impl == "graphics": @@ -328,7 +328,7 @@ class Escpos(object): function_type = 'B' else: raise BarcodeTypeError(( - "Barcode type '{bc} is not valid").format(bc=bc)) + "Barcode type '{bc} is not valid").format(bc=bc)) bc_types = BARCODE_TYPES[function_type.upper()] if bc.upper() not in bc_types.keys(): diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 554f293..8842c75 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -62,8 +62,8 @@ class Encoder(object): 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: {}' + 'Encoding "{}" cannot be used for the current profile. ' + 'Valid encodings are: {}' ).format(encoding, ','.join(self.codepages.keys()))) return encoding From fb18bb34ccc42637fa710110a0ea8ed7d85cd175 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 02:15:40 +0100 Subject: [PATCH 73/84] normalize imports --- src/escpos/escpos.py | 19 +++++++++++++++++-- src/escpos/magicencode.py | 4 +--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 48c06d7..617ff0c 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -17,9 +17,24 @@ from __future__ import unicode_literals import qrcode import textwrap +import six + +from .constants import ESC, GS, NUL, QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q +from .constants import QR_MODEL_1, QR_MODEL_2, QR_MICRO, BARCODE_TYPES, BARCODE_HEIGHT, BARCODE_WIDTH +from .constants import TXT_ALIGN_CT, TXT_ALIGN_LT, TXT_ALIGN_RT, BARCODE_FONT_A, BARCODE_FONT_B +from .constants import BARCODE_TXT_OFF, BARCODE_TXT_BTH, BARCODE_TXT_ABV, BARCODE_TXT_BLW +from .constants import TXT_HEIGHT, TXT_WIDTH, TXT_SIZE, TXT_NORMAL, TXT_SMOOTH_OFF, TXT_SMOOTH_ON +from .constants import TXT_FLIP_OFF, TXT_FLIP_ON, TXT_2WIDTH, TXT_2HEIGHT, TXT_4SQUARE +from .constants import TXT_UNDERL_OFF, TXT_UNDERL_ON, TXT_BOLD_OFF, TXT_BOLD_ON, SET_FONT, TXT_UNDERL2_ON +from .constants import TXT_INVERT_OFF, TXT_INVERT_ON, LINESPACING_FUNCS, LINESPACING_RESET +from .constants import PD_0, PD_N12, PD_N25, PD_N37, PD_N50, PD_P50, PD_P37, PD_P25, PD_P12 +from .constants import CD_KICK_DEC_SEQUENCE, CD_KICK_5, CD_KICK_2, PAPER_FULL_CUT, PAPER_PART_CUT +from .constants import HW_RESET, HW_SELECT, HW_INIT +from .constants import CTL_VT, CTL_HT, CTL_CR, CTL_FF, CTL_LF, CTL_SET_HT, PANEL_BUTTON_OFF, PANEL_BUTTON_ON + +from .exceptions import BarcodeTypeError, BarcodeSizeError, TabPosError +from .exceptions import CashDrawerError, SetVariableError, BarcodeCodeError -from .constants import * -from .exceptions import * from .magicencode import MagicEncode from abc import ABCMeta, abstractmethod # abstract base class support diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 8842c75..9c71ea0 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -19,10 +19,8 @@ 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 .exceptions import Error from .codepages import CodePages -import copy import six From 81028f9a352d2de4186a9bcc46fac0bd002d3536 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 02:16:22 +0100 Subject: [PATCH 74/84] refactor not ... in to ... not in ... --- src/escpos/magicencode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index 9c71ea0..a989118 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -58,7 +58,7 @@ class Encoder(object): TODO: Support encoding aliases: pc437 instead of cp437. """ encoding = CodePages.get_encoding_name(encoding) - if not encoding in self.codepages: + if encoding not in self.codepages: raise ValueError(( 'Encoding "{}" cannot be used for the current profile. ' 'Valid encodings are: {}' From 337e8ee19e7f2108c222dfb7e593aa1c8191de3a Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 02:29:08 +0100 Subject: [PATCH 75/84] add flake8-checks to travis --- .travis.yml | 2 ++ setup.cfg | 1 + setup.py | 3 +++ src/escpos/cli.py | 3 +++ src/escpos/codepages.py | 1 + tox.ini | 1 + 6 files changed, 11 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0932316..78fa8ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,8 @@ matrix: env: TOXENV=pypy3 - python: 2.7 env: TOXENV=docs + - python: 3.4 + env: TOXENV=flake8 allow_failures: - python: 3.5-dev - python: nightly diff --git a/setup.cfg b/setup.cfg index 95c89af..ea43f22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,3 +9,4 @@ universal=1 [flake8] exclude = .git,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test,src/escpos/constants.py max-line-length = 120 +# future-imports = absolute_import, division, print_function, unicode_literals # we are not there yet diff --git a/setup.py b/setup.py index adcac2a..c699c5a 100755 --- a/setup.py +++ b/setup.py @@ -50,6 +50,9 @@ setuptools_scm_template = """\ # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function from __future__ import unicode_literals version = '{version}' diff --git a/src/escpos/cli.py b/src/escpos/cli.py index 62742dd..dad2dfb 100644 --- a/src/escpos/cli.py +++ b/src/escpos/cli.py @@ -33,9 +33,11 @@ def str_to_bool(string): """ return string.lower() in ('y', 'yes', '1', 'true') + # A list of functions that work better with a newline to be sent after them. REQUIRES_NEWLINE = ('qr', 'barcode', 'text', 'block_text') + # Used in demo method # Key: The name of escpos function and the argument passed on the CLI. Some # manual translation is done in the case of barcodes_a -> barcode. @@ -568,5 +570,6 @@ def demo(printer, **kwargs): command(**params) printer.cut() + if __name__ == '__main__': main() diff --git a/src/escpos/codepages.py b/src/escpos/codepages.py index e22e1d0..ca63ef3 100644 --- a/src/escpos/codepages.py +++ b/src/escpos/codepages.py @@ -20,4 +20,5 @@ class CodePageManager: def get_encoding(self, encoding): return self.data[encoding] + CodePages = CodePageManager(CAPABILITIES['encodings']) diff --git a/tox.ini b/tox.ini index f2d6389..5dc5d64 100644 --- a/tox.ini +++ b/tox.ini @@ -22,5 +22,6 @@ commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html [testenv:flake8] basepython = python +# TODO add flake8-future deps = flake8 commands = flake8 From fc5ad1673ce15c9cae0b9b396577cb9c681c8aa5 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 02:44:26 +0100 Subject: [PATCH 76/84] add notice that we could add flake8-docstrings once --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 5dc5d64..d51853c 100644 --- a/tox.ini +++ b/tox.ini @@ -23,5 +23,6 @@ commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html [testenv:flake8] basepython = python # TODO add flake8-future +# TODO add flake8-docstrings deps = flake8 commands = flake8 From f885de2f2aa5cdd75880ebb7630b76816fe7fddb Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 02:56:15 +0100 Subject: [PATCH 77/84] switch to python2 with flake8 for now --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 78fa8ad..d9139e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,8 @@ matrix: env: TOXENV=pypy3 - python: 2.7 env: TOXENV=docs - - python: 3.4 + # TODO add flake8 also for python3 + - python: 2.7 env: TOXENV=flake8 allow_failures: - python: 3.5-dev From 1579f05cb72fd348a9d0de32fe9017eb54fe0f5f Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 03:00:23 +0100 Subject: [PATCH 78/84] use print function in example --- examples/codepage_tables.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/codepage_tables.py b/examples/codepage_tables.py index 13deb4b..7966036 100644 --- a/examples/codepage_tables.py +++ b/examples/codepage_tables.py @@ -1,6 +1,8 @@ """Prints code page tables. """ +from __future__ import print_function + import six import sys @@ -21,7 +23,7 @@ def main(): dummy.cut() - print dummy.output + print(dummy.output) def print_codepage(printer, codepage): From 1038844567c083052bf3c2ba6f8825eda63cd543 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 03:01:32 +0100 Subject: [PATCH 79/84] run flake8 in both python2 and python3 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d9139e4..37baaf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,9 +25,10 @@ matrix: env: TOXENV=pypy3 - python: 2.7 env: TOXENV=docs - # TODO add flake8 also for python3 - python: 2.7 env: TOXENV=flake8 + - python: 3.4 + env: TOXENV=flake8 allow_failures: - python: 3.5-dev - python: nightly From e92f00cdf34226aaafa495b1a925373b7b0a0e83 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 16:12:37 +0100 Subject: [PATCH 80/84] fix AttributeError when executing close When self.device is already None an AttributeError will occur. This is relevant to #189. --- src/escpos/printer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/escpos/printer.py b/src/escpos/printer.py index 55e2555..ab1021b 100644 --- a/src/escpos/printer.py +++ b/src/escpos/printer.py @@ -211,8 +211,9 @@ class Network(Escpos): def close(self): """ Close TCP connection """ - self.device.shutdown(socket.SHUT_RDWR) - self.device.close() + if self.device is not None: + self.device.shutdown(socket.SHUT_RDWR) + self.device.close() class File(Escpos): @@ -263,8 +264,9 @@ class File(Escpos): def close(self): """ Close system file """ - self.device.flush() - self.device.close() + if self.device is not None: + self.device.flush() + self.device.close() class Dummy(Escpos): From 2bcb1766aeff709c4786f15d07baf18022e03daf Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 31 Jan 2017 01:33:28 +0100 Subject: [PATCH 81/84] update python versions for travis this "skips" py36 since it has not been released --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 37baaf2..fa13a70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,10 +15,10 @@ matrix: env: TOXENV=py34 - python: 3.5 env: TOXENV=py35 - - python: 3.5-dev - env: TOXENV=py35 - - python: nightly + - python: 3.6-dev env: TOXENV=py36 + - python: nightly + env: TOXENV=py37 - python: pypy env: TOXENV=pypy - python: pypy3 @@ -27,10 +27,10 @@ matrix: env: TOXENV=docs - python: 2.7 env: TOXENV=flake8 - - python: 3.4 + - python: 3.5 env: TOXENV=flake8 allow_failures: - - python: 3.5-dev + - python: 3.6-dev - python: nightly - python: pypy3 before_install: From b76fb757024ef06f25fae4d1931d87bc34f281c3 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 31 Jan 2017 03:29:14 +0100 Subject: [PATCH 82/84] add python36 to travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index fa13a70..3966d74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ matrix: env: TOXENV=py34 - python: 3.5 env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 - python: 3.6-dev env: TOXENV=py36 - python: nightly From 3ee787e8b1fa50f99813d63ef05ce6e5cdcda67d Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 30 Jan 2017 00:39:43 +0100 Subject: [PATCH 83/84] change license to MIT --- COPYING | 674 ---------------------------- LICENSE | 21 + MANIFEST.in | 1 + setup.py | 4 +- src/escpos/constants.py | 4 +- src/escpos/escpos.py | 4 +- src/escpos/exceptions.py | 4 +- src/escpos/image.py | 2 +- src/escpos/magicencode.py | 2 +- src/escpos/printer.py | 4 +- test/test_abstract_base_class.py | 2 +- test/test_function_image.py | 2 +- test/test_function_panel_button.py | 2 +- test/test_function_qr_native.py | 2 +- test/test_function_qr_non-native.py | 2 +- test/test_function_text.py | 2 +- test/test_image.py | 2 +- test/test_load_module.py | 2 +- test/test_magicencode.py | 2 +- test/test_printer_file.py | 2 +- test/test_with_statement.py | 2 +- 21 files changed, 45 insertions(+), 697 deletions(-) delete mode 100644 COPYING create mode 100644 LICENSE diff --git a/COPYING b/COPYING deleted file mode 100644 index 94a9ed0..0000000 --- a/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3b78da5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2012-2017 python-escpos and Manuel F Martinez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index eeed97e..3b066a0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include *.rst include *.txt include COPYING +include LICENSE include INSTALL include tox.ini include capabilities-data/dist/capabilities.json diff --git a/setup.py b/setup.py index c699c5a..c5d77f6 100755 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup( download_url='https://github.com/python-escpos/python-escpos/archive/master.zip', description='Python library to manipulate ESC/POS Printers', bugtrack_url='https://github.com/python-escpos/python-escpos/issues', - license='GNU GPL v3', + license='MIT', long_description=read('README.rst'), author='Manuel F Martinez and others', author_email='manpaz@bashlinux.com', @@ -91,7 +91,7 @@ setup( 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', diff --git a/src/escpos/constants.py b/src/escpos/constants.py index b5cce39..e3225f8 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -7,8 +7,8 @@ moved to `capabilities` as in `escpos-php by @mike42 `_ and others :organization: Bashlinux and `python-escpos `_ -:copyright: Copyright (c) 2012 Bashlinux -:license: GNU GPL v3 +:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos +:license: MIT """ from __future__ import absolute_import diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 617ff0c..3e07fc0 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -6,8 +6,8 @@ This module contains the abstract base class :py:class:`Escpos`. :author: `Manuel F Martinez `_ and others :organization: Bashlinux and `python-escpos `_ -:copyright: Copyright (c) 2012 Bashlinux -:license: GNU GPL v3 +:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos +:license: MIT """ from __future__ import absolute_import diff --git a/src/escpos/exceptions.py b/src/escpos/exceptions.py index 0f9f058..8c574ff 100644 --- a/src/escpos/exceptions.py +++ b/src/escpos/exceptions.py @@ -20,8 +20,8 @@ Result/Exit codes: :author: `Manuel F Martinez `_ and others :organization: Bashlinux and `python-escpos `_ -:copyright: Copyright (c) 2012 Bashlinux -:license: GNU GPL v3 +:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos +:license: MIT """ from __future__ import absolute_import diff --git a/src/escpos/image.py b/src/escpos/image.py index 5165e5a..a5b15ab 100644 --- a/src/escpos/image.py +++ b/src/escpos/image.py @@ -5,7 +5,7 @@ This module contains the image format handler :py:class:`EscposImage`. :author: `Michael Billington `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 Michael Billington -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/src/escpos/magicencode.py b/src/escpos/magicencode.py index a989118..c87b20c 100644 --- a/src/escpos/magicencode.py +++ b/src/escpos/magicencode.py @@ -9,7 +9,7 @@ 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 +:license: MIT """ from __future__ import absolute_import diff --git a/src/escpos/printer.py b/src/escpos/printer.py index ab1021b..d14b93d 100644 --- a/src/escpos/printer.py +++ b/src/escpos/printer.py @@ -4,8 +4,8 @@ :author: `Manuel F Martinez `_ and others :organization: Bashlinux and `python-escpos `_ -:copyright: Copyright (c) 2012 Bashlinux -:license: GNU GPL v3 +:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_abstract_base_class.py b/test/test_abstract_base_class.py index f5ec61f..9450e1b 100644 --- a/test/test_abstract_base_class.py +++ b/test/test_abstract_base_class.py @@ -4,7 +4,7 @@ :author: `Patrick Kanzler `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 Patrick Kanzler -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_function_image.py b/test/test_function_image.py index b1762f6..27b4fd7 100644 --- a/test/test_function_image.py +++ b/test/test_function_image.py @@ -4,7 +4,7 @@ :author: `Michael Billington `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `Michael Billington `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_function_panel_button.py b/test/test_function_panel_button.py index 403bc75..1006e5b 100644 --- a/test/test_function_panel_button.py +++ b/test/test_function_panel_button.py @@ -4,7 +4,7 @@ :author: `Patrick Kanzler `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `python-escpos `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_function_qr_native.py b/test/test_function_qr_native.py index a3355ca..2fbaa64 100644 --- a/test/test_function_qr_native.py +++ b/test/test_function_qr_native.py @@ -4,7 +4,7 @@ :author: `Michael Billington `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `Michael Billington `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_function_qr_non-native.py b/test/test_function_qr_non-native.py index 5c0566a..bba47fd 100644 --- a/test/test_function_qr_non-native.py +++ b/test/test_function_qr_non-native.py @@ -5,7 +5,7 @@ :author: `Patrick Kanzler `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `python-escpos `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_function_text.py b/test/test_function_text.py index 7f3869c..04222a1 100644 --- a/test/test_function_text.py +++ b/test/test_function_text.py @@ -4,7 +4,7 @@ :author: `Patrick Kanzler `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `python-escpos `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_image.py b/test/test_image.py index 6fda6dd..4a9e8b0 100644 --- a/test/test_image.py +++ b/test/test_image.py @@ -5,7 +5,7 @@ converted to ESC/POS column & raster formats. :author: `Michael Billington `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `Michael Billington `_ -:license: GNU GPL v3 +:license: MIT """ from escpos.image import EscposImage diff --git a/test/test_load_module.py b/test/test_load_module.py index daf94b4..aeffc5b 100644 --- a/test/test_load_module.py +++ b/test/test_load_module.py @@ -4,7 +4,7 @@ :author: `Patrick Kanzler `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `python-escpos `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_magicencode.py b/test/test_magicencode.py index fe0edcf..bb2dcde 100644 --- a/test/test_magicencode.py +++ b/test/test_magicencode.py @@ -5,7 +5,7 @@ :author: `Patrick Kanzler `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `python-escpos `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_printer_file.py b/test/test_printer_file.py index d234de8..48b7606 100644 --- a/test/test_printer_file.py +++ b/test/test_printer_file.py @@ -5,7 +5,7 @@ :author: `Patrick Kanzler `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `python-escpos `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import diff --git a/test/test_with_statement.py b/test/test_with_statement.py index 71efc24..4abb283 100644 --- a/test/test_with_statement.py +++ b/test/test_with_statement.py @@ -4,7 +4,7 @@ :author: `Patrick Kanzler `_ :organization: `python-escpos `_ :copyright: Copyright (c) 2016 `python-escpos `_ -:license: GNU GPL v3 +:license: MIT """ from __future__ import absolute_import From e8aefd8388ef3b8cc494bfcf6aa0a8b223a792ab Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Tue, 31 Jan 2017 04:23:26 +0100 Subject: [PATCH 84/84] update CHANGELOG --- CHANGELOG.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c16632..d5a23a3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,13 +2,14 @@ Changelog ********* -2017-01-29 - Version 3.0a - "Grey Area" +2017-01-31 - Version 3.0a - "Grey Area" --------------------------------------- This release is the first alpha release of the new version 3.0. Please be aware that the API will still change until v3.0 is released. changes ^^^^^^^ +- change the project's license to MIT in accordance with the contributors (see python-escpos/python-escpos#171) - feature: add "capabilities" which are shared with escpos-php, capabilities are stored in `escpos-printer-db `_ - feature: the driver tries now to guess the appropriate codepage and sets it automatically (called "magic encode") @@ -18,6 +19,7 @@ changes - changed interface of method `blocktext`, changed behavior of multiple methods, for details refer to the documentation on `python-escpos.readthedocs.io `_ - add support for custom cash drawer sequence +- enforce flake8 on the src-files, test py36 and py37 on travis contributors ^^^^^^^^^^^^