From 1cfc7e7302bb3c6ac5632cc478d4c028d7c67a92 Mon Sep 17 00:00:00 2001 From: Philipp Klaus Date: Sun, 13 Jan 2019 01:24:18 +0100 Subject: [PATCH] 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. --- brother_ql/devicedependent.py | 273 ++++++++++++---------------------- brother_ql/helpers.py | 41 +++++ brother_ql/labels.py | 109 ++++++++++++++ brother_ql/models.py | 62 ++++++++ setup.py | 3 + 5 files changed, 309 insertions(+), 179 deletions(-) create mode 100644 brother_ql/helpers.py create mode 100644 brother_ql/labels.py create mode 100644 brother_ql/models.py diff --git a/brother_ql/devicedependent.py b/brother_ql/devicedependent.py index 246af43..4990a2a 100644 --- a/brother_ql/devicedependent.py +++ b/brother_ql/devicedependent.py @@ -1,179 +1,94 @@ - -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', -] - -min_max_length_dots = { - 'default': (295, 11811), - - # Those are using the default: - # QL-500 QL-550 QL-560 QL-650TD - - 'QL-1050': (295, 35433), - 'QL-1060N': (295, 35433), - - '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), -} - -min_max_feed = { - 'default': (35, 1500), -} - - -label_sizes = [ - "12", - "29", - "38", - "50", - "54", - "62", - "102", - "17x54", - "17x87", - "23x23", - "29x42", - "29x90", - "39x90", - "39x48", - "52x29", - "62x29", - "62x100", - "102x51", - "102x152", - "d12", - "d24", - "d58", -] - -# label_types -DIE_CUT_LABEL = 1 -ENDLESS_LABEL = 2 -ROUND_DIE_CUT_LABEL = 3 - -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}, -} - -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 - - # restrict_printers - if '102' in key: - label_type_specs[key]['restrict_printers'] = ['QL-1060N', 'QL-1050'] - else: - label_type_specs[key]['restrict_printers'] = [] - - # 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]) - -number_bytes_per_row = { - 'default': 90, - 'QL-1050': 162, - 'QL-1060N': 162, -} - -right_margin_addition = { - 'default': 0, - 'QL-1050': 44, - 'QL-1060N': 44, -} - -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', -] +""" +Deprecated Module brother_ql.devicedependent + +This module held constants and settings that were specific to +different QL-series printer models and to different label types. + +The content is now split into two modules: + +* brother_ql.models +* brother_ql.labels + +Please import directly from them as this module will be removed in a future version. +""" + +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 +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 = [] + +## Let's recreate them using the improved data structures +## in brother_ql.models and brother_ql.labels + +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 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) + +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 + + from brother_ql.labels import FormFactor + DIE_CUT_LABEL = FormFactor.DIE_CUT + ENDLESS_LABEL = FormFactor.ENDLESS + ROUND_DIE_CUT_LABEL = FormFactor.ROUND_DIE_CUT + + 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 + +def _populate_all_legacy_structures(): + _populate_label_legacy_structures() + _populate_model_legacy_structures() + +_populate_all_legacy_structures() diff --git a/brother_ql/helpers.py b/brother_ql/helpers.py new file mode 100644 index 0000000..089967e --- /dev/null +++ b/brother_ql/helpers.py @@ -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 diff --git a/brother_ql/labels.py b/brother_ql/labels.py new file mode 100644 index 0000000..d2f0d4f --- /dev/null +++ b/brother_ql/labels.py @@ -0,0 +1,109 @@ + +from attr import attrs, attrib +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 + +@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 = 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 = attrib(type=Tuple[int, int]) + #: The type of label + form_factor = attrib(type=FormFactor) + #: The total area (width, length) of the label in dots (@300dpi). + dots_total = attrib(type=Tuple[int, int]) + #: The printable area (width, length) of the label in dots (@300dpi). + 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 = 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 = 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 = attrib(type=List[str], factory=list) + #: Some labels allow printing in red, most don't. + color = attrib(type=Color, default=Color.BLACK_WHITE) + + def works_with_model(self, model): # type: 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): # type: 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" diff --git a/brother_ql/models.py b/brother_ql/models.py new file mode 100644 index 0000000..5544772 --- /dev/null +++ b/brother_ql/models.py @@ -0,0 +1,62 @@ +from attr import attrs, attrib +from typing import Tuple + +import copy + +from brother_ql.helpers import ElementsManager + +@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 = 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 = attrib(type=Tuple[int, int]) + #: The minimum and maximum amount of feeding a label + 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 = attrib(type=int, default=0) + #: Support for the 'mode setting' opcode + mode_setting = attrib(type=bool, default=True) + #: Model has a cutting blade to automatically cut labels + 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 = 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 = attrib(type=bool, default=True) + #: Support for two color printing (black/red/white) + #: available only on some newer models. + two_color = attrib(type=bool, default=False) + + @property + def name(self): + 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' diff --git a/setup.py b/setup.py index edfd252..09be49e 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,9 @@ "packbits", "pillow>=3.3.0", "pyusb", + 'attrs', + 'typing;python_version<"3.5"', + 'enum34;python_version<"3.4"', ], extras_require = { #'brother_ql_analyse': ["matplotlib",],