dataclasses & enum based replacement for devicedependent

This change replaces the simple lists and dictionaries
defined in brother_ql/devicedependent.py with data class
based definitions 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.
This commit is contained in:
Philipp Klaus
2019-01-12 21:30:51 +01:00
parent 40a1badd66
commit 0a3af68eaa
5 changed files with 293 additions and 163 deletions

View File

@@ -1,179 +1,94 @@
"""
Deprecated Module brother_ql.devicedependent
models = [
'QL-500',
'QL-550',
'QL-560',
'QL-570',
'QL-580N',
'QL-650TD',
'QL-700',
'QL-710W',
'QL-720NW',
'QL-800',
'QL-810W',
'QL-820NWB',
'QL-1050',
'QL-1060N',
]
This module held constants and settings that were specific to
different QL-series printer models and to different label types.
min_max_length_dots = {
'default': (295, 11811),
The content is now split into two modules:
# Those are using the default:
# QL-500 QL-550 QL-560 QL-650TD
* brother_ql.models
* brother_ql.labels
'QL-1050': (295, 35433),
'QL-1060N': (295, 35433),
Please import directly from them as this module will be removed in a future version.
"""
'QL-570': (150, 11811),
'QL-580N': (150, 11811),
'QL-700': (150, 11811),
'QL-710W': (150, 11811),
'QL-720NW': (150, 11811),
'QL-800': (150, 11811),
'QL-810W': (150, 11811),
'QL-820NWB':(150, 11811),
}
import logging
min_max_feed = {
'default': (35, 1500),
}
logger = logging.getLogger(__name__)
logger.warn("deprecation warning: brother_ql.devicedependent is deprecated and will be removed in a future release")
label_sizes = [
"12",
"29",
"38",
"50",
"54",
"62",
"102",
"17x54",
"17x87",
"23x23",
"29x42",
"29x90",
"39x90",
"39x48",
"52x29",
"62x29",
"62x100",
"102x51",
"102x152",
"d12",
"d24",
"d58",
]
## These module level variables were available here before.
# Concerning labels
DIE_CUT_LABEL = None
ENDLESS_LABEL = None
ROUND_DIE_CUT_LABEL = None
label_type_specs = {}
label_sizes = []
# And concerning printer models
models = []
min_max_length_dots = {}
min_max_feed = {}
number_bytes_per_row = {}
right_margin_addition = {}
modesetting = []
cuttingsupport = []
expandedmode = []
compressionsupport = []
two_color_support = []
# label_types
DIE_CUT_LABEL = 1
ENDLESS_LABEL = 2
ROUND_DIE_CUT_LABEL = 3
## Let's recreate them using the improved data structures
## in brother_ql.models and brother_ql.labels
label_type_specs = {
# (width, length)
"12": {'tape_size': ( 12, 0), 'dots_total': ( 142, 0), 'dots_printable': ( 106, 0), 'right_margin_dots': 29, 'feed_margin': 35},
"29": {'tape_size': ( 29, 0), 'dots_total': ( 342, 0), 'dots_printable': ( 306, 0), 'right_margin_dots': 6, 'feed_margin': 35},
"38": {'tape_size': ( 38, 0), 'dots_total': ( 449, 0), 'dots_printable': ( 413, 0), 'right_margin_dots': 12, 'feed_margin': 35},
"50": {'tape_size': ( 50, 0), 'dots_total': ( 590, 0), 'dots_printable': ( 554, 0), 'right_margin_dots': 12, 'feed_margin': 35},
"54": {'tape_size': ( 54, 0), 'dots_total': ( 636, 0), 'dots_printable': ( 590, 0), 'right_margin_dots': 0, 'feed_margin': 35},
"62": {'tape_size': ( 62, 0), 'dots_total': ( 732, 0), 'dots_printable': ( 696, 0), 'right_margin_dots': 12, 'feed_margin': 35},
"102": {'tape_size': (102, 0), 'dots_total': (1200, 0), 'dots_printable': (1164, 0), 'right_margin_dots': 12, 'feed_margin': 35},
"17x54": {'tape_size': ( 17, 54), 'dots_total': ( 201, 636), 'dots_printable': ( 165, 566), 'right_margin_dots': 0, 'feed_margin': 0},
"17x87": {'tape_size': ( 17, 87), 'dots_total': ( 201, 1026), 'dots_printable': ( 165, 956), 'right_margin_dots': 0, 'feed_margin': 0},
"23x23": {'tape_size': ( 23, 23), 'dots_total': ( 272, 272), 'dots_printable': ( 202, 202), 'right_margin_dots': 42, 'feed_margin': 0},
"29x42": {'tape_size': ( 29, 42), 'dots_total': ( 342, 495), 'dots_printable': ( 306, 425), 'right_margin_dots': 6, 'feed_margin': 0},
"29x90": {'tape_size': ( 29, 90), 'dots_total': ( 342, 1061), 'dots_printable': ( 306, 991), 'right_margin_dots': 6, 'feed_margin': 0},
"39x90": {'tape_size': ( 38, 90), 'dots_total': ( 449, 1061), 'dots_printable': ( 413, 991), 'right_margin_dots': 12, 'feed_margin': 0},
"39x48": {'tape_size': ( 39, 48), 'dots_total': ( 461, 565), 'dots_printable': ( 425, 495), 'right_margin_dots': 6, 'feed_margin': 0},
"52x29": {'tape_size': ( 52, 29), 'dots_total': ( 614, 341), 'dots_printable': ( 578, 271), 'right_margin_dots': 0, 'feed_margin': 0},
"62x29": {'tape_size': ( 62, 29), 'dots_total': ( 732, 341), 'dots_printable': ( 696, 271), 'right_margin_dots': 12, 'feed_margin': 0},
"62x100": {'tape_size': ( 62, 100), 'dots_total': ( 732, 1179), 'dots_printable': ( 696, 1109), 'right_margin_dots': 12, 'feed_margin': 0},
"102x51": {'tape_size': (102, 51), 'dots_total': (1200, 596), 'dots_printable': (1164, 526), 'right_margin_dots': 12, 'feed_margin': 0},
"102x152":{'tape_size': (102, 153), 'dots_total': (1200, 1804), 'dots_printable': (1164, 1660), 'right_margin_dots': 12, 'feed_margin': 0},
"d12": {'tape_size': ( 12, 12), 'dots_total': ( 142, 142), 'dots_printable': ( 94, 94), 'right_margin_dots':113, 'feed_margin': 35},
"d24": {'tape_size': ( 24, 24), 'dots_total': ( 284, 284), 'dots_printable': ( 236, 236), 'right_margin_dots': 42, 'feed_margin': 0},
"d58": {'tape_size': ( 58, 58), 'dots_total': ( 688, 688), 'dots_printable': ( 618, 618), 'right_margin_dots': 51, 'feed_margin': 0},
}
def _populate_model_legacy_structures():
from brother_ql.models import ModelsManager
global models
global min_max_length_dots, min_max_feed, number_bytes_per_row, right_margin_addition
global modesetting, cuttingsupport, expandedmode, compressionsupport, two_color_support
for key in label_type_specs:
# kind
if 'x' in key:
label_type_specs[key]['kind'] = DIE_CUT_LABEL
elif key.startswith('d'):
label_type_specs[key]['kind'] = ROUND_DIE_CUT_LABEL
else:
label_type_specs[key]['kind'] = ENDLESS_LABEL
for model in ModelsManager().iter_elements():
models.append(model.identifier)
min_max_length_dots[model.identifier] = model.min_max_length_dots
min_max_feed[model.identifier] = model.min_max_feed
number_bytes_per_row[model.identifier] = model.number_bytes_per_row
right_margin_addition[model.identifier] = model.additional_offset_r
if model.mode_setting: modesetting.append(model.identifier)
if model.cutting: cuttingsupport.append(model.identifier)
if model.expanded_mode: expandedmode.append(model.identifier)
if model.compression: compressionsupport.append(model.identifier)
if model.two_color: two_color_support.append(model.identifier)
# restrict_printers
if '102' in key:
label_type_specs[key]['restrict_printers'] = ['QL-1060N', 'QL-1050']
else:
label_type_specs[key]['restrict_printers'] = []
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 label_sizes, label_type_specs
# name
if 'x' in key:
label_type_specs[key]['name'] = '{0}mm x {1}mm die-cut'.format(*label_type_specs[key]['tape_size'])
elif key.startswith('d'):
label_type_specs[key]['name'] = '{0}mm round die-cut'.format(label_type_specs[key]['tape_size'][0])
else:
label_type_specs[key]['name'] = '{0}mm endless'.format(label_type_specs[key]['tape_size'][0])
from brother_ql.labels import FormFactor
DIE_CUT_LABEL = FormFactor.DIE_CUT
ENDLESS_LABEL = FormFactor.ENDLESS
ROUND_DIE_CUT_LABEL = FormFactor.ROUND_DIE_CUT
number_bytes_per_row = {
'default': 90,
'QL-1050': 162,
'QL-1060N': 162,
}
from brother_ql.labels import LabelsManager
lm = LabelsManager()
label_sizes = list(lm.iter_identifiers())
for label in lm.iter_elements():
l = {}
l['name'] = label.name
l['kind'] = label.form_factor
l['color'] = label.color
l['tape_size'] = label.tape_size
l['dots_total'] = label.dots_total
l['dots_printable'] = label.dots_printable
l['right_margin_dots'] = label.offset_r
l['feed_margin'] = label.feed_margin
l['restrict_printers'] = label.restricted_to_models
label_type_specs[label.identifier] = l
right_margin_addition = {
'default': 0,
'QL-1050': 44,
'QL-1060N': 44,
}
def _populate_all_legacy_structures():
_populate_label_legacy_structures()
_populate_model_legacy_structures()
modesetting = [
'QL-580N',
'QL-650TD',
'QL-1050',
'QL-1060N',
'QL-710W',
'QL-720NW',
'QL-800',
'QL-810W',
'QL-820NWB',
]
cuttingsupport = [
'QL-550',
'QL-560',
'QL-570',
'QL-580N',
'QL-650TD',
'QL-700',
'QL-1050',
'QL-1060N',
'QL-710W',
'QL-720NW',
'QL-800',
'QL-810W',
'QL-820NWB',
]
expandedmode = cuttingsupport
compressionsupport = [
'QL-580N',
'QL-650TD',
'QL-1050',
'QL-1060N',
'QL-710W',
'QL-720NW',
'QL-810W',
'QL-820NWB',
]
two_color_support = [
'QL-800',
'QL-810W',
'QL-820NWB',
]
_populate_all_legacy_structures()

