Compare commits

...

10 Commits

Author SHA1 Message Date
Philipp Klaus 56cf4394ad [fix] "response doesn't start with the usual header"
fixes #25, fixes #69, fixes #75, fixes #81

also seen in issues: #56, #71
2020-02-04 16:24:32 +01:00
Philipp Klaus 638b365d45 initial, still very untested support for PT-P750W 2019-09-02 22:37:38 +02:00
Philipp Klaus 05516a7692 'brother_ql analyze' updated to support P-Touch series files 2019-09-02 20:34:12 +02:00
Philipp Klaus b551b1fc94 make classes FormFactor() and Color() IntEnums 2019-01-21 11:28:47 +01:00
Philipp Klaus 5c2b72b18b Remove deprecation warning for now 2019-01-21 11:27:45 +01:00
Philipp Klaus 2eeac7a4b6 improve docstring documentation of brother_ql.raster 2019-01-13 11:19:04 +01:00
Philipp Klaus 99c35993c8 brother_ql.raster.Raster._unsupported() is now 'protected' 2019-01-13 11:18:26 +01:00
Philipp Klaus ca4ac0544c brother_ql.raster: remove unused import 2019-01-13 11:16:37 +01:00
Philipp Klaus 1cfc7e7302 attrs & enum based replacement for devicedependent
This change replaces the simple lists and dictionaries
defined in brother_ql/devicedependent.py with data class
definitions based on attrs. They are split into two
new modules:

* brother_ql/models.py and
* brother_ql/labels.py.

To keep the compatibility with other software relying on
this package, the old brother_ql/devicedependent.py module
can still be imported. Its content is recreated with the
help of the new modules in some _populate_legacy_structures()
functions.
2019-01-13 01:24:18 +01:00
Philipp Klaus df31020d4d Revert dataclasses based replacement for devicedependent
There is a big problem with this commit - it only works with:

* Python 3.5 and later due to
  type hints being introduced with
  PEP-484 https://www.python.org/dev/peps/pep-0484/
  lead to syntax errors on earlier versions.
* (even worse) only with Python 3.6+ due to
  PEP 526 variable annotations (introduced in 3.6)
  needed by dataclasses too.

We aim, however, at Python 2.7 compatibility with this project.

