Merge pull request #170 from miracle2k/text-encoding

Text encoding
This commit is contained in:
Patrick Kanzler 2016-09-29 19:23:54 +02:00 committed by GitHub
commit cd38cdf74e
16 changed files with 636 additions and 144 deletions

View File

@ -1,6 +1,20 @@
********* *********
Changelog Changelog
********* *********
2016-08-?? - Version 2.?.? - "?"
------------------------------------------------
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-26 - Version 2.2.0 - "Fate Amenable To Change" 2016-08-26 - Version 2.2.0 - "Fate Amenable To Change"
------------------------------------------------------ ------------------------------------------------------

View File

@ -177,6 +177,20 @@ And for a network printer::
host: 127.0.0.1 host: 127.0.0.1
port: 9000 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 Advanced Usage: Print from binary blob
-------------------------------------- --------------------------------------
@ -235,19 +249,3 @@ You could then for example print the code from another process than your main-pr
(Of course this will not make the printer print faster.) (Of course this will not make the printer print faster.)
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.

View File

@ -113,11 +113,12 @@ setup(
'pyyaml', 'pyyaml',
'argparse', 'argparse',
'argcomplete', 'argcomplete',
'future'
], ],
setup_requires=[ setup_requires=[
'setuptools_scm', 'setuptools_scm',
], ],
tests_require=['tox', 'pytest', 'pytest-cov', 'nose', 'scripttest', 'mock', 'hypothesis'], tests_require=['jaconv', 'tox', 'pytest', 'pytest-cov', 'pytest-mock', 'nose', 'scripttest', 'mock', 'hypothesis'],
cmdclass={'test': Tox}, cmdclass={'test': Tox},
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [

View File

@ -7,8 +7,9 @@ import yaml
# Load external printer database # Load external printer database
with open(path.join(path.dirname(__file__), 'capabilities.json')) as f: with open(path.join(path.dirname(__file__), 'capabilities.json')) as f:
CAPABILITIES = yaml.load(f) CAPABILITIES = yaml.load(f)
PROFILES = CAPABILITIES['profiles'] PROFILES = CAPABILITIES['profiles']
ENCODINGS = CAPABILITIES['encodings']
class NotSupported(Exception): class NotSupported(Exception):
@ -54,6 +55,12 @@ class BaseProfile(object):
""" """
return self.features.get(feature) return self.features.get(feature)
def get_code_pages(self):
"""Return the support code pages as a {name: index} dict.
"""
return {v: k for k, v in self.codePages.items()}
def get_profile(name=None, **kwargs): def get_profile(name=None, **kwargs):
"""Get the profile by name; if no name is given, return the """Get the profile by name; if no name is given, return the

22
src/escpos/codepages.py Normal file
View File

@ -0,0 +1,22 @@
from .capabilities import CAPABILITIES
class CodePageManager:
"""Holds information about all the code pages (as defined
in escpos-printer-db).
"""
def __init__(self, data):
self.data = data
def get_all(self):
return self.data.values()
def get_encoding_name(self, encoding):
# TODO resolve the encoding alias
return encoding.upper()
def get_encoding(self, encoding):
return self.data[encoding]
CodePages = CodePageManager(CAPABILITIES['encodings'])

View File

@ -124,27 +124,9 @@ LINESPACING_FUNCS = {
180: ESC + b'3', # line_spacing/180 of an inch, 0 <= line_spacing <= 255 180: ESC + b'3', # line_spacing/180 of an inch, 0 <= line_spacing <= 255
} }
# Char code table # Prefix to change the codepage. You need to attach a byte to indicate
CHARCODE_PC437 = ESC + b'\x74\x00' # USA: Standard Europe # the codepage to use. We use escpos-printer-db as the data source.
CHARCODE_JIS = ESC + b'\x74\x01' # Japanese Katakana CODEPAGE_CHANGE = ESC + b'\x74'
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
# Barcode format # Barcode format
_SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n _SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n

View File