41
brother_ql/helpers.py Normal file
View File

@@ -0,0 +1,41 @@
import logging
logger = logging.getLogger(__name__)
class ElementsManager(object):
"""
A class managing a collection of 'elements'.
Those elements are expected to be objects that
* can be compared for equality against each other
* have the attribute .identifier
"""
DEFAULT_ELEMENTS = []
ELEMENT_NAME = "element"
def __init__(self, elements=None):
if elements:
self._elements = elements
else:
self._elements = self.DEFAULT_ELEMENTS
def register(self, element, pos=-1):
if element not in self._elements:
if pos == -1: pos = len(self._labels)
self._labels.insert(len(self._labels), label)
else:
logger.warn("Won't register %s as it's already present: %s", self.ELEMENT_NAME, element)
def deregister(self, element):
if element in self._elements:
self._elements.remove(element)
else:
logger.warn("Trying to deregister a %s that's not registered currently: %s", self.ELEMENT_NAME, label)
def iter_identifiers(self):
for element in self._elements:
yield element.identifier
def iter_elements(self):
for element in self._elements:
yield element

109
brother_ql/labels.py Normal file
View File

@@ -0,0 +1,109 @@
from dataclasses import dataclass, field
from typing import List, Tuple
from enum import Enum
import copy
from brother_ql.helpers import ElementsManager
class FormFactor(Enum):
"""
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
continuous label tapes offer the ability to vary the label length.
"""
#: rectangular die-cut labels
DIE_CUT = 1
#: endless (continouse) labels
ENDLESS = 2
#: round die-cut labels
ROUND_DIE_CUT = 3
class Color(Enum):
"""
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.
"""
#: The label can be printed in black & white.
BLACK_WHITE = 0
#: The label can be printed in black, white & red.
BLACK_RED_WHITE = 1
@dataclass
class Label:
"""
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
#: 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]
#: The type of label
form_factor: FormFactor
#: The total area (width, length) of the label in dots (@300dpi).
dots_total: Tuple[int, int]
#: The printable area (width, length) of the label in dots (@300dpi).
dots_printable: Tuple[int, int]
#: The required offset from the right side of the label in dots to obtain a centered printout.
offset_r: 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
#: 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)
#: Some labels allow printing in red, most don't.
color: Color = Color.BLACK_WHITE
def works_with_model(self, model) -> bool:
"""
Method to determine if certain label can be printed by the specified printer model.
"""
if self.restricted_to_models and model not in models: return False
else: return True
@property
def name(self) -> str:
out = ""
if 'x' in self.identifier:
out = '{0}mm x {1}mm die-cut'.format(*self.tape_size)
elif self.identifier.startswith('d'):
out = '{0}mm round die-cut'.format(self.tape_size[0])
else:
out = '{0}mm endless'.format(self.tape_size[0])
if self.color == Color.BLACK_RED_WHITE:
out += ' (black/red/white)'
return out
ALL_LABELS = (
Label("12", ( 12, 0), FormFactor.ENDLESS, ( 142, 0), ( 106, 0), 29 , feed_margin=35),
Label("29", ( 29, 0), FormFactor.ENDLESS, ( 342, 0), ( 306, 0), 6 , feed_margin=35),
Label("38", ( 38, 0), FormFactor.ENDLESS, ( 449, 0), ( 413, 0), 12 , feed_margin=35),
Label("50", ( 50, 0), FormFactor.ENDLESS, ( 590, 0), ( 554, 0), 12 , feed_margin=35),
Label("54", ( 54, 0), FormFactor.ENDLESS, ( 636, 0), ( 590, 0), 0 , feed_margin=35),
Label("62", ( 62, 0), FormFactor.ENDLESS, ( 732, 0), ( 696, 0), 12 , feed_margin=35),
Label("62red", ( 62, 0), FormFactor.ENDLESS, ( 732, 0), ( 696, 0), 12 , feed_margin=35, color=Color.BLACK_RED_WHITE),
Label("102", (102, 0), FormFactor.ENDLESS, (1200, 0), (1164, 0), 12 , feed_margin=35, restricted_to_models=['QL-1050', 'QL-1060N']),
Label("17x54", ( 17, 54), FormFactor.DIE_CUT, ( 201, 636), ( 165, 566), 0 ),
Label("17x87", ( 17, 87), FormFactor.DIE_CUT, ( 201, 1026), ( 165, 956), 0 ),
Label("23x23", ( 23, 23), FormFactor.DIE_CUT, ( 272, 272), ( 202, 202), 42 ),
Label("29x42", ( 29, 42), FormFactor.DIE_CUT, ( 342, 495), ( 306, 425), 6 ),
Label("29x90", ( 29, 90), FormFactor.DIE_CUT, ( 342, 1061), ( 306, 991), 6 ),
Label("39x90", ( 38, 90), FormFactor.DIE_CUT, ( 449, 1061), ( 413, 991), 12 ),
Label("39x48", ( 39, 48), FormFactor.DIE_CUT, ( 461, 565), ( 425, 495), 6 ),
Label("52x29", ( 52, 29), FormFactor.DIE_CUT, ( 614, 341), ( 578, 271), 0 ),
Label("62x29", ( 62, 29), FormFactor.DIE_CUT, ( 732, 341), ( 696, 271), 12 ),
Label("62x100", ( 62, 100), FormFactor.DIE_CUT, ( 732, 1179), ( 696, 1109), 12 ),
Label("102x51", (102, 51), FormFactor.DIE_CUT, (1200, 596), (1164, 526), 12 , restricted_to_models=['QL-1050', 'QL-1060N']),
Label("102x152",(102, 153), FormFactor.DIE_CUT, (1200, 1804), (1164, 1660), 12 , restricted_to_models=['QL-1050', 'QL-1060N']),
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 ),
)
class LabelsManager(ElementsManager):
DEFAULT_ELEMENTS = copy.copy(ALL_LABELS)
ELEMENT_NAME = "label"

62
brother_ql/models.py Normal file
View File

@@ -0,0 +1,62 @@
from dataclasses import dataclass
from typing import Tuple
import copy
from brother_ql.helpers import ElementsManager
@dataclass
class Model:
"""
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
#: 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]
#: The minimum and maximum amount of feeding a label
min_max_feed: Tuple[int, int] = (35, 1500)
number_bytes_per_row: int = 90
#: The required additional offset from the right side
additional_offset_r: int = 0
#: Support for the 'mode setting' opcode
mode_setting: bool = True
#: Model has a cutting blade to automatically cut labels
cutting: bool = True
#: Model has support for the 'expanded mode' opcode.
#: (So far, all models that have cutting support do).
expanded_mode: bool = True
#: Model has support for compressing the transmitted raster data.
#: Some models with only USB connectivity don't support compression.
compression: bool = True
#: Support for two color printing (black/red/white)
#: available only on some newer models.
two_color: bool = False
@property
def name(self) -> str:
return self.identifier
ALL_MODELS = [
Model('QL-500', (295, 11811), compression=False, mode_setting=False, expanded_mode=False, cutting=False),
Model('QL-550', (295, 11811), compression=False, mode_setting=False),
Model('QL-560', (295, 11811), compression=False, mode_setting=False),
Model('QL-570', (150, 11811), compression=False, mode_setting=False),
Model('QL-580N', (150, 11811)),
Model('QL-650TD', (295, 11811)),
Model('QL-700', (150, 11811), compression=False, mode_setting=False),
Model('QL-710W', (150, 11811)),
Model('QL-720NW', (150, 11811)),
Model('QL-800', (150, 11811), two_color=True, compression=False),
Model('QL-810W', (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-1060N', (295, 35433), number_bytes_per_row=162, additional_offset_r=44),
]
class ModelsManager(ElementsManager):
DEFAULT_ELEMENTS = copy.copy(ALL_MODELS)
ELEMENTS_NAME = 'model'

View File

@@ -43,6 +43,9 @@ setup(name='brother_ql',
"packbits",
"pillow>=3.3.0",
"pyusb",
'dataclasses;python_version<"3.7"',
'typing;python_version<"3.5"',
'enum34;python_version<"3.4"',
],
extras_require = {
#'brother_ql_analyse': ["matplotlib",],