Port to current version of escpos-printer-db.

This commit is contained in:
Michael Elsdörfer 2016-08-30 17:05:31 +02:00
parent 40be69347c
commit 2f89f3fe3a
7 changed files with 216 additions and 277 deletions

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):
@ -51,6 +52,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.lower(): 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

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

@ -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'])

View File

@ -123,101 +123,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
} }
# 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' 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 # Barcode format
_SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n _SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n

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

@ -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',
}

View File

@ -20,68 +20,16 @@ from __future__ import unicode_literals
from .constants import CODEPAGE_CHANGE from .constants import CODEPAGE_CHANGE
from .exceptions import CharCodeError, Error from .exceptions import CharCodeError, Error
from .capabilities import get_profile from .capabilities import get_profile
from .codepages import CodePages
import copy import copy
import six 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): class Encoder(object):
"""Takes a list of available code spaces. Picks the right one for a """Takes a list of available code spaces. Picks the right one for a
given character. 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 thus already knows what the final byte in the target encoding would
be. Nevertheless, the API of this class doesn't return the byte. 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 100000000 loops, best of 3: 0.0141 usec per loop
""" """
def __init__(self, codepages): def __init__(self, codepage_map):
self.codepages = codepages self.codepages = codepage_map
self.reverse = {v:k for k, v in codepages.items()} self.available_encodings = set(codepage_map.keys())
self.available_encodings = set(codepages.values())
self.used_encodings = set() self.used_encodings = set()
def get_sequence(self, encoding): def get_sequence(self, encoding):
return self.reverse[encoding] return int(self.codepages[encoding])
def get_encoding(self, 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) encoding = CodePages.get_encoding(encoding)
if not encoding in self.available_encodings: if not encoding in self.codepages:
raise ValueError('This encoding cannot be used for the current profile') raise ValueError((
'Encoding "{}" cannot be used for the current profile. '
'Valid encodings are: {}'
).format(encoding, ','.join(self.codepages.keys())))
return encoding 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): def can_encode(self, encoding, char):
try: try:
encoded = code_pages.encode(char, encoding) encoded = CodePages.encode(char, encoding)
assert type(encoded) is bytes assert type(encoded) is bytes
return encoded return encoded
except LookupError: except LookupError:
@ -134,7 +78,7 @@ class Encoder(object):
return True return True
def find_suitable_codespace(self, char): def find_suitable_encoding(self, char):
"""The order of our search is a specific one: """The order of our search is a specific one:
1. code pages that we already tried before; there is a good 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 that the code page we pick for this character is actually
supported. 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): if self.can_encode(encoding, char):
# This encoding worked; at it to the set of used ones. # This encoding worked; at it to the set of used ones.
self.used_encodings.add(encoding) self.used_encodings.add(encoding)
@ -160,14 +111,20 @@ class Encoder(object):
class MagicEncode(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 This will consider the printers supported codepages, according
symbol will be inserted. 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 If the printer does not support a suitable code page, it can
initializing this class, set it here. If the current encoding is insert an error character.
unknown, the first character emitted will be a codepage switch.
: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, def __init__(self, driver, encoding=None, disabled=False,
defaultsymbol='?', encoder=None): defaultsymbol='?', encoder=None):
@ -175,7 +132,7 @@ class MagicEncode(object):
raise Error('If you disable magic encode, you need to define an encoding!') raise Error('If you disable magic encode, you need to define an encoding!')
self.driver = driver 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.encoding = self.encoder.get_encoding(encoding) if encoding else None
self.defaultsymbol = defaultsymbol self.defaultsymbol = defaultsymbol
@ -219,12 +176,12 @@ class MagicEncode(object):
# We have to find another way to print this character. # We have to find another way to print this character.
# See if any of the code pages that the printer profile supports # See if any of the code pages that the printer profile supports
# can encode this character. # can encode this character.
codespace = self.encoder.find_suitable_codespace(char) encoding = self.encoder.find_suitable_encoding(char)
if not codespace: if not encoding:
self._handle_character_failed(char) self._handle_character_failed(char)
continue continue
self.write_with_encoding(codespace, char) self.write_with_encoding(encoding, char)
def _handle_character_failed(self, char): def _handle_character_failed(self, char):
"""Called when no codepage was found to render a character. """Called when no codepage was found to render a character.
@ -239,8 +196,6 @@ class MagicEncode(object):
type=type(text) type=type(text)
)) ))
encoding = self.encoder.get_encoding(encoding)
# We always know the current code page; if the new codepage # We always know the current code page; if the new codepage
# is different, emit a change command. # is different, emit a change command.
if encoding != self.encoding: if encoding != self.encoding:
@ -251,78 +206,4 @@ class MagicEncode(object):
)) ))
if text: if text:
self.driver._raw(code_pages.encode(text, encoding, errors="replace")) self.driver._raw(CodePages.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',
}

