diff --git a/src/dymoprint/command_line.py b/src/dymoprint/command_line.py index 5b21040..a94dcf4 100755 --- a/src/dymoprint/command_line.py +++ b/src/dymoprint/command_line.py @@ -12,7 +12,7 @@ from PIL import Image, ImageOps from . import __version__ -from .constants import DEFAULT_MARGIN, PIXELS_PER_MM, USE_QR, e_qrcode +from .constants import DEFAULT_MARGIN_PX, PIXELS_PER_MM, USE_QR, e_qrcode from .dymo_print_engines import DymoRenderEngine, print_label from .font_config import font_filename from .metadata import our_metadata @@ -135,8 +135,8 @@ def parse_args(): parser.add_argument( "-m", type=int, - default=DEFAULT_MARGIN, - help=f"Margin in px (default is {DEFAULT_MARGIN})", + default=DEFAULT_MARGIN_PX, + help=f"Margin in px (default is {DEFAULT_MARGIN_PX})", ) parser.add_argument( "--scale", type=int, default=90, help="Scaling font factor, [0,10] [%%]" diff --git a/src/dymoprint/constants.py b/src/dymoprint/constants.py index 5f1e50d..3d7803e 100755 --- a/src/dymoprint/constants.py +++ b/src/dymoprint/constants.py @@ -65,7 +65,7 @@ DEFAULT_FONT_STYLE = "regular" -DEFAULT_MARGIN = 56 +DEFAULT_MARGIN_PX = 56 FLAG_TO_STYLE = { "r": "regular", diff --git a/src/dymoprint/detect.py b/src/dymoprint/detect.py index b07678e..435e439 100644 --- a/src/dymoprint/detect.py +++ b/src/dymoprint/detect.py @@ -17,6 +17,8 @@ class DetectedDevice(NamedTuple): + id: int + """See dymoprint.constants.SUPPORTED_PRODUCTS for a list of known IDs.""" dev: usb.core.Device intf: usb.core.Interface devout: usb.core.Endpoint @@ -124,7 +126,9 @@ def detect_device() -> DetectedDevice: if not devout or not devin: die("The device endpoints not be found.") - return DetectedDevice(dev, intf, devout, devin) + return DetectedDevice( + id=dev.idProduct, dev=dev, intf=intf, devout=devout, devin=devin + ) def instruct_on_access_denied(dev: usb.core.Device) -> NoReturn: diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py index 7d2a8a7..176c6b2 100644 --- a/src/dymoprint/dymo_print_engines.py +++ b/src/dymoprint/dymo_print_engines.py @@ -12,7 +12,7 @@ from . import DymoLabeler from .barcode_writer import BarcodeImageWriter -from .constants import DEFAULT_MARGIN, PIXELS_PER_MM, QRCode +from .constants import DEFAULT_MARGIN_PX, PIXELS_PER_MM, QRCode from .utils import die, draw_image, scaling @@ -226,32 +226,49 @@ def merge_render( def print_label( - label_bitmap: Image.Image, margin_px: int = DEFAULT_MARGIN, tape_size_mm: int = 12 + label_bitmap: Image.Image, + margin_px: int = DEFAULT_MARGIN_PX, + tape_size_mm: int = 12, ) -> None: - """Print a label bitmap to the detected printer.""" - # convert the image to the proper matrix for the dymo labeler object + """Print a label bitmap to the detected printer. + + The label bitmap is a PIL image in 1-bit format (mode=1), and pixels with value + equal to 1 are burned. + """ + detected_device = detect_device() + + # Convert the image to the proper matrix for the dymo labeler object so that + # rows span the width of the label, and the first row corresponds to the left + # edge of the label. label_rotated = label_bitmap.transpose(Image.ROTATE_270) - labelstream = label_rotated.tobytes() + + # Convert the image to raw bytes. Pixels along rows are chunked into groups of + # 8 pixels, and subsequent rows are concatenated. + labelstream: bytes = label_rotated.tobytes() + + # Regather the bytes into rows label_stream_row_length = int(math.ceil(label_bitmap.height / 8)) if len(labelstream) // label_stream_row_length != label_bitmap.width: die("An internal problem was encountered while processing the label " "bitmap!") - label_rows = [ + label_rows: list[bytes] = [ labelstream[i : i + label_stream_row_length] for i in range(0, len(labelstream), label_stream_row_length) ] - label_matrix = [array.array("B", label_row).tolist() for label_row in label_rows] - detected_device = detect_device() + # Convert bytes into ints + label_matrix: list[list[int]] = [ + array.array("B", label_row).tolist() for label_row in label_rows + ] lm = DymoLabeler( detected_device.devout, detected_device.devin, synwait=64, - tape_size=tape_size_mm, + tape_size_mm=tape_size_mm, ) print("Printing label..") - lm.printLabel(label_matrix, margin=margin_px) + lm.printLabel(label_matrix, margin_px=margin_px) print("Done printing.") usb.util.dispose_resources(detected_device.dev) print("Cleaned up.") diff --git a/src/dymoprint/gui.py b/src/dymoprint/gui.py index 0b00b3c..5790b34 100644 --- a/src/dymoprint/gui.py +++ b/src/dymoprint/gui.py @@ -20,7 +20,7 @@ ) from usb.core import USBError -from .constants import DEFAULT_MARGIN, ICON_DIR +from .constants import DEFAULT_MARGIN_PX, ICON_DIR from .dymo_print_engines import DymoRenderEngine, print_label from .q_dymo_labels_list import QDymoLabelList @@ -65,7 +65,7 @@ def init_elements(self): self.margin.setMinimum(20) self.margin.setMaximum(1000) - self.margin.setValue(DEFAULT_MARGIN) + self.margin.setValue(DEFAULT_MARGIN_PX) self.tape_size.addItem("19", 19) self.tape_size.addItem("12", 12) self.tape_size.addItem("9", 9) diff --git a/src/dymoprint/labeler.py b/src/dymoprint/labeler.py index ec41cd8..367dac8 100755 --- a/src/dymoprint/labeler.py +++ b/src/dymoprint/labeler.py @@ -6,11 +6,11 @@ # this notice are preserved. # === END LICENSE STATEMENT === import array -from typing import Optional +from typing import List, Optional import usb -from .constants import DEFAULT_MARGIN, ESC, SYN +from .constants import DEFAULT_MARGIN_PX, ESC, SYN class DymoLabeler: @@ -28,6 +28,8 @@ class DymoLabeler: """ + tape_size_mm: int + @staticmethod def max_bytes_per_line(tape_size_mm: int = 12) -> int: return int(8 * tape_size_mm / 12) @@ -42,11 +44,11 @@ def max_bytes_per_line(tape_size_mm: int = 12) -> int: devout: usb.core.Endpoint devin: usb.core.Endpoint - def __init__(self, devout, devin, synwait=None, tape_size=12): + def __init__(self, devout, devin, synwait=None, tape_size_mm=12): """Initialize the LabelManager object. (HLF)""" - self.tape_size = tape_size - self.cmd: list[int] = [] + self.tape_size_mm = tape_size_mm + self.cmd: List[int] = [] self.response = False self.bytesPerLine_ = None self.dotTab_ = 0 @@ -125,7 +127,7 @@ def statusRequest(self): def dotTab(self, value): """Set the bias text height, in bytes. (MLF)""" - if value < 0 or value > self.max_bytes_per_line(self.tape_size): + if value < 0 or value > self.max_bytes_per_line(self.tape_size_mm): raise ValueError cmd = [ESC, ord("B"), value] self.buildCommand(cmd) @@ -143,7 +145,9 @@ def tapeColor(self, value): def bytesPerLine(self, value: int): """Set the number of bytes sent in the following lines. (MLF)""" - if value < 0 or value + self.dotTab_ > self.max_bytes_per_line(self.tape_size): + if value < 0 or value + self.dotTab_ > self.max_bytes_per_line( + self.tape_size_mm + ): raise ValueError if value == self.bytesPerLine_: return @@ -168,8 +172,8 @@ def chainMark(self): """Set Chain Mark. (MLF)""" self.dotTab(0) - self.bytesPerLine(self.max_bytes_per_line(self.tape_size)) - self.line([0x99] * self.max_bytes_per_line(self.tape_size)) + self.bytesPerLine(self.max_bytes_per_line(self.tape_size_mm)) + self.line([0x99] * self.max_bytes_per_line(self.tape_size_mm)) def skipLines(self, value): """Set number of lines of white to print. (MLF)""" @@ -193,16 +197,16 @@ def getStatus(self): response = self.sendCommand() print(response) - def printLabel(self, lines, margin=DEFAULT_MARGIN): + def printLabel(self, lines: List[List[int]], margin_px=DEFAULT_MARGIN_PX): """Print the label described by lines. (Automatically split label if larger than maxLines)""" while len(lines) > self.maxLines + 1: - self.rawPrintLabel(lines[0 : self.maxLines], margin=0) + self.rawPrintLabel(lines[0 : self.maxLines], margin_px=0) del lines[0 : self.maxLines] - self.rawPrintLabel(lines, margin=margin) + self.rawPrintLabel(lines, margin_px=margin_px) - def rawPrintLabel(self, lines, margin=DEFAULT_MARGIN): + def rawPrintLabel(self, lines: List[List[int]], margin_px=DEFAULT_MARGIN_PX): """Print the label described by lines. (HLF)""" # optimize the matrix for the dymo label printer @@ -219,8 +223,8 @@ def rawPrintLabel(self, lines, margin=DEFAULT_MARGIN): self.dotTab(dottab) for line in lines: self.line(line) - if margin > 0: - self.skipLines(margin * 2) + if margin_px > 0: + self.skipLines(margin_px * 2) self.statusRequest() response = self.sendCommand() print(f"Post-send response: {response}")