diff --git a/brother_ql/cli.py b/brother_ql/cli.py index 4865aa6..edddeca 100755 --- a/brother_ql/cli.py +++ b/brother_ql/cli.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Python standard library from __future__ import print_function @@ -13,7 +13,7 @@ # imports from this very package from brother_ql.models import ModelsManager from brother_ql.labels import LabelsManager -from brother_ql.backends import available_backends, backend_factory +from brother_ql.backends import available_backends logger = logging.getLogger('brother_ql') @@ -140,6 +140,7 @@ def env(ctx: click.Context, *args, **kwargs): @click.option('-f', '--format', type=click.Choice(('default', 'json', 'raw_bytes', 'raw_base64', 'raw_hex')), default='default', help='Output Format.') @click.pass_context def status_cmd(ctx: click.Context, *args, **kwargs): + """ Prints status information from the chosen printer """ from brother_ql.backends.helpers import status as status_fn try: status, raw = status_fn(printer_model=ctx.meta.get('MODEL'), printer_identifier=ctx.meta.get('PRINTER'), backend_identifier=ctx.meta.get('BACKEND')) @@ -203,9 +204,10 @@ def print_cmd(ctx: click.Context, *args, **kwargs): @click.option('-f', '--filename-format', help="Filename format string. Default is: label{counter:04d}.png.") @click.pass_context def analyze_cmd(ctx: click.Context, *args, **kwargs): + """ Interprets a binary file containing raster instructions for Brother QL-Series printers """ from brother_ql.reader import BrotherQLReader reader = BrotherQLReader(kwargs['instructions']) - filename_format = kwargs.get('filename_format') + filename_format = kwargs.get('filename_format') if filename_format is not None: reader.filename_fmt = filename_format reader.analyse() @@ -214,6 +216,7 @@ def analyze_cmd(ctx: click.Context, *args, **kwargs): @click.argument('instructions', type=click.File('rb')) @click.pass_context def send_cmd(ctx: click.Context, *args, **kwargs): + """ Sends a raw instructions file to the printer """ from brother_ql.backends.helpers import send send(instructions=kwargs['instructions'].read(), printer_identifier=ctx.meta.get('PRINTER'), backend_identifier=ctx.meta.get('BACKEND'), blocking=True) diff --git a/brother_ql/conversion.py b/brother_ql/conversion.py index e083edc..fada77b 100644 --- a/brother_ql/conversion.py +++ b/brother_ql/conversion.py @@ -1,4 +1,7 @@ -#!/usr/bin/env python +""" +This module offers a high-level API for converting images +into a raster instruction file for the printer. +""" from __future__ import division, unicode_literals from builtins import str @@ -10,23 +13,20 @@ from brother_ql.raster import BrotherQLRaster from brother_ql.labels import LabelsManager, FormFactor -from brother_ql.models import ModelsManager from brother_ql import BrotherQLUnsupportedCmd logger = logging.getLogger(__name__) logging.getLogger("PIL.PngImagePlugin").setLevel(logging.WARNING) -def convert(qlr: BrotherQLRaster, images: list[str | Image.Image], label: str, **kwargs): +def convert(qlr: BrotherQLRaster, images: list[str | Image.Image], label: str, **kwargs): r"""Converts one or more images to a raster instruction file. :param qlr: An instance of the BrotherQLRaster class - :type qlr: :py:class:`brother_ql.raster.BrotherQLRaster` :param images: The images to be converted. They can be filenames or instances of Pillow's Image. - :type images: list(PIL.Image.Image) or list(str) images - :param str label: + :param label: Type of label the printout should be on. :param \**kwargs: See below @@ -36,13 +36,21 @@ def convert(qlr: BrotherQLRaster, images: list[str | Image.Image], label: str, Enable cutting after printing the labels. * **dither** (``bool``) -- Instead of applying a threshold to the pixel values, approximate grey tones with dithering. - * **compress** - * **red** - * **rotate** - * **dpi_600** - * **hq** - * **threshold** + * **compress** (``bool``) -- + Applies packbits compression to the image data in the raster + * **red** (``bool``) -- + Enables generation of a red channel for use with supported printer/label combinations + * **rotate** -- + Whether to rotate the image ("auto"|0|90|180|270) + * **dpi_600** (``bool``) -- + Whether to enable 300x600dpi mode for supported printers (takes 600x600dpi input image) + * **hq** -- + ??? + * **threshold** (``int``) -- + The threshold value to determine if a result pixel is black or white (0-255) """ + # TODO: seems like `hq` or `pquality` is just not used, you should investigate + label_specs = LabelsManager()[label] dots_printable = label_specs.dots_printable @@ -195,7 +203,11 @@ def convert(qlr: BrotherQLRaster, images: list[str | Image.Image], label: str, def filtered_hsv(im, filter_h, filter_s, filter_v, default_col=(255,255,255)): - """ https://stackoverflow.com/a/22237709/183995 """ + """ + https://stackoverflow.com/a/22237709/183995 + + :meta private: + """ hsv_im = im.convert('HSV') H, S, V = 0, 1, 2 diff --git a/brother_ql/exceptions.py b/brother_ql/exceptions.py index d32d9b6..107976e 100644 --- a/brother_ql/exceptions.py +++ b/brother_ql/exceptions.py @@ -1,12 +1,19 @@ +""" +This module contains all the classes of exceptions that may be raised by brother_ql +""" class BrotherQLError(Exception): + """ Base class for exceptions from this package """ pass class BrotherQLUnsupportedCmd(BrotherQLError): + """ Raised when a raster command is not supported with a given printer/label combination """ pass class BrotherQLUnknownModel(BrotherQLError): + """ Unrecognized printer model """ pass class BrotherQLRasterError(BrotherQLError): + """ Raised when invalid data is passed to functions on ``brother_ql.raster.BrotherQLRaster`` """ pass diff --git a/brother_ql/helpers.py b/brother_ql/helpers.py index c711047..a42d587 100644 --- a/brother_ql/helpers.py +++ b/brother_ql/helpers.py @@ -10,21 +10,30 @@ class ElementsManager(object): * can be compared for equality against each other * have the attribute .identifier """ - elements = [] - element_name = "element" + elements = [] #: list of elements contained by this manager + element_name = "element" #: what an element is def iter_identifiers(self): + """ Returns an iterator over the identifiers of all the elements in this manager """ for element in self.elements: yield element.identifier def iter_elements(self): + """ Returns an iterator over all the element objects in this manager """ for element in self.elements: yield element def identifiers(self): + """ Returns a list of the identifiers of all the elements in this manager """ return list(map(lambda e : e.identifier, self.elements)) - def get(self, identifier, default=None): + def get(self, identifier: str, default=None): + """ + Returns the element object with the given identifier, or if it doesn't exist, the default value + + :param identifier: the identifier to look for + :param default: what to return if the given identifier is not found + """ return next(filter(lambda l : l.identifier == identifier, self.elements), default) def __getitem__(self, key): diff --git a/brother_ql/labels.py b/brother_ql/labels.py index 95434bd..eb2502c 100644 --- a/brother_ql/labels.py +++ b/brother_ql/labels.py @@ -15,7 +15,7 @@ class FormFactor(IntEnum): """ #: rectangular die-cut labels DIE_CUT = 1 - #: endless (continouse) labels + #: endless (continuous) labels ENDLESS = 2 #: round die-cut labels ROUND_DIE_CUT = 3 @@ -67,6 +67,9 @@ def works_with_model(self, model: str) -> bool: @property def name(self) -> str: + """ + Return a formatted, human-readable name for the label + """ out = "" if 'x' in self.identifier: @@ -114,7 +117,10 @@ def name(self) -> str: ) class LabelsManager(ElementsManager): - elements = copy.copy(ALL_LABELS) + """ + Class for accessing the list of supported labels + """ + elements = copy.copy(ALL_LABELS) #: :meta private: element_name = "label" def find_label_by_size(self, width: int, height: int): diff --git a/brother_ql/models.py b/brother_ql/models.py index 969e31e..db4922b 100644 --- a/brother_ql/models.py +++ b/brother_ql/models.py @@ -40,6 +40,9 @@ class Model(object): @property def name(self) -> str: + """ + Returns the printer identifier (already human-readable) + """ return self.identifier ALL_MODELS = [ @@ -64,5 +67,8 @@ def name(self) -> str: ] class ModelsManager(ElementsManager): - elements = copy.copy(ALL_MODELS) + """ + Class for accessing the list of supported printer models + """ + elements = copy.copy(ALL_MODELS) #: :meta private: element_name = 'model' diff --git a/brother_ql/output_helpers.py b/brother_ql/output_helpers.py index b6a1f20..9ee720b 100644 --- a/brother_ql/output_helpers.py +++ b/brother_ql/output_helpers.py @@ -1,16 +1,24 @@ +""" +Module containing helper functions for printing information in a human-readable format +""" + import logging +from typing import Sequence from brother_ql.labels import FormFactor, LabelsManager logger = logging.getLogger(__name__) -def textual_label_description(labels_to_include): +def textual_label_description(label_sizes_to_include: Sequence[str]) -> str: + """ + Returns a textual description of labels with the specified sizes + """ output = "Supported label sizes:\n" output = "" fmt = " {label_size:9s} {dots_printable:14s} {label_descr:26s}\n" output += fmt.format(label_size="Name", dots_printable="Printable px", label_descr="Description") #output += fmt.format(label_size="", dots_printable="width x height", label_descr="") - for label_size in labels_to_include: + for label_size in label_sizes_to_include: label = LabelsManager()[label_size] if label.form_factor in (FormFactor.DIE_CUT, FormFactor.ROUND_DIE_CUT): dp_fmt = "{0:4d} x {1:4d}" @@ -24,13 +32,17 @@ def textual_label_description(labels_to_include): return output def log_discovered_devices(available_devices, level=logging.INFO): - for ad in available_devices: + """ + Logs all automatically discovered devices to console. + """ + for dev in available_devices: result = {'model': 'unknown'} - result.update(ad) + result.update(dev) logger.log(level, " Found a label printer: {identifier} (model: {model})".format(**result)) -def textual_description_discovered_devices(available_devices): +def textual_description_discovered_devices(available_devices) -> str: + # TODO: figure out what is the type of the argument and document it output = "" - for ad in available_devices: - output += ad['identifier'] + for dev in available_devices: + output += dev['identifier'] return output diff --git a/brother_ql/raster.py b/brother_ql/raster.py index b0e3639..8ea0a01 100644 --- a/brother_ql/raster.py +++ b/brother_ql/raster.py @@ -18,7 +18,7 @@ from brother_ql.models import Model, ModelsManager -from . import BrotherQLError, BrotherQLUnsupportedCmd, BrotherQLUnknownModel, BrotherQLRasterError +from brother_ql import BrotherQLError, BrotherQLUnsupportedCmd, BrotherQLUnknownModel, BrotherQLRasterError from io import BytesIO @@ -86,6 +86,7 @@ def _unsupported(self, problem): @property def two_color_support(self): + """ :meta private: """ return self.model.two_color def add_initialize(self): @@ -112,16 +113,24 @@ def add_invalidate(self): self.data += b'\x00' * self.num_invalidate_bytes @property - def mtype(self): return self._mtype + def mtype(self): + """ :meta private: """ + return self._mtype @property - def mwidth(self): return self._mwidth + def mwidth(self): + """ :meta private: """ + return self._mwidth @property - def mlength(self): return self._mlength + def mlength(self): + """ :meta private: """ + return self._mlength @property - def pquality(self): return self._pquality + def pquality(self): + """ :meta private: """ + return self._pquality @mtype.setter def mtype(self, value): diff --git a/brother_ql/reader.py b/brother_ql/reader.py index 8ee05df..69a0721 100755 --- a/brother_ql/reader.py +++ b/brother_ql/reader.py @@ -1,9 +1,9 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -import struct import io import logging -import sys +import struct +from typing import Any from PIL import Image from PIL.ImageOps import colorize @@ -128,13 +128,13 @@ 'Reserved', ] -def hex_format(data): - try: # Py3 - return ' '.join('{:02X}'.format(byte) for byte in data) - except ValueError: # Py2 - return ' '.join('{:02X}'.format(ord(byte)) for byte in data) +def hex_format(data: bytes): + """ + Prints the hexadecimal representation of some bytes + """ + return ' '.join('{:02X}'.format(byte) for byte in data) -def chunker(data, raise_exception=False): +def chunker(data: bytes, raise_exception=False): """ Breaks data stream (bytes) into a list of bytes objects containing single instructions each. @@ -166,12 +166,18 @@ def chunker(data, raise_exception=False): data = data[num_bytes:] return instructions -def match_opcode(data): +def match_opcode(data: bytes): + """ + Finds the opcode matching the given instruction + """ matching_opcodes = [opcode for opcode in OPCODES.keys() if data.startswith(opcode)] assert len(matching_opcodes) == 1 return matching_opcodes[0] -def interpret_response(data): +def interpret_response(data: bytes) -> dict[str, Any]: + """ + Interprets a raw response received from the printer + """ data = bytes(data) if len(data) < 32: raise NameError('Insufficient amount of data received', hex_format(data)) @@ -224,6 +230,7 @@ def interpret_response(data): identified_media = LabelsManager().find_label_by_size(media_width, media_length) + # TODO: make a real class for this response = { 'status_type': status_type, 'phase_type': phase_type, @@ -247,7 +254,7 @@ def merge_specific_instructions(chunks, join_preamble=True, join_raster=True): instruction_buffer = b'' for instruction in chunks: opcode = match_opcode(instruction) - if join_preamble and OPCODES[opcode][0] == 'preamble' and last_opcode == 'preamble': + if join_preamble and OPCODES[opcode][0] == 'preamble' and last_opcode == 'preamble': instruction_buffer += instruction elif join_raster and 'raster' in OPCODES[opcode][0] and 'raster' in last_opcode: instruction_buffer += instruction @@ -261,12 +268,15 @@ def merge_specific_instructions(chunks, join_preamble=True, join_raster=True): return new_instructions class BrotherQLReader(object): + """ + Class for decoding raw printer instruction rasters + """ DEFAULT_FILENAME_FMT = 'label{counter:04d}.png' - def __init__(self, brother_file): - if type(brother_file) in (str,): + def __init__(self, brother_file: str | io.BufferedReader): + if type(brother_file) is str: brother_file = io.open(brother_file, 'rb') - self.brother_file = brother_file + self.brother_file: io.BufferedReader = brother_file # type: ignore self.mwidth, self.mheight = None, None self.raster_no = None self.black_rows = [] @@ -279,6 +289,10 @@ def __init__(self, brother_file): self.filename_fmt = self.DEFAULT_FILENAME_FMT def analyse(self): + """ + Analyzes the instruction file and prints the results, + decoding rasters and saving them as images in the current working directory. + """ instructions = self.brother_file.read() for instruction in chunker(instructions): for opcode in OPCODES.keys(): diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..775b2a9 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,53 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'brother_ql_next' +copyright = '2024 brother_ql_next Contributors.' +author = 'LunarEclipse' +release = '0.11.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', +] + +templates_path = ['templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +# SphinxAwesome Theme: https://sphinx-themes.org/sample-sites/sphinxawesome-theme/ +html_permalinks_icon = '#' +html_theme = 'sphinxawesome_theme' + +html_static_path = ['static'] +html_css_files = [ + 'custom.css', +] + + +# -- Options for InterSphinx ------------------------------------------------- +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None) +} +intersphinx_timeout = 30 + +# -- Options for AutoDoc ----------------------------------------------------- +import os, sys +sys.path.insert(0, os.path.abspath('..')) +#autodoc_member_order = "bysource" +#autodoc_typehints = "description" +#autodoc_preserve_defaults = True +#autodoc_class_signature = "separated" diff --git a/docs/exceptions.rst b/docs/exceptions.rst new file mode 100644 index 0000000..f0381ce --- /dev/null +++ b/docs/exceptions.rst @@ -0,0 +1,8 @@ +List of Exceptions +================== + +List of Exceptions +------------------ + +.. automodule:: brother_ql.exceptions + :members: diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d17b4e4 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,33 @@ +brother_ql_next API Documentation +================================= + +This is documentation for programmers using brother_ql as a library. + +Some parts might be a bit rough, contributions to improve them are welcome. + +For documentation geared towards users of the CLI, see the `README file on GitHub `_. + +Table of Contents +----------------- + +.. toctree:: + :numbered: + :maxdepth: 2 + + labels_and_printers + raster + reader + exceptions + +Undocumented modules: +^^^^^^^^^^^^^^^^^^^^^ + +* ``brother_ql.output_helpers`` - currently inflexible, intended as an internal module +* ``brother_ql.backends.*`` - TODO - incomprehensible code that will require a bunch of work to document + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/labels_and_printers.rst b/docs/labels_and_printers.rst new file mode 100644 index 0000000..28fbd9c --- /dev/null +++ b/docs/labels_and_printers.rst @@ -0,0 +1,24 @@ +Querying supported Labels and Printers +====================================== + +This page covers accessing data about supported labels and printers. + +Base Class +---------- + +The API is designed around ElementsManager - a class that wraps a static list of objects which each contain data about one element. + +.. automodule:: brother_ql.helpers + :members: + +Printer Models +-------------- + +.. automodule:: brother_ql.models + :members: + +Label Types +----------- + +.. automodule:: brother_ql.labels + :members: diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/raster.rst b/docs/raster.rst new file mode 100644 index 0000000..a0a3470 --- /dev/null +++ b/docs/raster.rst @@ -0,0 +1,16 @@ +Creating rasters +================ + +High level Raster creation +-------------------------- + +.. automodule:: brother_ql.conversion + :members: + +Low level Raster creation +------------------------- + +.. automodule:: brother_ql.raster + :members: + :undoc-members: + diff --git a/docs/reader.rst b/docs/reader.rst new file mode 100644 index 0000000..09270fa --- /dev/null +++ b/docs/reader.rst @@ -0,0 +1,8 @@ +Parsing existing Rasters +======================== + +Parsing existing Rasters +------------------------ + +.. automodule:: brother_ql.reader + :members: diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..666ac20 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinxawesome-theme==5.2.0 diff --git a/docs/static/custom.css b/docs/static/custom.css new file mode 100644 index 0000000..36befa1 --- /dev/null +++ b/docs/static/custom.css @@ -0,0 +1,23 @@ +/* Multiline function definitions... + * https://github.com/sphinx-doc/sphinx/issues/1514#issuecomment-742703082 + * ... that only apply on functions with more than 2 parameters + * https://stackoverflow.com/a/74016670 + * https://stackoverflow.com/a/77911336 + */ + +/* Newlines (\a) and spaces (\20) before each parameter */ +.sig-object:has(> :nth-child(3 of .sig-param)) .sig-param::before { + content: "\a\20\20\20\20"; + white-space: pre; +} + +/* Newline after the last parameter (so the closing bracket is on a new line) */ +.sig-object:has(> :nth-child(3 of .sig-param)) em.sig-param:last-of-type::after { + content: "\a"; + white-space: pre; +} + +/* Restore underlines for some links */ +.contents ul li a.reference, .toctree-wrapper ul li a.reference { + text-decoration-line: underline !important; +}