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 import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+1 -1
View File
@@ -9,7 +9,7 @@ Install via `pip install pyusb`
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from builtins import str from builtins import str, bytes
import time import time
+8 -3
View File
@@ -9,7 +9,8 @@ from PIL import Image
import PIL.ImageOps, PIL.ImageChops import PIL.ImageOps, PIL.ImageChops
from brother_ql.raster import BrotherQLRaster 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 import BrotherQLUnsupportedCmd
from brother_ql.image_trafos import filtered_hsv from brother_ql.image_trafos import filtered_hsv
@@ -102,7 +103,7 @@ def convert(qlr, images, label, **kwargs):
else: else:
dots_expected = dots_printable 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): if rotate not in ('auto', 0):
im = im.rotate(rotate, expand=True) im = im.rotate(rotate, expand=True)
if dpi_600: if dpi_600:
@@ -161,10 +162,14 @@ def convert(qlr, images, label, **kwargs):
qlr.mtype = 0x0B qlr.mtype = 0x0B
qlr.mwidth = tape_size[0] qlr.mwidth = tape_size[0]
qlr.mlength = tape_size[1] qlr.mlength = tape_size[1]
else: elif label_specs['kind'] in (ENDLESS_LABEL, ):
qlr.mtype = 0x0A qlr.mtype = 0x0A
qlr.mwidth = tape_size[0] qlr.mwidth = tape_size[0]
qlr.mlength = 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.pquality = int(hq)
qlr.add_media_and_quality(im.size[1]) qlr.add_media_and_quality(im.size[1])
try: try:
+3 -3
View File
@@ -16,13 +16,12 @@ import logging
logger = logging.getLogger(__name__) 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. ## These module level variables were available here before.
# Concerning labels # Concerning labels
DIE_CUT_LABEL = None DIE_CUT_LABEL = None
ENDLESS_LABEL = None ENDLESS_LABEL = None
ROUND_DIE_CUT_LABEL = None ROUND_DIE_CUT_LABEL = None
PTOUCH_ENDLESS_LABEL = None
label_type_specs = {} label_type_specs = {}
label_sizes = [] label_sizes = []
# And concerning printer models # 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 contain this code inside a function so that the imports
we do in here are not visible at the module level. 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 global label_sizes, label_type_specs
from brother_ql.labels import FormFactor from brother_ql.labels import FormFactor
DIE_CUT_LABEL = FormFactor.DIE_CUT DIE_CUT_LABEL = FormFactor.DIE_CUT
ENDLESS_LABEL = FormFactor.ENDLESS ENDLESS_LABEL = FormFactor.ENDLESS
ROUND_DIE_CUT_LABEL = FormFactor.ROUND_DIE_CUT ROUND_DIE_CUT_LABEL = FormFactor.ROUND_DIE_CUT
PTOUCH_ENDLESS_LABEL =FormFactor.PTOUCH_ENDLESS
from brother_ql.labels import LabelsManager from brother_ql.labels import LabelsManager
lm = 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 typing import List, Tuple
from enum import Enum from enum import IntEnum
import copy import copy
from brother_ql.helpers import ElementsManager from brother_ql.helpers import ElementsManager
class FormFactor(Enum): class FormFactor(IntEnum):
""" """
Enumeration representing the form factor of a label. 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 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 ENDLESS = 2
#: round die-cut labels #: round die-cut labels
ROUND_DIE_CUT = 3 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. 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. 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. #: The label can be printed in black, white & red.
BLACK_RED_WHITE = 1 BLACK_RED_WHITE = 1
@dataclass @attrs
class Label: class Label(object):
""" """
This class represents a label. All specifics of a certain label This class represents a label. All specifics of a certain label
and what the rasterizer needs to take care of depending on the and what the rasterizer needs to take care of depending on the
label choosen, should be contained in this class. label choosen, should be contained in this class.
""" """
#: A string identifier given to each label that can be selected. Eg. '29'. #: 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. #: 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 #: The type of label
form_factor: FormFactor form_factor = attrib(type=FormFactor)
#: The total area (width, length) of the label in dots (@300dpi). #: 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). #: 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. #: 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. #: An additional amount of feeding when printing the label.
#: This is non-zero for some smaller label sizes and for endless labels. #: 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. #: If a label can only be printed with certain label printers, this member variable lists the allowed ones.
#: Otherwise it's an empty list. #: 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. #: 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. Method to determine if certain label can be printed by the specified printer model.
""" """
@@ -66,7 +68,7 @@ class Label:
else: return True else: return True
@property @property
def name(self) -> str: def name(self): # type: str
out = "" out = ""
if 'x' in self.identifier: if 'x' in self.identifier:
out = '{0}mm x {1}mm die-cut'.format(*self.tape_size) 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("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("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("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): class LabelsManager(ElementsManager):
+16 -14
View File
@@ -1,43 +1,43 @@
from dataclasses import dataclass from attr import attrs, attrib
from typing import Tuple from typing import Tuple
import copy import copy
from brother_ql.helpers import ElementsManager from brother_ql.helpers import ElementsManager
@dataclass @attrs
class Model: class Model(object):
""" """
This class represents a printer model. All specifics of a certain model This class represents a printer model. All specifics of a certain model
and the opcodes it supports should be contained in this class. and the opcodes it supports should be contained in this class.
""" """
#: A string identifier given to each model implemented. Eg. 'QL-500'. #: 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. #: Minimum and maximum number of rows or 'dots' that can be printed.
#: Together with the dpi this gives the minimum and maximum length #: Together with the dpi this gives the minimum and maximum length
#: for continuous tape printing. #: 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 #: The minimum and maximum amount of feeding a label
min_max_feed: Tuple[int, int] = (35, 1500) min_max_feed = attrib(type=Tuple[int, int], default=(35, 1500))
number_bytes_per_row: int = 90 number_bytes_per_row = attrib(type=int, default=90)
#: The required additional offset from the right side #: 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 #: 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 #: 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. #: Model has support for the 'expanded mode' opcode.
#: (So far, all models that have cutting support do). #: (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. #: Model has support for compressing the transmitted raster data.
#: Some models with only USB connectivity don't support compression. #: 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) #: Support for two color printing (black/red/white)
#: available only on some newer models. #: available only on some newer models.
two_color: bool = False two_color = attrib(type=bool, default=False)
@property @property
def name(self) -> str: def name(self):
return self.identifier return self.identifier
ALL_MODELS = [ ALL_MODELS = [
@@ -55,6 +55,8 @@ ALL_MODELS = [
Model('QL-820NWB',(150, 11811), two_color=True), Model('QL-820NWB',(150, 11811), two_color=True),
Model('QL-1050', (295, 35433), number_bytes_per_row=162, additional_offset_r=44), 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('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): class ModelsManager(ElementsManager):
+54 -12
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 from builtins import bytes
@@ -12,7 +20,6 @@ from .devicedependent import models, \
min_max_feed, \ min_max_feed, \
min_max_length_dots, \ min_max_length_dots, \
number_bytes_per_row, \ number_bytes_per_row, \
right_margin_addition, \
compressionsupport, \ compressionsupport, \
cuttingsupport, \ cuttingsupport, \
expandedmode, \ expandedmode, \
@@ -30,6 +37,21 @@ logger = logging.getLogger(__name__)
class BrotherQLRaster(object): 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'): def __init__(self, model='QL-500'):
if model not in models: if model not in models:
raise BrotherQLUnknownModel() raise BrotherQLUnknownModel()
@@ -57,7 +79,7 @@ class BrotherQLRaster(object):
else: else:
logger.warning(problem) logger.warning(problem)
def unsupported(self, problem): def _unsupported(self, problem):
""" """
Raises BrotherQLUnsupportedCmd if Raises BrotherQLUnsupportedCmd if
exception_on_warning is set to True. exception_on_warning is set to True.
@@ -86,7 +108,7 @@ class BrotherQLRaster(object):
the mode change (others are in raster mode already). the mode change (others are in raster mode already).
""" """
if self.model not in modesetting: 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 return
self.data += b'\x1B\x69\x61\x01' # ESC i a self.data += b'\x1B\x69\x61\x01' # ESC i a
@@ -139,24 +161,24 @@ class BrotherQLRaster(object):
def add_autocut(self, autocut = False): def add_autocut(self, autocut = False):
if self.model not in cuttingsupport: 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 return
self.data += b'\x1B\x69\x4D' # ESC i M self.data += b'\x1B\x69\x4D' # ESC i M
self.data += bytes([autocut << 6]) self.data += bytes([autocut << 6])
def add_cut_every(self, n=1): def add_cut_every(self, n=1):
if self.model not in cuttingsupport: 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 return
self.data += b'\x1B\x69\x41' # ESC i A self.data += b'\x1B\x69\x41' # ESC i A
self.data += bytes([n & 0xFF]) self.data += bytes([n & 0xFF])
def add_expanded_mode(self): def add_expanded_mode(self):
if self.model not in expandedmode: 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 return
if self.two_color_printing and not self.two_color_support: 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 return
self.data += b'\x1B\x69\x4B' # ESC i K self.data += b'\x1B\x69\x4B' # ESC i K
flags = 0x00 flags = 0x00
@@ -170,8 +192,16 @@ class BrotherQLRaster(object):
self.data += struct.pack('<H', dots) self.data += struct.pack('<H', dots)
def add_compression(self, compression=True): 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: 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 return
self._compression = compression self._compression = compression
self.data += b'\x4D' # M self.data += b'\x4D' # M
@@ -185,7 +215,14 @@ class BrotherQLRaster(object):
return nbpr*8 return nbpr*8
def add_raster_data(self, image, second_image=None): 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)) logger.debug("raster_image_size: {0}x{1}".format(*image.size))
if image.size[0] != self.get_pixel_width(): if image.size[0] != self.get_pixel_width():
fmt = 'Wrong pixel width: {}, expected {}' fmt = 'Wrong pixel width: {}, expected {}'
@@ -208,13 +245,18 @@ class BrotherQLRaster(object):
while start + row_len <= frame_len: while start + row_len <= frame_len:
for i, frame in enumerate(frames): for i, frame in enumerate(frames):
row = frame[start:start+row_len] row = frame[start:start+row_len]
if self._compression:
row = packbits.encode(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: if second_image:
file_str.write(b'\x77\x01' if i == 0 else b'\x77\x02') file_str.write(b'\x77\x01' if i == 0 else b'\x77\x02')
else: else:
file_str.write(b'\x67\x00') file_str.write(b'\x67\x00')
if self._compression: file_str.write(bytes([translen]))
row = packbits.encode(row)
file_str.write(bytes([len(row)]))
file_str.write(row) file_str.write(row)
start += row_len start += row_len
self.data += file_str.getvalue() self.data += file_str.getvalue()
+27 -9
View File
@@ -16,8 +16,10 @@ OPCODES = {
# signature name following bytes description # signature name following bytes description
b'\x00': ("preamble", -1, "Preamble, 200-300x 0x00 to clear comamnd buffer"), b'\x00': ("preamble", -1, "Preamble, 200-300x 0x00 to clear comamnd buffer"),
b'\x4D': ("compression", 1, ""), b'\x4D': ("compression", 1, ""),
b'\x67': ("raster", -1, ""), b'\x67': ("raster QL", -1, ""),
b'\x77': ("2-color raster", -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'\x0C': ("print", 0, "print intermediate page"),
b'\x1A': ("print", 0, "print final page"), b'\x1A': ("print", 0, "print final page"),
b'\x1b\x40': ("init", 0, "initialization"), b'\x1b\x40': ("init", 0, "initialization"),
@@ -139,12 +141,15 @@ def chunker(data, raise_exception=False):
opcode_def = OPCODES[opcode] opcode_def = OPCODES[opcode]
num_bytes = len(opcode) num_bytes = len(opcode)
if opcode_def[1] > 0: num_bytes += opcode_def[1] 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 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] #payload = data[len(opcode):num_bytes]
instructions.append(data[:num_bytes]) instructions.append(data[:num_bytes])
yield instructions[-1]
data = data[num_bytes:] data = data[num_bytes:]
return instructions #return instructions
def match_opcode(data): def match_opcode(data):
matching_opcodes = [opcode for opcode in OPCODES.keys() if data.startswith(opcode)] 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))) logger.info(" {} ({}) --> found! (payload: {})".format(opcode_def[0], hex_format(opcode), hex_format(payload)))
if opcode_def[0] == 'compression': if opcode_def[0] == 'compression':
self.compression = payload[0] == 0x02 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 rpl = bytes(payload[2:]) # raster payload
if self.compression: if self.compression:
row = bytes() row = bytes()
@@ -282,7 +291,7 @@ class BrotherQLReader(object):
if index >= len(rpl): break if index >= len(rpl): break
else: else:
row = rpl row = rpl
if opcode_def[0] == 'raster': if opcode_def[0] in ('raster QL', 'raster P-touch'):
self.black_rows.append(row) self.black_rows.append(row)
else: # 2-color else: # 2-color
if payload[0] == 0x01: if payload[0] == 0x01:
@@ -306,15 +315,24 @@ class BrotherQLReader(object):
logger.info("Len of red rows: %d", len(self.red_rows)) logger.info("Len of red rows: %d", len(self.red_rows))
def get_im(rows): def get_im(rows):
if not len(rows): return None if not len(rows): return None
size = (len(rows[0])*8, len(rows)) width_dots = max(len(row) for row in rows)
data = bytes(b''.join(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 data = bytes([2**8 + ~byte for byte in data]) # invert b/w
im = Image.frombytes("1", size, data, decoder_name='raw') im = Image.frombytes("1", size, data, decoder_name='raw')
return im return im
im_black, im_red = (get_im(rows) for rows in (self.black_rows, self.red_rows))
if not self.two_color_printing: if not self.two_color_printing:
im_black = get_im(self.black_rows)
im = im_black im = im_black
else: else:
im_black, im_red = (get_im(rows) for rows in (self.black_rows, self.red_rows))
im_black = im_black.convert("RGBA") im_black = im_black.convert("RGBA")
im_red = im_red.convert("L") im_red = im_red.convert("L")
im_red = colorize(im_red, (255, 0, 0), (255, 255, 255)) im_red = colorize(im_red, (255, 0, 0), (255, 255, 255))
+1 -1
View File
@@ -43,7 +43,7 @@ setup(name='brother_ql',
"packbits", "packbits",
"pillow>=3.3.0", "pillow>=3.3.0",
"pyusb", "pyusb",
'dataclasses;python_version<"3.7"', 'attrs',
'typing;python_version<"3.5"', 'typing;python_version<"3.5"',
'enum34;python_version<"3.4"', 'enum34;python_version<"3.4"',
], ],