So after all, I reverse the commit and will implement the changes
in a different way.
2019-01-13 00:40:55 +01:00
9 changed files with 134 additions and 62 deletions
+2
View File
@@ -1,4 +1,6 @@
from builtins import bytes
import logging
logger = logging.getLogger(__name__)
+1 -1
View File
@@ -9,7 +9,7 @@ Install via `pip install pyusb`
"""
from __future__ import unicode_literals
from builtins import str
from builtins import str, bytes
import time
+8 -3
View File
@@ -9,7 +9,8 @@ from PIL import Image
import PIL.ImageOps, PIL.ImageChops
from brother_ql.raster import BrotherQLRaster
from brother_ql.devicedependent import label_type_specs, ENDLESS_LABEL, DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL, right_margin_addition
from brother_ql.devicedependent import ENDLESS_LABEL, DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL, PTOUCH_ENDLESS_LABEL
from brother_ql.devicedependent import label_type_specs, right_margin_addition
from brother_ql import BrotherQLUnsupportedCmd
from brother_ql.image_trafos import filtered_hsv
@@ -102,7 +103,7 @@ def convert(qlr, images, label, **kwargs):
else:
dots_expected = dots_printable
if label_specs['kind'] == ENDLESS_LABEL:
if label_specs['kind'] in (ENDLESS_LABEL, PTOUCH_ENDLESS_LABEL):
if rotate not in ('auto', 0):
im = im.rotate(rotate, expand=True)
if dpi_600:
@@ -161,10 +162,14 @@ def convert(qlr, images, label, **kwargs):
qlr.mtype = 0x0B
qlr.mwidth = tape_size[0]
qlr.mlength = tape_size[1]
else:
elif label_specs['kind'] in (ENDLESS_LABEL, ):
qlr.mtype = 0x0A
qlr.mwidth = tape_size[0]
qlr.mlength = 0
elif label_specs['kind'] in (PTOUCH_ENDLESS_LABEL, ):
qlr.mtype = 0x00
qlr.mwidth = tape_size[0]
qlr.mlength = 0
qlr.pquality = int(hq)
qlr.add_media_and_quality(im.size[1])
try:
+3 -3
View File
@@ -16,13 +16,12 @@ import logging
logger = logging.getLogger(__name__)
logger.warn("deprecation warning: brother_ql.devicedependent is deprecated and will be removed in a future release")
## These module level variables were available here before.
# Concerning labels
DIE_CUT_LABEL = None
ENDLESS_LABEL = None
ROUND_DIE_CUT_LABEL = None
PTOUCH_ENDLESS_LABEL = None
label_type_specs = {}
label_sizes = []
# And concerning printer models
@@ -63,13 +62,14 @@ def _populate_label_legacy_structures():
We contain this code inside a function so that the imports
we do in here are not visible at the module level.
"""
global DIE_CUT_LABEL, ENDLESS_LABEL, ROUND_DIE_CUT_LABEL
global DIE_CUT_LABEL, ENDLESS_LABEL, ROUND_DIE_CUT_LABEL, PTOUCH_ENDLESS_LABEL
global label_sizes, label_type_specs
from brother_ql.labels import FormFactor
DIE_CUT_LABEL = FormFactor.DIE_CUT
ENDLESS_LABEL = FormFactor.ENDLESS
ROUND_DIE_CUT_LABEL = FormFactor.ROUND_DIE_CUT
PTOUCH_ENDLESS_LABEL =FormFactor.PTOUCH_ENDLESS
from brother_ql.labels import LabelsManager
lm = LabelsManager()
+20 -17
View File
@@ -1,13 +1,13 @@
from dataclasses import dataclass, field
from attr import attrs, attrib
from typing import List, Tuple
from enum import Enum
from enum import IntEnum
import copy
from brother_ql.helpers import ElementsManager
class FormFactor(Enum):
class FormFactor(IntEnum):
"""
Enumeration representing the form factor of a label.
The labels for the Brother QL series are supplied either as die-cut (pre-sized), or for more flexibility the
@@ -19,8 +19,10 @@ class FormFactor(Enum):
ENDLESS = 2
#: round die-cut labels
ROUND_DIE_CUT = 3
#: endless P-touch labels
PTOUCH_ENDLESS = 4
class Color(Enum):
class Color(IntEnum):
"""
Enumeration representing the colors to be printed on a label. Most labels only support printing black on white.
Some newer ones can also print in black and red on white.
@@ -30,35 +32,35 @@ class Color(Enum):
#: The label can be printed in black, white & red.
BLACK_RED_WHITE = 1
@dataclass
class Label:
@attrs
class Label(object):
"""
This class represents a label. All specifics of a certain label
and what the rasterizer needs to take care of depending on the
label choosen, should be contained in this class.
"""
#: A string identifier given to each label that can be selected. Eg. '29'.
identifier: str
identifier = attrib(type=str)
#: The tape size of a single label (width, lenght) in mm. For endless labels, the length is 0 by definition.
tape_size: Tuple[int, int]
tape_size = attrib(type=Tuple[int, int])
#: The type of label
form_factor: FormFactor
form_factor = attrib(type=FormFactor)
#: The total area (width, length) of the label in dots (@300dpi).
dots_total: Tuple[int, int]
dots_total = attrib(type=Tuple[int, int])
#: The printable area (width, length) of the label in dots (@300dpi).
dots_printable: Tuple[int, int]
dots_printable = attrib(type=Tuple[int, int])
#: The required offset from the right side of the label in dots to obtain a centered printout.
offset_r: int
offset_r = attrib(type=int)
#: An additional amount of feeding when printing the label.
#: This is non-zero for some smaller label sizes and for endless labels.
feed_margin: int = 0
feed_margin = attrib(type=int, default=0)
#: If a label can only be printed with certain label printers, this member variable lists the allowed ones.
#: Otherwise it's an empty list.
restricted_to_models: List[str] = field(default_factory=list)
restricted_to_models = attrib(type=List[str], factory=list)
#: Some labels allow printing in red, most don't.
color: Color = Color.BLACK_WHITE
color = attrib(type=Color, default=Color.BLACK_WHITE)
def works_with_model(self, model) -> bool:
def works_with_model(self, model): # type: bool
"""
Method to determine if certain label can be printed by the specified printer model.
"""
@@ -66,7 +68,7 @@ class Label:
else: return True
@property
def name(self) -> str:
def name(self): # type: str
out = ""
if 'x' in self.identifier:
out = '{0}mm x {1}mm die-cut'.format(*self.tape_size)
@@ -102,6 +104,7 @@ ALL_LABELS = (
Label("d12", ( 12, 12), FormFactor.ROUND_DIE_CUT, ( 142, 142), ( 94, 94), 113 , feed_margin=35),
Label("d24", ( 24, 24), FormFactor.ROUND_DIE_CUT, ( 284, 284), ( 236, 236), 42 ),
Label("d58", ( 58, 58), FormFactor.ROUND_DIE_CUT, ( 688, 688), ( 618, 618), 51 ),
Label("pt24", ( 24, 0), FormFactor.PTOUCH_ENDLESS,( 128, 0), ( 128, 0), 0, feed_margin=14),
)
class LabelsManager(ElementsManager):
+16 -14
View File
@@ -1,43 +1,43 @@
from dataclasses import dataclass
from attr import attrs, attrib
from typing import Tuple
import copy
from brother_ql.helpers import ElementsManager
@dataclass
class Model:
@attrs
class Model(object):
"""
This class represents a printer model. All specifics of a certain model
and the opcodes it supports should be contained in this class.
"""
#: A string identifier given to each model implemented. Eg. 'QL-500'.
identifier: str
identifier = attrib(type=str)
#: Minimum and maximum number of rows or 'dots' that can be printed.
#: Together with the dpi this gives the minimum and maximum length
#: for continuous tape printing.
min_max_length_dots: Tuple[int, int]
min_max_length_dots = attrib(type=Tuple[int, int])
#: The minimum and maximum amount of feeding a label
min_max_feed: Tuple[int, int] = (35, 1500)
number_bytes_per_row: int = 90
min_max_feed = attrib(type=Tuple[int, int], default=(35, 1500))
number_bytes_per_row = attrib(type=int, default=90)
#: The required additional offset from the right side
additional_offset_r: int = 0
additional_offset_r = attrib(type=int, default=0)
#: Support for the 'mode setting' opcode
mode_setting: bool = True
mode_setting = attrib(type=bool, default=True)
#: Model has a cutting blade to automatically cut labels
cutting: bool = True
cutting = attrib(type=bool, default=True)
#: Model has support for the 'expanded mode' opcode.
#: (So far, all models that have cutting support do).
expanded_mode: bool = True
expanded_mode = attrib(type=bool, default=True)
#: Model has support for compressing the transmitted raster data.
#: Some models with only USB connectivity don't support compression.
compression: bool = True
compression = attrib(type=bool, default=True)
#: Support for two color printing (black/red/white)
#: available only on some newer models.
two_color: bool = False
two_color = attrib(type=bool, default=False)
@property
def name(self) -> str:
def name(self):
return self.identifier
ALL_MODELS = [
@@ -55,6 +55,8 @@ ALL_MODELS = [
Model('QL-820NWB',(150, 11811), two_color=True),
Model('QL-1050', (295, 35433), number_bytes_per_row=162, additional_offset_r=44),
Model('QL-1060N', (295, 35433), number_bytes_per_row=162, additional_offset_r=44),
Model('PT-P750W', (31, 14172), number_bytes_per_row=16),
Model('PT-P900W', (57, 28346), number_bytes_per_row=70),
]
class ModelsManager(ElementsManager):
+56 -14
View File
@@ -1,3 +1,11 @@
"""
This module contains the implementation of the raster language
of the Brother QL-series label printers according to their
documentation and to reverse engineering efforts.
The central piece of code in this module is the class
:py:class:`BrotherQLRaster`.
"""
from builtins import bytes
@@ -12,7 +20,6 @@ from .devicedependent import models, \
min_max_feed, \
min_max_length_dots, \
number_bytes_per_row, \
right_margin_addition, \
compressionsupport, \
cuttingsupport, \
expandedmode, \
@@ -30,6 +37,21 @@ logger = logging.getLogger(__name__)
class BrotherQLRaster(object):
"""
This class facilitates the creation of a complete set
of raster instructions by adding them one after the other
using the methods of the class. Each method call is adding
instructions to the member variable :py:attr:`data`.
Instatiate the class by providing the printer
model as argument.
:param str model: Choose from the list of available models.
:ivar bytes data: The resulting bytecode with all instructions.
:ivar bool exception_on_warning: If set to True, an exception is raised if trying to add instruction which are not supported on the selected model. If set to False, the instruction is simply ignored and a warning sent to logging/stderr.
"""
def __init__(self, model='QL-500'):
if model not in models:
raise BrotherQLUnknownModel()
@@ -57,7 +79,7 @@ class BrotherQLRaster(object):
else:
logger.warning(problem)
def unsupported(self, problem):
def _unsupported(self, problem):
"""
Raises BrotherQLUnsupportedCmd if
exception_on_warning is set to True.
@@ -86,7 +108,7 @@ class BrotherQLRaster(object):
the mode change (others are in raster mode already).
"""
if self.model not in modesetting:
self.unsupported("Trying to switch the operating mode on a printer that doesn't support the command.")
self._unsupported("Trying to switch the operating mode on a printer that doesn't support the command.")
return
self.data += b'\x1B\x69\x61\x01' # ESC i a
@@ -139,24 +161,24 @@ class BrotherQLRaster(object):
def add_autocut(self, autocut = False):
if self.model not in cuttingsupport:
self.unsupported("Trying to call add_autocut with a printer that doesn't support it")
self._unsupported("Trying to call add_autocut with a printer that doesn't support it")
return
self.data += b'\x1B\x69\x4D' # ESC i M
self.data += bytes([autocut << 6])
def add_cut_every(self, n=1):
if self.model not in cuttingsupport:
self.unsupported("Trying to call add_cut_every with a printer that doesn't support it")
self._unsupported("Trying to call add_cut_every with a printer that doesn't support it")
return
self.data += b'\x1B\x69\x41' # ESC i A
self.data += bytes([n & 0xFF])
def add_expanded_mode(self):
if self.model not in expandedmode:
self.unsupported("Trying to set expanded mode (dpi/cutting at end) on a printer that doesn't support it")
self._unsupported("Trying to set expanded mode (dpi/cutting at end) on a printer that doesn't support it")
return
if self.two_color_printing and not self.two_color_support:
self.unsupported("Trying to set two_color_printing in expanded mode on a printer that doesn't support it.")
self._unsupported("Trying to set two_color_printing in expanded mode on a printer that doesn't support it.")
return
self.data += b'\x1B\x69\x4B' # ESC i K
flags = 0x00
@@ -170,8 +192,16 @@ class BrotherQLRaster(object):
self.data += struct.pack('<H', dots)
def add_compression(self, compression=True):
"""
Add an instruction enabling or disabling compression for the transmitted raster image lines.
Not all models support compression. If the specific model doesn't support it but this method
is called trying to enable it, either a warning is set or an exception is raised depending on
the value of :py:attr:`exception_on_warning`
:param bool compression: Whether compression should be on or off
"""
if self.model not in compressionsupport:
self.unsupported("Trying to set compression on a printer that doesn't support it")
self._unsupported("Trying to set compression on a printer that doesn't support it")
return
self._compression = compression
self.data += b'\x4D' # M
@@ -185,7 +215,14 @@ class BrotherQLRaster(object):
return nbpr*8
def add_raster_data(self, image, second_image=None):
""" image: Pillow Image() """
"""
Add the image data to the instructions.
The provided image has to be binary (every pixel
is either black or white).
:param PIL.Image.Image image: The image to be converted and added to the raster instructions
:param PIL.Image.Image second_image: A second image with a separate color layer (red layer for the QL-800 series)
"""
logger.debug("raster_image_size: {0}x{1}".format(*image.size))
if image.size[0] != self.get_pixel_width():
fmt = 'Wrong pixel width: {}, expected {}'
@@ -208,13 +245,18 @@ class BrotherQLRaster(object):
while start + row_len <= frame_len:
for i, frame in enumerate(frames):
row = frame[start:start+row_len]
if second_image:
file_str.write(b'\x77\x01' if i == 0 else b'\x77\x02')
else:
file_str.write(b'\x67\x00')
if self._compression:
row = packbits.encode(row)
file_str.write(bytes([len(row)]))
translen = len(row) # number of bytes to be transmitted
if self.model.startswith('PT'):
file_str.write(b'\x47')
file_str.write(bytes([translen%256, translen//256]))
else:
if second_image:
file_str.write(b'\x77\x01' if i == 0 else b'\x77\x02')
else:
file_str.write(b'\x67\x00')
file_str.write(bytes([translen]))
file_str.write(row)
start += row_len
self.data += file_str.getvalue()
+27 -9
View File
@@ -16,8 +16,10 @@ OPCODES = {
# signature name following bytes description
b'\x00': ("preamble", -1, "Preamble, 200-300x 0x00 to clear comamnd buffer"),
b'\x4D': ("compression", 1, ""),
b'\x67': ("raster", -1, ""),
b'\x77': ("2-color raster", -1, ""),
b'\x67': ("raster QL", -1, ""),
b'\x47': ("raster P-touch", -1, ""),
b'\x77': ("2-color raster QL", -1, ""),
b'\x5a': ("zero raster", 0, "empty raster line"),
b'\x0C': ("print", 0, "print intermediate page"),
b'\x1A': ("print", 0, "print final page"),
b'\x1b\x40': ("init", 0, "initialization"),
@@ -139,12 +141,15 @@ def chunker(data, raise_exception=False):
opcode_def = OPCODES[opcode]
num_bytes = len(opcode)
if opcode_def[1] > 0: num_bytes += opcode_def[1]
if 'raster' in opcode_def[0]:
elif opcode_def[0] in ('raster QL', '2-color raster QL'):
num_bytes += data[2] + 2
elif opcode_def[0] in ('raster P-touch',):
num_bytes += data[1] + data[2]*256 + 2
#payload = data[len(opcode):num_bytes]
instructions.append(data[:num_bytes])
yield instructions[-1]
data = data[num_bytes:]
return instructions
#return instructions
def match_opcode(data):
matching_opcodes = [opcode for opcode in OPCODES.keys() if data.startswith(opcode)]
@@ -262,7 +267,11 @@ class BrotherQLReader(object):
logger.info(" {} ({}) --> found! (payload: {})".format(opcode_def[0], hex_format(opcode), hex_format(payload)))
if opcode_def[0] == 'compression':
self.compression = payload[0] == 0x02
if 'raster' in opcode_def[0]:
if opcode_def[0] == 'zero raster':
self.black_rows.append(bytes())
if self.two_color_printing:
self.red_rows.append(bytes())
if opcode_def[0] in ('raster QL', '2-color raster QL', 'raster P-touch'):
rpl = bytes(payload[2:]) # raster payload
if self.compression:
row = bytes()
@@ -282,7 +291,7 @@ class BrotherQLReader(object):
if index >= len(rpl): break
else:
row = rpl
if opcode_def[0] == 'raster':
if opcode_def[0] in ('raster QL', 'raster P-touch'):
self.black_rows.append(row)
else: # 2-color
if payload[0] == 0x01:
@@ -306,15 +315,24 @@ class BrotherQLReader(object):
logger.info("Len of red rows: %d", len(self.red_rows))
def get_im(rows):
if not len(rows): return None
size = (len(rows[0])*8, len(rows))
data = bytes(b''.join(rows))
width_dots = max(len(row) for row in rows)
height_dots = len(rows)
size = (width_dots*8, height_dots)
expanded_rows = []
for row in rows:
if len(row) == 0:
expanded_rows.append(b'\x00'*width_dots)
else:
expanded_rows.append(row)
data = bytes(b''.join(expanded_rows))
data = bytes([2**8 + ~byte for byte in data]) # invert b/w
im = Image.frombytes("1", size, data, decoder_name='raw')
return im
im_black, im_red = (get_im(rows) for rows in (self.black_rows, self.red_rows))
if not self.two_color_printing:
im_black = get_im(self.black_rows)
im = im_black
else:
im_black, im_red = (get_im(rows) for rows in (self.black_rows, self.red_rows))
im_black = im_black.convert("RGBA")
im_red = im_red.convert("L")
im_red = colorize(im_red, (255, 0, 0), (255, 255, 255))
+1 -1
View File
@@ -43,7 +43,7 @@ setup(name='brother_ql',
"packbits",
"pillow>=3.3.0",
"pyusb",
'dataclasses;python_version<"3.7"',
'attrs',
'typing;python_version<"3.5"',
'enum34;python_version<"3.4"',
],