@ -20,6 +20,7 @@ import textwrap
from .constants import * from .constants import *
from .exceptions import * from .exceptions import *
from .magicencode import MagicEncode
from abc import ABCMeta, abstractmethod # abstract base class support from abc import ABCMeta, abstractmethod # abstract base class support
from escpos.image import EscposImage from escpos.image import EscposImage
@ -34,13 +35,13 @@ class Escpos(object):
class. class.
""" """
device = None device = None
codepage = None
def __init__(self, profile=None): def __init__(self, profile=None, magic_encode_args=None, **kwargs):
""" Initialize ESCPOS Printer """ Initialize ESCPOS Printer
:param profile: Printer profile""" :param profile: Printer profile"""
self.profile = get_profile(profile) self.profile = get_profile(profile)
self.magic = MagicEncode(self, **(magic_encode_args or {}))
def __del__(self): def __del__(self):
""" call self.close upon deletion """ """ call self.close upon deletion """
@ -216,82 +217,20 @@ class Escpos(object):
inp_number //= 256 inp_number //= 256
return outp return outp
def charcode(self, code): def charcode(self, code="AUTO"):
""" Set Character Code Table """ Set Character Code Table
Sends the control sequence from :py:mod:`escpos.constants` to the printer Sets the control sequence from ``CHARCODE`` in :py:mod:`escpos.constants` as active. It will be sent with
with :py:meth:`escpos.printer.'implementation'._raw()`. 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 :param code: Name of CharCode
:raises: :py:exc:`~escpos.exceptions.CharCodeError` :raises: :py:exc:`~escpos.exceptions.CharCodeError`
""" """
# TODO improve this (rather unhandy code) if code.upper() == "AUTO":
# TODO check the codepages self.magic.force_encoding(False)
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'
else: else:
raise CharCodeError() self.magic.force_encoding(code)
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A",
align_ct=True, function_type=None): align_ct=True, function_type=None):
@ -450,14 +389,8 @@ class Escpos(object):
:param txt: text to be printed :param txt: text to be printed
:raises: :py:exc:`~escpos.exceptions.TextError` :raises: :py:exc:`~escpos.exceptions.TextError`
""" """
if txt: txt = six.text_type(txt)
if self.codepage: self.magic.write(txt)
self._raw(txt.encode(self.codepage))
else:
self._raw(txt.encode())
else:
# TODO: why is it problematic to print an empty string?
raise TextError()
def block_text(self, txt, font=None, columns=None): def block_text(self, txt, font=None, columns=None):
""" Text is printed wrapped to specified columns """ Text is printed wrapped to specified columns

View File

@ -87,7 +87,7 @@ class BarcodeCodeError(Error):
self.resultcode = 30 self.resultcode = 30
def __str__(self): def __str__(self):
return "No Barcode code was supplied" return "No Barcode code was supplied ({msg})".format(msg=self.msg)
class ImageSizeError(Error): class ImageSizeError(Error):
@ -101,7 +101,7 @@ class ImageSizeError(Error):
self.resultcode = 40 self.resultcode = 40
def __str__(self): 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): class TextError(Error):
@ -116,7 +116,7 @@ class TextError(Error):
self.resultcode = 50 self.resultcode = 50
def __str__(self): 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): class CashDrawerError(Error):
@ -131,7 +131,7 @@ class CashDrawerError(Error):
self.resultcode = 60 self.resultcode = 60
def __str__(self): 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): class TabPosError(Error):
@ -146,7 +146,7 @@ class TabPosError(Error):
self.resultcode = 70 self.resultcode = 70
def __str__(self): 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): class CharCodeError(Error):
@ -161,7 +161,7 @@ class CharCodeError(Error):
self.resultcode = 80 self.resultcode = 80
def __str__(self): 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): class USBNotFoundError(Error):
@ -176,7 +176,7 @@ class USBNotFoundError(Error):
self.resultcode = 90 self.resultcode = 90
def __str__(self): def __str__(self):
return "USB device not found" return "USB device not found ({msg})".format(msg=self.msg)
class SetVariableError(Error): class SetVariableError(Error):
@ -191,7 +191,7 @@ class SetVariableError(Error):
self.resultcode = 100 self.resultcode = 100
def __str__(self): def __str__(self):
return "Set variable out of range" return "Set variable out of range ({msg})".format(msg=self.msg)
# Configuration errors # Configuration errors

108
src/escpos/katakana.py Normal file
View File

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
"""Helpers to encode Japanese characters.
I doubt that this currently works correctly.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
try:
import jaconv
except ImportError:
jaconv = None
def encode_katakana(text):
"""I don't think this quite works yet."""
encoded = []
for char in text:
if jaconv:
# try to convert japanese text to half-katakanas
char = jaconv.z2h(jaconv.hira2kata(char))
# TODO: "the conversion may result in multiple characters"
# If that really can happen (I am not really shure), than the string would have to be split and every single
# character has to passed through the following lines.
if char in TXT_ENC_KATAKANA_MAP:
encoded.append(TXT_ENC_KATAKANA_MAP[char])
else:
#TODO doesn't this discard all that is not in the map? Can we be shure that the input does contain only
# encodable characters? We could at least throw an exception if encoding is not possible.
pass
return b"".join(encoded)
TXT_ENC_KATAKANA_MAP = {
# Maps UTF-8 Katakana symbols to KATAKANA Page Codes
# TODO: has this really to be hardcoded?
# Half-Width Katakanas
'': b'\xa1',
'': b'\xa2',
'': b'\xa3',
'': b'\xa4',
'': b'\xa5',
'': b'\xa6',
'': b'\xa7',
'': b'\xa8',
'': b'\xa9',
'': b'\xaa',
'': b'\xab',
'': b'\xac',
'': b'\xad',
'': b'\xae',
'': b'\xaf',
'': b'\xb0',
'': b'\xb1',
'': b'\xb2',
'': b'\xb3',
'': b'\xb4',
'': b'\xb5',
'': b'\xb6',
'': b'\xb7',
'': b'\xb8',
'': b'\xb9',
'': b'\xba',
'': b'\xbb',
'': b'\xbc',
'': b'\xbd',
'': b'\xbe',
'ソ': b'\xbf',
'': b'\xc0',
'': b'\xc1',
'': b'\xc2',
'': b'\xc3',
'': b'\xc4',
'': b'\xc5',
'': b'\xc6',
'': b'\xc7',
'': b'\xc8',
'': b'\xc9',
'': b'\xca',
'': b'\xcb',
'': b'\xcc',
'': b'\xcd',
'': b'\xce',
'': b'\xcf',
'': b'\xd0',
'': b'\xd1',
'': b'\xd2',
'': b'\xd3',
'': b'\xd4',
'': b'\xd5',
'': b'\xd6',
'': b'\xd7',
'': b'\xd8',
'': b'\xd9',
'': b'\xda',
'': b'\xdb',
'': b'\xdc',
'': b'\xdd',
'': b'\xde',
'': b'\xdf',
}

290
src/escpos/magicencode.py Normal file
View File