View File

@ -12,23 +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 import mock
from hypothesis import given from hypothesis import given, assume
import hypothesis.strategies as st import hypothesis.strategies as st
from escpos.printer import Dummy from escpos.printer import Dummy
def get_printer():
return Dummy(magic_encode_args={'disabled': True, 'encoding': 'cp437'})
@given(text=st.text()) @given(text=st.text())
def test_function_text_dies_ist_ein_test_lf(text): def test_text(text):
"""test the text printing function with simple string and compare output""" """Test that text() calls the MagicEncode object.
instance = Dummy() """
instance.magic.encode_text = mock.Mock() instance = get_printer()
instance.magic.write = mock.Mock()
instance.text(text) instance.text(text)
instance.magic.encode_text.assert_called_with(txt=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 == \

View File

@ -17,7 +17,8 @@ import pytest
from nose.tools import raises, assert_raises from nose.tools import raises, assert_raises
from hypothesis import given, example from hypothesis import given, example
import hypothesis.strategies as st 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 from escpos.exceptions import CharCodeError, Error
@ -25,13 +26,13 @@ from escpos.exceptions import CharCodeError, Error
class TestEncoder: class TestEncoder:
def test_can_encode(self): def test_can_encode(self):
assert not Encoder({1: 'cp437'}).can_encode('cp437', u'') assert not Encoder({'cp437': 1}).can_encode('cp437', u'')
assert Encoder({1: 'cp437'}).can_encode('cp437', u'á') assert Encoder({'cp437': 1}).can_encode('cp437', u'á')
assert not Encoder({1: 'foobar'}).can_encode('foobar', 'a') assert not Encoder({'foobar': 1}).can_encode('foobar', 'a')
def test_find_suitable_encoding(self): def test_find_suitable_encoding(self):
assert not Encoder({1: 'cp437'}).find_suitable_codespace(u'') assert not Encoder({'cp437': 1}).find_suitable_encoding(u'')
assert Encoder({1: 'cp858'}).find_suitable_codespace(u'') == 'cp858' assert Encoder({'cp858': 1}).find_suitable_encoding(u'') == 'cp858'
@raises(ValueError) @raises(ValueError)
def test_get_encoding(self): def test_get_encoding(self):
@ -51,12 +52,12 @@ class TestMagicEncode:
def test_init_from_none(self, driver): def test_init_from_none(self, driver):
encode = MagicEncode(driver, encoding=None) encode = MagicEncode(driver, encoding=None)
encode.write_with_encoding('cp858', '€ ist teuro.') 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): def test_change_from_another(self, driver):
encode = MagicEncode(driver, encoding='cp437') encode = MagicEncode(driver, encoding='cp437')
encode.write_with_encoding('cp858', '€ ist teuro.') 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): def test_no_change(self, driver):
encode = MagicEncode(driver, encoding='cp858') encode = MagicEncode(driver, encoding='cp858')
@ -68,7 +69,7 @@ class TestMagicEncode:
def test_write(self, driver): def test_write(self, driver):
encode = MagicEncode(driver) encode = MagicEncode(driver)
encode.write('€ ist teuro.') 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): def test_write_disabled(self, driver):
encode = MagicEncode(driver, encoding='cp437', disabled=True) encode = MagicEncode(driver, encoding='cp437', disabled=True)
@ -77,7 +78,7 @@ class TestMagicEncode:
def test_write_no_codepage(self, driver): def test_write_no_codepage(self, driver):
encode = MagicEncode( encode = MagicEncode(
driver, defaultsymbol="_", encoder=Encoder({1: 'cp437'}), driver, defaultsymbol="_", encoder=Encoder({'cp437': 1}),
encoding='cp437') encoding='cp437')
encode.write(u'€ ist teuro.') encode.write(u'€ ist teuro.')
assert driver.output == b'_ ist teuro.' assert driver.output == b'_ ist teuro.'
@ -87,10 +88,10 @@ class TestMagicEncode:
def test(self, driver): def test(self, driver):
encode = MagicEncode(driver) encode = MagicEncode(driver)
encode.force_encoding('cp437') encode.force_encoding('cp437')
assert driver.output == b'\x1bt' assert driver.output == b'\x1bt\x00'
encode.write('€ ist teuro.') encode.write('€ ist teuro.')
assert driver.output == b'\x1bt? ist teuro.' assert driver.output == b'\x1bt\x00? ist teuro.'
class TestKatakana: class TestKatakana: