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;
+}