@ -0,0 +1,290 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
""" Magic Encode
This module tries to convert an UTF-8 string to an encoded string for the printer.
It uses trial and error in order to guess the right codepage.
The code is based on the encoding-code in py-xml-escpos by @fvdsn.
:author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 Patrick Kanzler and Frédéric van der Essen
:license: GNU GPL v3
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from builtins import bytes
from .constants import CODEPAGE_CHANGE
from .exceptions import CharCodeError, Error
from .capabilities import get_profile
from .codepages import CodePages
import copy
import six
class Encoder(object):
"""Takes a list of available code spaces. Picks the right one for a
given character.
Note: To determine the code page, it needs to do the conversion, and
thus already knows what the final byte in the target encoding would
be. Nevertheless, the API of this class doesn't return the byte.
The caller use to do the character conversion itself.
$ python -m timeit -s "{u'ö':'a'}.get(u'ö')"
100000000 loops, best of 3: 0.0133 usec per loop
$ python -m timeit -s "u'ö'.encode('latin1')"
100000000 loops, best of 3: 0.0141 usec per loop
"""
def __init__(self, codepage_map):
self.codepages = codepage_map
self.available_encodings = set(codepage_map.keys())
self.available_characters = {}
self.used_encodings = set()
def get_sequence(self, encoding):
return int(self.codepages[encoding])
def get_encoding_name(self, encoding):
"""Given an encoding provided by the user, will return a
canonical encoding name; and also validate that the encoding
is supported.
TODO: Support encoding aliases: pc437 instead of cp437.
"""
encoding = CodePages.get_encoding_name(encoding)
if not encoding in self.codepages:
raise ValueError((
'Encoding "{}" cannot be used for the current profile. '
'Valid encodings are: {}'
).format(encoding, ','.join(self.codepages.keys())))
return encoding
def _get_codepage_char_list(self, encoding):
"""Get codepage character list
Gets characters 128-255 for a given code page, as an array.
:param encoding: The name of the encoding. This must appear in the CodePage list
"""
codepage = CodePages.get_encoding(encoding)
if 'data' in codepage:
encodable_chars = list("".join(codepage['data']))
assert(len(encodable_chars) == 128)
return encodable_chars
elif 'python_encode' in codepage:
encodable_chars = [u" "] * 128
for i in range(0, 128):
codepoint = i + 128
try:
encodable_chars[i] = bytes([codepoint]).decode(codepage['python_encode'])
except UnicodeDecodeError:
# Non-encodable character, just skip it
pass
return encodable_chars
raise LookupError("Can't find a known encoding for {}".format(encoding))
def _get_codepage_char_map(self, encoding):
""" Get codepage character map
Process an encoding and return a map of UTF-characters to code points
in this encoding.
This is generated once only, and returned from a cache.
:param encoding: The name of the encoding.
"""
# Skip things that were loaded previously
if encoding in self.available_characters:
return self.available_characters[encoding]
codepage_char_list = self._get_codepage_char_list(encoding)
codepage_char_map = dict((utf8, i + 128) for (i, utf8) in enumerate(codepage_char_list))
self.available_characters[encoding] = codepage_char_map
return codepage_char_map
def can_encode(self, encoding, char):
"""Determine if a character is encodeable in the given code page.
:param encoding: The name of the encoding.
:param char: The character to attempt to encode.
"""
available_map = {}
try:
available_map = self._get_codepage_char_map(encoding)
except LookupError:
return False
# Decide whether this character is encodeable in this code page
is_ascii = ord(char) < 128
is_encodable = char in available_map
return is_ascii or is_encodable
def _encode_char(self, char, charmap, defaultchar):
""" Encode a single character with the given encoding map
:param char: char to encode
:param charmap: dictionary for mapping characters in this code page
"""
if ord(char) < 128:
return ord(char)
if char in charmap:
return charmap[char]
return ord(defaultchar)
def encode(self, text, encoding, defaultchar='?'):
""" Encode text under the given encoding
:param text: Text to encode
:param encoding: Encoding name to use (must be defined in capabilities)
:param defaultchar: Fallback for non-encodable characters
"""
codepage_char_map = self._get_codepage_char_map(encoding)
output_bytes = bytes([self._encode_char(char, codepage_char_map, defaultchar) for char in text])
return output_bytes
def __encoding_sort_func(self, item):
key, index = item
return (
key in self.used_encodings,
index
)
def find_suitable_encoding(self, char):
"""The order of our search is a specific one:
1. code pages that we already tried before; there is a good
chance they might work again, reducing the search space,
and by re-using already used encodings we might also
reduce the number of codepage change instructiosn we have
to send. Still, any performance gains will presumably be
fairly minor.
2. code pages in lower ESCPOS slots first. Presumably, they
are more likely to be supported, so if a printer profile
is missing or incomplete, we might increase our change
that the code page we pick for this character is actually
supported.
"""
sorted_encodings = sorted(
self.codepages.items(),
key=self.__encoding_sort_func)
for encoding, _ in sorted_encodings:
if self.can_encode(encoding, char):
# This encoding worked; at it to the set of used ones.
self.used_encodings.add(encoding)
return encoding
def split_writable_text(encoder, text, encoding):
"""Splits off as many characters from the begnning of text as
are writable with "encoding". Returns a 2-tuple (writable, rest).
"""
if not encoding:
return None, text
for idx, char in enumerate(text):
if encoder.can_encode(encoding, char):
continue
return text[:idx], text[idx:]
return text, None
class MagicEncode(object):
"""A helper that helps us to automatically switch to the right
code page to encode any given Unicode character.
This will consider the printers supported codepages, according
to the printer profile, and if a character cannot be encoded
with the current profile, it will attempt to find a suitable one.
If the printer does not support a suitable code page, it can
insert an error character.
:param encoding: If you know the current encoding of the printer
when initializing this class, set it here. If the current
encoding is unknown, the first character emitted will be a
codepage switch.
"""
def __init__(self, driver, encoding=None, disabled=False,
defaultsymbol='?', encoder=None):
if disabled and not encoding:
raise Error('If you disable magic encode, you need to define an encoding!')
self.driver = driver
self.encoder = encoder or Encoder(driver.profile.get_code_pages())
self.encoding = self.encoder.get_encoding_name(encoding) if encoding else None
self.defaultsymbol = defaultsymbol
self.disabled = disabled
def force_encoding(self, encoding):
"""Sets a fixed encoding. The change is emitted right away.
From now one, this buffer will switch the code page anymore.
However, it will still keep track of the current code page.
"""
if not encoding:
self.disabled = False
else:
self.write_with_encoding(encoding, None)
self.disabled = True
def write(self, text):
"""Write the text, automatically switching encodings.
"""
if self.disabled:
self.write_with_encoding(self.encoding, text)
return
# See how far we can go into the text with the current encoding
to_write, text = split_writable_text(self.encoder, text, self.encoding)
if to_write:
self.write_with_encoding(self.encoding, to_write)
while text:
# See if any of the code pages that the printer profile
# supports can encode this character.
encoding = self.encoder.find_suitable_encoding(text[0])
if not encoding:
self._handle_character_failed(text[0])
text = text[1:]
continue
# Write as much text as possible with the encoding found.
to_write, text = split_writable_text(self.encoder, text, encoding)
if to_write:
self.write_with_encoding(encoding, to_write)
def _handle_character_failed(self, char):
"""Called when no codepage was found to render a character.
"""
# Writing the default symbol via write() allows us to avoid
# unnecesary codepage switches.
self.write(self.defaultsymbol)
def write_with_encoding(self, encoding, text):
if text is not None and type(text) is not six.text_type:
raise Error("The supplied text has to be unicode, but is of type {type}.".format(
type=type(text)
))
# We always know the current code page; if the new codepage
# is different, emit a change command.
if encoding != self.encoding:
self.encoding = encoding
self.driver._raw(
CODEPAGE_CHANGE +
six.int2byte(self.encoder.get_sequence(encoding)))
if text:
self.driver._raw(self.encoder.encode(text, encoding))

7
test/conftest.py Normal file
View File

@ -0,0 +1,7 @@
import pytest
from escpos.printer import Dummy
@pytest.fixture
def driver():
return Dummy()

View File

@ -10,7 +10,7 @@ from __future__ import unicode_literals
import os import os
import sys import sys
from scripttest import TestFileEnvironment from scripttest import TestFileEnvironment
from nose.tools import assert_equals from nose.tools import assert_equals, nottest
import escpos import escpos
TEST_DIR = os.path.abspath('test/test-cli-output') TEST_DIR = os.path.abspath('test/test-cli-output')
@ -84,6 +84,7 @@ class TestCLI():
assert not result.stderr assert not result.stderr
assert_equals(escpos.__version__, result.stdout.strip()) 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): def test_cli_text(self):
""" Make sure text returns what we sent it """ """ Make sure text returns what we sent it """
test_text = 'this is some text' test_text = 'this is some text'

View File

@ -12,18 +12,29 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
import pytest
import mock
from hypothesis import given, assume
import hypothesis.strategies as st
from escpos.printer import Dummy from escpos.printer import Dummy
def test_function_text_dies_ist_ein_test_lf(): def get_printer():
"""test the text printing function with simple string and compare output""" return Dummy(magic_encode_args={'disabled': True, 'encoding': 'CP437'})
instance = Dummy()
instance.text('Dies ist ein Test.\n')
assert instance.output == b'Dies ist ein Test.\n' @given(text=st.text())
def test_text(text):
"""Test that text() calls the MagicEncode object.
"""
instance = get_printer()
instance.magic.write = mock.Mock()
instance.text(text)
instance.magic.write.assert_called_with(text)
def test_block_text(): def test_block_text():
printer = Dummy() printer = get_printer()
printer.block_text( printer.block_text(
"All the presidents men were eating falafel for breakfast.", font='a') "All the presidents men were eating falafel for breakfast.", font='a')
assert printer.output == \ assert printer.output == \

114
test/test_magicencode.py Normal file
View File

@ -0,0 +1,114 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""tests for the magic encode module
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/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
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
from escpos.katakana import encode_katakana
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({'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'
@raises(ValueError)
def test_get_encoding(self):
Encoder({}).get_encoding_name('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\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\x13\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\x0f\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({'CP437': 1}),
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\x00'
encode.write('€ ist teuro.')
assert driver.output == b'\x1bt\x00? ist teuro.'
try:
import jaconv
except ImportError:
jaconv = None
@pytest.mark.skipif(not jaconv, reason="jaconv not installed")
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'

View File

@ -15,7 +15,7 @@ from __future__ import unicode_literals
import six import six
import mock import pytest
from hypothesis import given from hypothesis import given
from hypothesis.strategies import text from hypothesis.strategies import text
@ -27,21 +27,22 @@ else:
mock_open_call = '__builtin__.open' mock_open_call = '__builtin__.open'
@given(path=text()) @given(path=text())
@mock.patch(mock_open_call) def test_load_file_printer(mocker, path):
@mock.patch('escpos.escpos.Escpos.__init__')
def test_load_file_printer(mock_escpos, mock_open, path):
"""test the loading of the file-printer""" """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) printer.File(devfile=path)
assert mock_escpos.called assert mock_escpos.called
mock_open.assert_called_with(path, "wb") mock_open.assert_called_with(path, "wb")
@given(txt=text()) @given(txt=text())
@mock.patch.object(printer.File, 'device') def test_auto_flush(mocker, txt):
@mock.patch(mock_open_call)
@mock.patch('escpos.escpos.Escpos.__init__')
def test_auto_flush(mock_escpos, mock_open, mock_device, txt):
"""test auto_flush in file-printer""" """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) p = printer.File(auto_flush=False)
# inject the mocked device-object # inject the mocked device-object
p.device = mock_device p.device = mock_device
@ -56,10 +57,11 @@ def test_auto_flush(mock_escpos, mock_open, mock_device, txt):
@given(txt=text()) @given(txt=text())
@mock.patch.object(printer.File, 'device') def test_flush_on_close(mocker, txt):
@mock.patch(mock_open_call)
def test_flush_on_close(mock_open, mock_device, txt):
"""test flush on close in file-printer""" """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) p = printer.File(auto_flush=False)
# inject the mocked device-object # inject the mocked device-object
p.device = mock_device p.device = mock_device

View File

@ -3,11 +3,13 @@ envlist = py27, py34, py35, docs
[testenv] [testenv]
deps = nose deps = nose
jaconv
coverage coverage
scripttest scripttest
mock mock
pytest pytest
pytest-cov pytest-cov
pytest-mock
hypothesis hypothesis
commands = py.test --cov escpos commands = py.test --cov escpos