From 258ff60f6057ed87593c4dab23252774f29b6cab Mon Sep 17 00:00:00 2001 From: Johan Bouwsema Date: Sat, 15 Apr 2023 21:28:09 +0200 Subject: [PATCH 01/17] add fontsize textalign labelsize add multiline textalign and fontsize replace text for multiline_text mv anchor to center of text add multiline text alignment add fontsize selection change default aligment to left set constants back to default add fixed label length use original font_config.py --- src/dymoprint/command_line.py | 80 ++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/src/dymoprint/command_line.py b/src/dymoprint/command_line.py index 2e67c34..2c200a7 100755 --- a/src/dymoprint/command_line.py +++ b/src/dymoprint/command_line.py @@ -61,6 +61,38 @@ def parse_args(): default="r", help="Set fonts style (regular,bold,italic,narrow)", ) + parser.add_argument( + "-t", + choices=[1, 2, 3, 4, 5, 6, 7, 8], + default=FONT_SIZERATIO * 8, + type=int, + help="Set relative textsize (1-8) where 1 is smallest and 8 is biggest", + ) + parser.add_argument( + "-a", + choices=[ + "left", + "center", + "right", + ], + default="left", + help="Align multiline text (left,center,right)", + ) + parser.add_argument( + "-l", + type=int, + help="Specify minimum label length in mm" + ) + parser.add_argument( + "-j", + choices=[ + "left", + "center", + "right", + ], + default="center", + help="Justify content of label if minimum label length is specified (left,center,right)", + ) parser.add_argument("-u", nargs="?", help='Set user font, overrides "-s" parameter') parser.add_argument( "-n", @@ -183,10 +215,12 @@ def main(): else: fontoffset = min(args.f, 3) + font_sizeratio = args.t / 8 + # create an empty label image labelheight = DymoLabeler._MAX_BYTES_PER_LINE * 8 lineheight = float(labelheight) / len(labeltext) - fontsize = int(round(lineheight * FONT_SIZERATIO)) + fontsize = int(round(lineheight * font_sizeratio)) font = ImageFont.truetype(FONT_FILENAME, fontsize) labelwidth = max(font.getsize(line)[0] for line in labeltext) + (fontoffset * 2) textbitmap = Image.new("1", (labelwidth, labelheight)) @@ -206,9 +240,10 @@ def main(): ) # write the text into the empty image - for i, line in enumerate(labeltext): - lineposition = int(round(i * lineheight)) - labeldraw.text((fontoffset, lineposition), line, font=font, fill=255) + multilinetext = '\n'.join(labeltext) + align = args.a + + labeldraw.multiline_text((labelwidth / 2, labelheight / 2), multilinetext, align=align, anchor="mm", font=font, fill=255) bitmaps.append(textbitmap) @@ -224,19 +259,32 @@ def main(): if len(bitmaps) > 1: padding = 4 - labelbitmap = Image.new( - "1", - ( - sum(b.width for b in bitmaps) + padding * (len(bitmaps) - 1), - bitmaps[0].height, - ), - ) - offset = 0 - for bitmap in bitmaps: - labelbitmap.paste(bitmap, box=(offset, 0)) - offset += bitmap.width + padding else: - labelbitmap = bitmaps[0] + padding = 0 + computedlabellength = sum(b.width for b in bitmaps) + padding * (len(bitmaps) - 1) + if args.l is not None: + if args.m is not None: + minlabellength = (args.l * 7) - args.m + else: + minlabellength = (args.l * 7) - 56 * 2 + labellength = max(computedlabellength, minlabellength) + if (args.j == "left"): offset = 0 + if (args.j == "center"): offset = max(0, int((minlabellength - computedlabellength) / 2)) + if (args.j == "right"): offset = max(0, int(minlabellength - computedlabellength)) + else: + labellength = computedlabellength + offset = 0 + labelbitmap = Image.new( + "1", + ( + labellength, + bitmaps[0].height, + ), + ) + for bitmap in bitmaps: + labelbitmap.paste(bitmap, box=(offset, 0)) + offset += bitmap.width + padding + # convert the image to the proper matrix for the dymo labeler object labelrotated = labelbitmap.transpose(Image.ROTATE_270) From 215eba2ea4a862069f3b5090f84d1929f5ab6be6 Mon Sep 17 00:00:00 2001 From: Michal Roman Date: Wed, 3 May 2023 18:47:34 +0200 Subject: [PATCH 02/17] DymoLabeler added tape_size param --- src/dymoprint/labeler.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/dymoprint/labeler.py b/src/dymoprint/labeler.py index 18a3ec6..c665049 100755 --- a/src/dymoprint/labeler.py +++ b/src/dymoprint/labeler.py @@ -26,7 +26,10 @@ class DymoLabeler: """ - _MAX_BYTES_PER_LINE = 8 # 64 pixels on a 12mm tape + @staticmethod + def max_bytes_per_line(tape_size=12): + return int(8*tape_size/12) + # Max number of print lines to send before waiting for a response. This helps # to avoid timeouts due to differences between data transfer and @@ -36,9 +39,10 @@ class DymoLabeler: # sensible timeout can also be calculated dynamically. synwait: Optional[int] - def __init__(self, devout, devin, synwait=None): + def __init__(self, devout, devin, synwait=None,tape_size=12): """Initialize the LabelManager object. (HLF)""" + self.tape_size = tape_size self.cmd: list[int] = [] self.response = False self.bytesPerLine_ = None @@ -118,7 +122,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: + if value < 0 or value > self.max_bytes_per_line(self.tape_size): raise ValueError cmd = [ESC, ord("B"), value] self.buildCommand(cmd) @@ -136,7 +140,7 @@ def tapeColor(self, value): def bytesPerLine(self, value): """Set the number of bytes sent in the following lines. (MLF)""" - if value < 0 or value + self.dotTab_ > self._MAX_BYTES_PER_LINE: + if value < 0 or value + self.dotTab_ > self.max_bytes_per_line(self.tape_size): raise ValueError if value == self.bytesPerLine_: return @@ -161,8 +165,8 @@ def chainMark(self): """Set Chain Mark. (MLF)""" self.dotTab(0) - self.bytesPerLine(self._MAX_BYTES_PER_LINE) - self.line([0x99] * self._MAX_BYTES_PER_LINE) + self.bytesPerLine(self.max_bytes_per_line(self.tape_size)) + self.line([0x99] * self.max_bytes_per_line(self.tape_size)) def skipLines(self, value): """Set number of lines of white to print. (MLF)""" From 0285451485785adf665037bfe7c0310684a70949 Mon Sep 17 00:00:00 2001 From: Michal Roman Date: Wed, 3 May 2023 18:48:45 +0200 Subject: [PATCH 03/17] introduction of DymoRenderEngine & DymoPrinterServer --- src/dymoprint/dymo_print_engines.py | 307 ++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 src/dymoprint/dymo_print_engines.py diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py new file mode 100644 index 0000000..bd1bdef --- /dev/null +++ b/src/dymoprint/dymo_print_engines.py @@ -0,0 +1,307 @@ +import array +import math +import os + +import barcode as barcode_module +import usb +from PIL import Image, ImageOps +from PIL import ImageFont + +from . import DymoLabeler +from .barcode_writer import BarcodeImageWriter +from .constants import (DEV_CLASS, DEV_LM280_CLASS, DEV_LM280_PRODUCT, DEV_NAME, DEV_NODE, DEV_PRODUCT, DEV_VENDOR, ) +from .constants import (QRCode, ) +from .utils import access_error, die, getDeviceFile +from .utils import draw_image, scaling + + +class DymoRenderEngine: + def __init__(self, tape_size=12): + """ + Initializes a DymoRenderEngine object with a specified tape size. + + Args: + tape_size (int): The size of the tape in millimeters. Default is 12mm. + """ + self.tape_size = tape_size + + def render_empty(self): + """ + Renders an empty label image. + + Returns: + Image: An empty label image. + """ + label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 + return Image.new("1", (1, label_height)) + + def render_qr(self, qr_input_text): + """ + Renders a QR code image from the input text. + + Args: + qr_input_text (str): The input text to be encoded in the QR code. + + Returns: + Image: A QR code image. + """ + label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 + if len(qr_input_text) == 0: + return Image.new("1", (1, label_height)) + + # create QR object from first string + code = QRCode(qr_input_text, error="M") + qr_text = code.text(quiet_zone=1).split() + + # create an empty label image + qr_scale = label_height // len(qr_text) + qr_offset = (label_height - len(qr_text) * qr_scale) // 2 + label_width = (len(qr_text) * qr_scale) + + if not qr_scale: + die("Error: too much information to store in the QR code, points are smaller than the device resolution") + + code_bitmap = Image.new("1", (label_width, label_height)) + + with draw_image(code_bitmap) as label_draw: + # write the qr-code into the empty image + for i, line in enumerate(qr_text): + for j in range(len(line)): + if line[j] == "1": + pix = scaling((j * qr_scale, i * qr_scale + qr_offset), qr_scale) + label_draw.point(pix, 255) + return code_bitmap + + def render_barcode(self, barcode_input_text, bar_code_type): + """ + Renders a barcode image from the input text and barcode type. + + Args: + barcode_input_text (str): The input text to be encoded in the barcode. + bar_code_type (str): The type of barcode to be rendered. + + Returns: + Image: A barcode image. + """ + label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 + if len(barcode_input_text) == 0: + return Image.new("1", (1, label_height)) + + code = barcode_module.get(bar_code_type, barcode_input_text, writer=BarcodeImageWriter()) + code_bitmap = code.render( + { + "font_size": 0, + "vertical_margin": 8, + "module_height": (DymoLabeler.max_bytes_per_line(self.tape_size) * 8) - 16, + "module_width": 2, + "background": "black", + "foreground": "white", + } + ) + return code_bitmap + + def render_text(self, labeltext: list[str], font_file_name: str, frame_width, font_size_ratio=0.9): + """ + Renders a text image from the input text, font file name, frame width, and font size ratio. + + Args: + labeltext (list[str]): The input text to be rendered. + font_file_name (str): The name of the font file to be used. + frame_width (int): The width of the frame around the text. + font_size_ratio (float): The ratio of font size to line height. Default is 1. + + Returns: + Image: A text image. + """ + if type(labeltext) is str: + labeltext = [labeltext] + + if len(labeltext) == 0: + labeltext = [' '] + + # create an empty label image + label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 + line_height = float(label_height) / len(labeltext) + fontsize = int(round(line_height * font_size_ratio)) + + font_offset = int((line_height - fontsize) / 2) + + if frame_width: + frame_width = min(frame_width, font_offset) + frame_width = min(frame_width, 3) + + font = ImageFont.truetype(font_file_name, fontsize) + label_width = max(font.getsize(line)[0] for line in labeltext) + (font_offset * 2) + text_bitmap = Image.new("1", (label_width, label_height)) + with draw_image(text_bitmap) as label_draw: + + # draw frame into empty image + if frame_width: + label_draw.rectangle( + ((0, 4), (label_width - 1, label_height - 4)), fill=255 + ) + label_draw.rectangle( + ( + (frame_width, 4 + frame_width), + (label_width - (frame_width + 1), label_height - (frame_width + 4)), + ), + fill=0, + ) + + # write the text into the empty image + for i, line in enumerate(labeltext): + line_position = int(round(i * line_height)) + label_draw.text((font_offset, line_position + font_offset), line, font=font, fill=255) + return text_bitmap + + def render_picture(self, picture_path: str): + """ + Renders a picture image from the input picture path. + + Args: + picture_path (str): The path of the picture to be rendered. + + Returns: + Image: A picture image. + """ + if len(picture_path): + if os.path.exists(picture_path): + label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 + with Image.open(picture_path) as img: + if img.height > label_height: + ratio = label_height / img.height + img.thumbnail( + (int(math.ceil(img.width * ratio)), label_height), Image.ANTIALIAS + ) + return ImageOps.invert(img).convert("1") + else: + die(f"picture path:{picture_path} doesn't exist ") + label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 + return Image.new("1", (1, label_height)) + + @staticmethod + def merge_render(bitmaps): + """ + Merges multiple images into a single image. + + Args: + bitmaps (list[Image]): A list of images to be merged. + + Returns: + Image: A merged image. + """ + if len(bitmaps) > 1: + padding = 4 + label_bitmap = Image.new( + "1", + ( + sum(b.width for b in bitmaps) + padding * (len(bitmaps) - 1), + bitmaps[0].height, + ), + ) + offset = 0 + for bitmap in bitmaps: + label_bitmap.paste(bitmap, box=(offset, 0)) + offset += bitmap.width + padding + else: + label_bitmap = bitmaps[0] + return label_bitmap + + +class DymoPrinterServer: + @staticmethod + def print_label(label_bitmap, margin=56, tape_size: int = 12): + """ + Prints a label using a Dymo labeler object. + + :param label_bitmap: The image to be printed as a label. + :type label_bitmap: Image + :param margin: The margin size in dots. Default is 56. + :type margin: int, optional + :param tape_size: The size of the tape in millimeters. Default is 12. + :type tape_size: int, optional + :return: None + :rtype: None + """ + # convert the image to the proper matrix for the dymo labeler object + label_rotated = label_bitmap.transpose(Image.ROTATE_270) + labelstream = label_rotated.tobytes() + 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 = [ + 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] + # get device file name + if not DEV_NODE: + dev = getDeviceFile(DEV_CLASS, DEV_VENDOR, DEV_PRODUCT) + else: + dev = DEV_NODE + + if dev: + devout = open(dev, "rb+") + devin = devout + # We are in the normal HID file mode, so no syn_wait is needed. + syn_wait = None + else: + # We are in the experimental PyUSB mode, if a device can be found. + syn_wait = 64 + # Find and prepare device communication endpoints. + dev = usb.core.find( + custom_match=lambda d: ( + d.idVendor == DEV_VENDOR and d.idProduct == DEV_LM280_PRODUCT + ) + ) + + if dev is None: + die("The device '%s' could not be found on this system." % DEV_NAME) + else: + print("Entering experimental PyUSB mode.") + + try: + dev.set_configuration() + except usb.core.USBError as e: + if e.errno == 13: + raise RuntimeError("Access denied") + if e.errno == 16: + # Resource busy + pass + else: + raise + + intf = usb.util.find_descriptor( + dev.get_active_configuration(), bInterfaceClass=DEV_LM280_CLASS + ) + if dev.is_kernel_driver_active(intf.bInterfaceNumber): + dev.detach_kernel_driver(intf.bInterfaceNumber) + devout = usb.util.find_descriptor( + intf, + custom_match=( + lambda e: usb.util.endpoint_direction(e.bEndpointAddress) + == usb.util.ENDPOINT_OUT + ), + ) + devin = usb.util.find_descriptor( + intf, + custom_match=( + lambda e: usb.util.endpoint_direction(e.bEndpointAddress) + == usb.util.ENDPOINT_IN + ), + ) + + if not devout or not devin: + die("The device '%s' could not be found on this system." % DEV_NAME) + + # create dymo labeler object + try: + lm = DymoLabeler(devout, devin, synwait=syn_wait, tape_size=tape_size) + except IOError: + die(access_error(dev)) + + print("Printing label..") + if margin is not None: + lm.printLabel(label_matrix, margin=margin) + else: + lm.printLabel(label_matrix) From 2318ad9028aa2924c1f88094f760635162b04473 Mon Sep 17 00:00:00 2001 From: Michal Roman Date: Wed, 3 May 2023 18:49:28 +0200 Subject: [PATCH 04/17] refactor command_line to use DymoRenderEngine --- src/dymoprint/command_line.py | 227 +++------------------------------- 1 file changed, 19 insertions(+), 208 deletions(-) diff --git a/src/dymoprint/command_line.py b/src/dymoprint/command_line.py index 2e67c34..abe3958 100755 --- a/src/dymoprint/command_line.py +++ b/src/dymoprint/command_line.py @@ -7,38 +7,23 @@ # === END LICENSE STATEMENT === import argparse -import array -import math import os -import barcode as barcode_module -import usb -from PIL import Image, ImageFont, ImageOps +from PIL import Image, ImageOps -from . import DymoLabeler, __version__ -from .barcode_writer import BarcodeImageWriter +from . import __version__ from .constants import ( - DEV_CLASS, - DEV_LM280_CLASS, - DEV_LM280_NAME, - DEV_LM280_PRODUCT, - DEV_NAME, - DEV_NODE, - DEV_PRODUCT, - DEV_VENDOR, - FONT_SIZERATIO, USE_QR, - QRCode, e_qrcode, ) +from .dymo_print_engines import DymoPrinterServer, DymoRenderEngine from .font_config import font_filename from .metadata import our_metadata from .unicode_blocks import image_to_unicode -from .utils import access_error, die, draw_image, getDeviceFile, scaling +from .utils import die def parse_args(): - # check for any text specified on the command line parser = argparse.ArgumentParser(description=our_metadata["Summary"]) parser.add_argument( @@ -105,12 +90,15 @@ def parse_args(): ) parser.add_argument("-p", "--picture", help="Print the specified picture") parser.add_argument("-m", type=int, help="Override margin (default is 56*2)") - # parser.add_argument('-t',type=int,choices=[6, 9, 12],default=12,help='Tape size: 6,9,12 mm, default=12mm') + parser.add_argument("--scale", type=int, default=90, help="Scaling font factor, [0,10] %") + parser.add_argument('-t', type=int, choices=[6, 9, 12], default=12, help='Tape size: 6,9,12 mm, default=12mm') return parser.parse_args() def main(): args = parse_args() + print_server = DymoPrinterServer() + render_engine = DymoRenderEngine(args.t) # read config file FONT_FILENAME = font_filename(args.s) @@ -133,206 +121,29 @@ def main(): bitmaps = [] if args.qr: - # create QR object from first string - code = QRCode(labeltext.pop(0), error="M") - qr_text = code.text(quiet_zone=1).split() - - # create an empty label image - labelheight = DymoLabeler._MAX_BYTES_PER_LINE * 8 - labelwidth = labelheight - qr_scale = labelheight // len(qr_text) - qr_offset = (labelheight - len(qr_text) * qr_scale) // 2 - - if not qr_scale: - die( - "Error: too much information to store in the QR code, points are smaller than the device resolution" - ) - - codebitmap = Image.new("1", (labelwidth, labelheight)) - - with draw_image(codebitmap) as labeldraw: - # write the qr-code into the empty image - for i, line in enumerate(qr_text): - for j in range(len(line)): - if line[j] == "1": - pix = scaling( - (j * qr_scale, i * qr_scale + qr_offset), qr_scale - ) - labeldraw.point(pix, 255) - - bitmaps.append(codebitmap) + bitmaps.append(render_engine.render_qr(labeltext.pop(0))) elif args.c: - code = barcode_module.get(args.c, labeltext.pop(0), writer=BarcodeImageWriter()) - codebitmap = code.render( - { - "font_size": 0, - "vertical_margin": 8, - "module_height": (DymoLabeler._MAX_BYTES_PER_LINE * 8) - 16, - "module_width": 2, - "background": "black", - "foreground": "white", - } - ) - - bitmaps.append(codebitmap) + bitmaps.append(render_engine.render_barcode(labeltext.pop(0), args.c)) if labeltext: - if args.f == None: - fontoffset = 0 - else: - fontoffset = min(args.f, 3) - - # create an empty label image - labelheight = DymoLabeler._MAX_BYTES_PER_LINE * 8 - lineheight = float(labelheight) / len(labeltext) - fontsize = int(round(lineheight * FONT_SIZERATIO)) - font = ImageFont.truetype(FONT_FILENAME, fontsize) - labelwidth = max(font.getsize(line)[0] for line in labeltext) + (fontoffset * 2) - textbitmap = Image.new("1", (labelwidth, labelheight)) - with draw_image(textbitmap) as labeldraw: - - # draw frame into empty image - if args.f is not None: - labeldraw.rectangle( - ((0, 0), (labelwidth - 1, labelheight - 1)), fill=255 - ) - labeldraw.rectangle( - ( - (fontoffset, fontoffset), - (labelwidth - (fontoffset + 1), labelheight - (fontoffset + 1)), - ), - fill=0, - ) - - # write the text into the empty image - for i, line in enumerate(labeltext): - lineposition = int(round(i * lineheight)) - labeldraw.text((fontoffset, lineposition), line, font=font, fill=255) - - bitmaps.append(textbitmap) + bitmaps.append(render_engine.render_text(labeltext, FONT_FILENAME, args.f, int(args.scale) / 100.0)) if args.picture: - labelheight = DymoLabeler._MAX_BYTES_PER_LINE * 8 - with Image.open(args.picture) as img: - if img.height > labelheight: - ratio = labelheight / img.height - img.thumbnail( - (int(math.ceil(img.width * ratio)), labelheight), Image.ANTIALIAS - ) - bitmaps.append(ImageOps.invert(img).convert("1")) - - if len(bitmaps) > 1: - padding = 4 - labelbitmap = Image.new( - "1", - ( - sum(b.width for b in bitmaps) + padding * (len(bitmaps) - 1), - bitmaps[0].height, - ), - ) - offset = 0 - for bitmap in bitmaps: - labelbitmap.paste(bitmap, box=(offset, 0)) - offset += bitmap.width + padding - else: - labelbitmap = bitmaps[0] + bitmaps.append(render_engine.render_picture(args.picture)) - # convert the image to the proper matrix for the dymo labeler object - labelrotated = labelbitmap.transpose(Image.ROTATE_270) - labelstream = labelrotated.tobytes() - labelstreamrowlength = int(math.ceil(labelbitmap.height / 8)) - if len(labelstream) // labelstreamrowlength != labelbitmap.width: - die("An internal problem was encountered while processing the label " "bitmap!") - labelrows = [ - labelstream[i : i + labelstreamrowlength] - for i in range(0, len(labelstream), labelstreamrowlength) - ] - labelmatrix = [array.array("B", labelrow).tolist() for labelrow in labelrows] + label_bitmap = render_engine.merge_render(bitmaps) # print or show the label if args.preview or args.preview_inverted or args.imagemagick: print("Demo mode: showing label..") # fix size, adding print borders - labelimage = Image.new("L", (56 + labelbitmap.width + 56, labelbitmap.height)) - labelimage.paste(labelbitmap, (56, 0)) + label_image = Image.new("L", (56 + label_bitmap.width + 56, label_bitmap.height)) + label_image.paste(label_bitmap, (56, 0)) if args.preview or args.preview_inverted: - print(image_to_unicode(labelrotated, invert=args.preview_inverted)) + label_rotated = label_bitmap.transpose(Image.ROTATE_270) + print(image_to_unicode(label_rotated, invert=args.preview_inverted)) if args.imagemagick: - ImageOps.invert(labelimage).show() + ImageOps.invert(label_image).show() else: - # get device file name - if not DEV_NODE: - dev = getDeviceFile(DEV_CLASS, DEV_VENDOR, DEV_PRODUCT) - else: - dev = DEV_NODE - - if dev: - devout = open(dev, "rb+") - devin = devout - # We are in the normal HID file mode, so no synwait is needed. - synwait = None - else: - # We are in the experimental PyUSB mode, if a device can be found. - synwait = 64 - # Find and prepare device communication endpoints. - dev = usb.core.find( - custom_match=lambda d: ( - d.idVendor == DEV_VENDOR and d.idProduct == DEV_LM280_PRODUCT - ) - ) - - if dev is None: - device_not_found() - else: - print("Entering experimental PyUSB mode.") - - try: - dev.set_configuration() - except usb.core.USBError as e: - if e.errno == 13: - raise RuntimeError("Access denied") - if e.errno == 16: - # Resource busy - pass - else: - raise - - intf = usb.util.find_descriptor( - dev.get_active_configuration(), bInterfaceClass=DEV_LM280_CLASS - ) - if dev.is_kernel_driver_active(intf.bInterfaceNumber): - dev.detach_kernel_driver(intf.bInterfaceNumber) - devout = usb.util.find_descriptor( - intf, - custom_match=( - lambda e: usb.util.endpoint_direction(e.bEndpointAddress) - == usb.util.ENDPOINT_OUT - ), - ) - devin = usb.util.find_descriptor( - intf, - custom_match=( - lambda e: usb.util.endpoint_direction(e.bEndpointAddress) - == usb.util.ENDPOINT_IN - ), - ) - - if not devout or not devin: - device_not_found() - - # create dymo labeler object - try: - lm = DymoLabeler(devout, devin, synwait=synwait) - except IOError: - die(access_error(dev)) - - print("Printing label..") - if args.m is not None: - lm.printLabel(labelmatrix, margin=args.m) - else: - lm.printLabel(labelmatrix) - - -def device_not_found(): - die("The device '%s' could not be found on this system." % DEV_NAME) + print_server.print_label(label_bitmap, margin=args.m) From 8e5e5ac4cca723a762244bef5eda16e96716b6c2 Mon Sep 17 00:00:00 2001 From: Michal Roman Date: Wed, 3 May 2023 18:50:24 +0200 Subject: [PATCH 05/17] introduction of q_dymo_label_widgets --- src/dymoprint/q_dymo_label_widgets.py | 285 ++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/dymoprint/q_dymo_label_widgets.py diff --git a/src/dymoprint/q_dymo_label_widgets.py b/src/dymoprint/q_dymo_label_widgets.py new file mode 100644 index 0000000..3f05a6e --- /dev/null +++ b/src/dymoprint/q_dymo_label_widgets.py @@ -0,0 +1,285 @@ +import os + +import dymoprint_fonts +from PyQt5 import QtCore +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QSpinBox, QPlainTextEdit, QLineEdit, QPushButton, \ + QFileDialog, QMessageBox + +from .font_config import parse_fonts + + +class BaseDymoLabelWidget(QWidget): + """ + A base class for creating Dymo label widgets. + Signals: + -------- + itemRenderSignal : PyQtSignal + Signal emitted when the content of the label is changed. + Methods: + -------- + content_changed() + Emits the itemRenderSignal when the content of the label is changed. + render_label() + Abstract method to be implemented by subclasses for rendering the label. + """ + itemRenderSignal = QtCore.pyqtSignal(name='itemRenderSignal') + + def content_changed(self): + """ + Emits the itemRenderSignal when the content of the label is changed. + """ + self.itemRenderSignal.emit() + + def render_label(self): + """ + Abstract method to be implemented by subclasses for rendering the label. + """ + pass + + +class TextDymoLabelWidget(BaseDymoLabelWidget): + """ + A widget for rendering text on a Dymo label. + Args: + render_engine (RenderEngine): The rendering engine to use. + parent (QWidget): The parent widget of this widget. + Attributes: + render_engine (RenderEngine): The rendering engine used by this widget. + label (QPlainTextEdit): The text label to be rendered on the Dymo label. + font_style (QComboBox): The font style selection dropdown. + font_size (QSpinBox): The font size selection spinner. + draw_frame (QSpinBox): The frame width selection spinner. + Signals: + itemRenderSignal: A signal emitted when the content of the label changes. + """ + + def __init__(self, render_engine, parent=None): + super(TextDymoLabelWidget, self).__init__(parent) + self.render_engine = render_engine + + self.label = QPlainTextEdit("text") + self.label.setFixedHeight(15 * (len(self.label.toPlainText().splitlines()) + 2)) + self.setFixedHeight(self.label.height() + 10) + self.font_style = QComboBox() + self.font_size = QSpinBox() + self.font_size.setMaximum(100) + self.font_size.setMinimum(0) + self.font_size.setSingleStep(1) + self.font_size.setValue(90) + self.draw_frame = QSpinBox() + for (name, font_path) in parse_fonts(): + self.font_style.addItem(name, font_path) + if "Regular" in name: + self.font_style.setCurrentText(name) + + layout = QHBoxLayout() + ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) + item_icon = QLabel() + item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "txt_icon.png")).pixmap(32, 32)) + item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) + layout.addWidget(item_icon) + layout.addWidget(self.label) + layout.addWidget(QLabel("Font:")) + layout.addWidget(self.font_style) + layout.addWidget(QLabel("Size [%]:")) + layout.addWidget(self.font_size) + layout.addWidget(QLabel("Frame Width:")) + layout.addWidget(self.draw_frame) + self.label.textChanged.connect(self.content_changed) + self.draw_frame.valueChanged.connect(self.content_changed) + self.font_size.valueChanged.connect(self.content_changed) + self.font_style.currentTextChanged.connect(self.content_changed) + self.setLayout(layout) + + def content_changed(self): + """ + Updates the height of the label and emits the itemRenderSignal when the content of the label changes. + """ + self.label.setFixedHeight(15 * (len(self.label.toPlainText().splitlines()) + 2)) + self.setFixedHeight(self.label.height() + 10) + self.itemRenderSignal.emit() + + def render_label(self): + """ + Renders the label using the current settings. + Returns: + QImage: The rendered label image. + Raises: + QMessageBox.warning: If the rendering fails. + """ + try: + render = self.render_engine.render_text(labeltext=self.label.toPlainText().splitlines(), + font_file_name=self.font_style.currentData(), + frame_width=self.draw_frame.value(), + font_size_ratio=self.font_size.value() / 100.0) + return render + except BaseException as err: + QMessageBox.warning(self, "TextDymoLabelWidget render fail!", f"{err}") + return self.render_engine.render_empty() + + +class QrDymoLabelWidget(BaseDymoLabelWidget): + """ + A widget for rendering QR codes on Dymo labels. + Args: + render_engine (RenderEngine): The render engine to use for rendering the QR code. + parent (QWidget, optional): The parent widget. Defaults to None. + """ + + def __init__(self, render_engine, parent=None): + """ + Initializes the QrDymoLabelWidget. + Args: + render_engine (RenderEngine): The render engine to use for rendering the QR code. + parent (QWidget, optional): The parent widget. Defaults to None. + """ + super(QrDymoLabelWidget, self).__init__(parent) + self.render_engine = render_engine + + self.label = QLineEdit("") + layout = QHBoxLayout() + ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) + item_icon = QLabel() + item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "qr_icon.png")).pixmap(32, 32)) + item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) + layout.addWidget(item_icon) + layout.addWidget(self.label) + self.label.textChanged.connect(self.content_changed) + self.setLayout(layout) + + def render_label(self): + """ + Renders the QR code on the Dymo label. + Returns: + bytes: The rendered QR code as bytes. + Raises: + QMessageBox.warning: If the rendering fails. + """ + try: + render = self.render_engine.render_qr(self.label.text()) + return render + + except BaseException as err: + QMessageBox.warning(self, "QrDymoLabelWidget render fail!", f"{err}") + return self.render_engine.render_empty() + + +class BarcodeDymoLabelWidget(BaseDymoLabelWidget): + """ + A widget for rendering barcode labels using the Dymo label printer. + Args: + render_engine (DymoRenderEngine): An instance of the DymoRenderEngine class. + parent (QWidget): The parent widget of this widget. + Attributes: + render_engine (DymoRenderEngine): An instance of the DymoRenderEngine class. + label (QLineEdit): A QLineEdit widget for entering the content of the barcode label. + codding (QComboBox): A QComboBox widget for selecting the type of barcode to render. + Signals: + content_changed(): Emitted when the content of the label or the selected barcode type changes. + Methods: + __init__(self, render_engine, parent=None): Initializes the widget. + render_label(self): Renders the barcode label using the current content and barcode type. + """ + + def __init__(self, render_engine, parent=None): + super(BarcodeDymoLabelWidget, self).__init__(parent) + self.render_engine = render_engine + + self.label = QLineEdit("") + layout = QHBoxLayout() + ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) + item_icon = QLabel() + item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "barcode_icon.png")).pixmap(32, 32)) + item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) + self.codding = QComboBox() + self.codding.addItems([ + "code39", + "code128", + "ean", + "ean13", + "ean8", + "gs1", + "gtin", + "isbn", + "isbn10", + "isbn13", + "issn", + "jan", + "pzn", + "upc", + "upca", + ]) + + layout.addWidget(item_icon) + layout.addWidget(self.label) + layout.addWidget(QLabel("Codding:")) + layout.addWidget(self.codding) + + self.label.textChanged.connect(self.content_changed) + self.codding.currentTextChanged.connect(self.content_changed) + self.setLayout(layout) + + def render_label(self): + """ + Renders the barcode label using the current content and barcode type. + Returns: + QPixmap: A QPixmap object representing the rendered barcode label. + """ + try: + render = self.render_engine.render_barcode(self.label.text(), self.codding.currentText()) + return render + + except BaseException as err: + QMessageBox.warning(self, "BarcodeDymoLabelWidget render fail!", f"{err}") + return self.render_engine.render_empty() + + +class ImageDymoLabelWidget(BaseDymoLabelWidget): + """ + A widget for rendering image-based Dymo labels. + Args: + render_engine (RenderEngine): The render engine to use for rendering the label. + parent (QWidget, optional): The parent widget. Defaults to None. + """ + + def __init__(self, render_engine, parent=None): + """ + Initializes the ImageDymoLabelWidget. + Args: + render_engine (RenderEngine): The render engine to use for rendering the label. + parent (QWidget, optional): The parent widget. Defaults to None. + """ + super(ImageDymoLabelWidget, self).__init__(parent) + self.render_engine = render_engine + + self.label = QLineEdit("") + layout = QHBoxLayout() + ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) + item_icon = QLabel() + item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "img_icon.png")).pixmap(32, 32)) + item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) + + button = QPushButton('Select file') + file_dialog = QFileDialog() + button.clicked.connect(lambda: self.label.setText(file_dialog.getOpenFileName()[0])) + + layout.addWidget(item_icon) + layout.addWidget(self.label) + layout.addWidget(button) + + self.label.textChanged.connect(self.content_changed) + self.setLayout(layout) + + def render_label(self): + """ + Renders the label using the render engine and the selected image file. + Returns: + QPixmap: The rendered label as a QPixmap. + """ + try: + render = self.render_engine.render_picture(self.label.text()) + return render + except BaseException as err: + QMessageBox.warning(self, "ImageDymoLabelWidget render fail!", f"{err}") + return self.render_engine.render_empty() From 7b8da164be32049aeb0215c1ea86455407b5eeef Mon Sep 17 00:00:00 2001 From: Michal Roman Date: Wed, 3 May 2023 18:50:54 +0200 Subject: [PATCH 06/17] introduction of q_dymo_labels_list --- src/dymoprint/q_dymo_labels_list.py | 128 ++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/dymoprint/q_dymo_labels_list.py diff --git a/src/dymoprint/q_dymo_labels_list.py b/src/dymoprint/q_dymo_labels_list.py new file mode 100644 index 0000000..4c82ee0 --- /dev/null +++ b/src/dymoprint/q_dymo_labels_list.py @@ -0,0 +1,128 @@ +import PIL +from PIL.Image import Image +from PyQt5 import QtCore +from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QAbstractItemView, QMenu +from .q_dymo_label_widgets import TextDymoLabelWidget, QrDymoLabelWidget, BarcodeDymoLabelWidget, \ + ImageDymoLabelWidget + + +class QDymoLabelList(QListWidget): + """ + A custom QListWidget for displaying and managing Dymo label widgets. + Args: + render_engine (RenderEngine): The render engine to use for rendering the label. + parent (QWidget): The parent widget of this QListWidget. + Attributes: + renderSignal (QtCore.pyqtSignal): A signal emitted when the label is rendered. + render_engine (RenderEngine): The render engine used for rendering the label. + Methods: + __init__(self, render_engine, parent=None): Initializes the QListWidget with the given render engine and parent. + dropEvent(self, e) -> None: Overrides the default drop event to update the label rendering. + update_render_engine(self, render_engine): Updates the render engine used for rendering the label. + render_label(self): Renders the label using the current render engine and emits the renderSignal. + contextMenuEvent(self, event): Overrides the default context menu event to add or delete label widgets. + """ + + renderSignal = QtCore.pyqtSignal(PIL.Image.Image, name='renderSignal') + + def __init__(self, render_engine, parent=None): + super(QDymoLabelList, self).__init__(parent) + self.render_engine = render_engine + self.setAlternatingRowColors(True) + self.setDragDropMode(QAbstractItemView.InternalMove) + for item_widget in [TextDymoLabelWidget(self.render_engine) ]: + item = QListWidgetItem(self) + item.setSizeHint(item_widget.sizeHint()) + self.addItem(item) + self.setItemWidget(item, item_widget) + item_widget.itemRenderSignal.connect(self.render_label) + self.render_label() + + def dropEvent(self, e) -> None: + """ + Overrides the default drop event to update the label rendering. + Args: + e (QDropEvent): The drop event. + """ + super().dropEvent(e) + self.render_label() + + def update_render_engine(self, render_engine): + """ + Updates the render engine used for rendering the label. + Args: + render_engine (RenderEngine): The new render engine to use. + """ + self.render_engine = render_engine + for i in range(self.count()): + item_widget = self.itemWidget(self.item(i)) + item_widget.render_engine = render_engine + self.render_label() + + def render_label(self): + """ + Renders the label using the current render engine and emits the renderSignal. + """ + bitmaps = [] + for i in range(self.count()): + item = self.item(i) + item_widget = self.itemWidget(self.item(i)) + if item_widget and item: + item.setSizeHint(item_widget.sizeHint()) + bitmaps.append(item_widget.render_label()) + label_bitmap = self.render_engine.merge_render(bitmaps) + + self.renderSignal.emit(label_bitmap) + + def contextMenuEvent(self, event): + """ + Overrides the default context menu event to add or delete label widgets. + Args: + event (QContextMenuEvent): The context menu event. + """ + contextMenu = QMenu(self) + add_text = contextMenu.addAction("AddText") + add_qr = contextMenu.addAction("AddQR") + add_barcode = contextMenu.addAction("AddBarcode") + add_img = contextMenu.addAction("AddImage") + delete = contextMenu.addAction("Delete") + menu_click = contextMenu.exec(event.globalPos()) + + if menu_click == add_text: + item = QListWidgetItem(self) + item_widget = TextDymoLabelWidget(self.render_engine) + item.setSizeHint(item_widget.sizeHint()) + self.addItem(item) + self.setItemWidget(item, item_widget) + item_widget.itemRenderSignal.connect(self.render_label) + + if menu_click == add_qr: + item = QListWidgetItem(self) + item_widget = QrDymoLabelWidget(self.render_engine) + item.setSizeHint(item_widget.sizeHint()) + self.addItem(item) + self.setItemWidget(item, item_widget) + item_widget.itemRenderSignal.connect(self.render_label) + + if menu_click == add_barcode: + item = QListWidgetItem(self) + item_widget = BarcodeDymoLabelWidget(self.render_engine) + item.setSizeHint(item_widget.sizeHint()) + self.addItem(item) + self.setItemWidget(item, item_widget) + item_widget.itemRenderSignal.connect(self.render_label) + + if menu_click == add_img: + item = QListWidgetItem(self) + item_widget = ImageDymoLabelWidget(self.render_engine) + item.setSizeHint(item_widget.sizeHint()) + self.addItem(item) + self.setItemWidget(item, item_widget) + item_widget.itemRenderSignal.connect(self.render_label) + if menu_click == delete: + try: + item = self.itemAt(event.pos()) + self.takeItem(self.indexFromItem(item).row()) # self.update() + except Exception as e: + print(f"No item selected {e}") + self.render_label() From e0a4c75685b0bf9f0784ae0eedba86f31dd415fd Mon Sep 17 00:00:00 2001 From: Michal Roman Date: Wed, 3 May 2023 18:55:31 +0200 Subject: [PATCH 07/17] added DymoPrint GUI --- README.md | 51 ++++++++- TODO.md | 4 +- data/fonts/barcode_icon.png | Bin 0 -> 6027 bytes data/fonts/img_icon.png | Bin 0 -> 5193 bytes data/fonts/qr_icon.png | Bin 0 -> 282 bytes data/fonts/txt_icon.png | Bin 0 -> 3506 bytes doc/DymoPrint_example_1.png | Bin 0 -> 34893 bytes doc/DymoPrint_example_2.png | Bin 0 -> 40573 bytes doc/DymoPrint_example_3.png | Bin 0 -> 37194 bytes setup.cfg | 3 + src/dymoprint/dymo_print_engines.py | 11 +- src/dymoprint/font_config.py | 14 ++- src/dymoprint/gui.py | 147 ++++++++++++++++++++++++++ src/dymoprint/q_dymo_label_widgets.py | 44 +++++--- src/dymoprint/q_dymo_labels_list.py | 15 +-- src/dymoprint/utils.py | 15 ++- 16 files changed, 263 insertions(+), 41 deletions(-) create mode 100644 data/fonts/barcode_icon.png create mode 100644 data/fonts/img_icon.png create mode 100644 data/fonts/qr_icon.png create mode 100644 data/fonts/txt_icon.png create mode 100644 doc/DymoPrint_example_1.png create mode 100644 doc/DymoPrint_example_2.png create mode 100644 doc/DymoPrint_example_3.png create mode 100644 src/dymoprint/gui.py diff --git a/README.md b/README.md index 1d9457f..04a107f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Linux Software to print with LabelManager PnP from Dymo * Supports barcode printing * Supports image printing * Supports combined barcode / qrcode and text printing +* GUI Application based on PyQt6 - expanded combinations ## Installation @@ -158,6 +159,52 @@ Any picture with JPEG standard may be printed. Beware it will be downsized to ta Take care of the trailing "" - you may enter text here which gets printed in front of the image +## GUI + +### Run DymoPrint GUI + +```dymoprint_gui``` + + +### Features +* Live preview +* margin settings +* type size selector +* visualization of tape color schema +* the ability to freely arrange the content using the "Node" list + * Text Node: + * payload text - can be multi-line + * font selector + * font scaling - the percentage of line-height + * frame border width steering + * Qr Node: + * payload text + * BarCode Node: + * payload text + * codding selector + * Image Node: + * path to file + +Nodes can be freely arranged, simply drag&drop rows on the list. +To add or delete the node from the label - right-click on the list and select the action from the context menu. +To print - click the print button. + +### Example + +Example 1: multiple text + QR code + +![alt](doc/DymoPrint_example_1.png) + +Example 2: two images + text with frame, white on red + +![alt](doc/DymoPrint_example_2.png) + +Example 3: barcode, text, image + +![alt](doc/DymoPrint_example_3.png) + + + ## Development Besides the travis-ci one should run the following command on a feature implemention or change to ensure the same outcome on a real device: @@ -173,10 +220,10 @@ dymoprint -c code128 Test "bc_txt" ### ToDo * (?)support multiple ProductIDs (1001, 1002) -> use usb-modeswitch? -* put everything in classes that would need to be used by a GUI +* ~~put everything in classes that would need to be used by a GUI~~ * ~~for more options use command line parser framework~~ * ~~allow selection of font with command line options~~ -* allow font size specification with command line option (points, pixels?) +* ~~allow font size specification with command line option (points, pixels?)~~ * ~~provide an option to show a preview of what the label will look like~~ * ~~read and write a .dymoprint file containing user preferences~~ * ~~print barcodes~~ diff --git a/TODO.md b/TODO.md index 18a722a..595d1e2 100755 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,8 @@ - [ ] support multiple ProductIDs (1001, 1002) -> use usb-modeswitch? -- [ ] put everything in classes that would need to be used by a GUI +- [x] put everything in classes that would need to be used by a GUI - [x] for more options use command line parser framework - [x] allow selection of font with command line options -- [ ] allow font size specification with command line option (points, pixels?) +- [x] allow font size specification with command line option (points, pixels?) - [x] provide an option to show a preview of what the label will look like - [x] read and write a .dymoprint file containing user preferences - [ ] print graphics and barcodes diff --git a/data/fonts/barcode_icon.png b/data/fonts/barcode_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3da207ecb469caf0521b45d8f47872ade98efd5f GIT binary patch literal 6027 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 Uh1cy1_u3C&Z{uj)WQn=;f0RqWlmGw# literal 0 HcmV?d00001 diff --git a/data/fonts/qr_icon.png b/data/fonts/qr_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5f8b8fd659133f856e23edb854ed0be8735fc21b GIT binary patch literal 282 zcmV+#0pT z!nfGwJsS~n1VzYFj);FFa2AQ-0weiB;7c1=Qw&;IJW?|mA~Y7;{k6!K9u6MpiY_*P gvaApEoBT|UC%#sci0wG{-~a#s07*qoM6N<$f~g02DgXcg literal 0 HcmV?d00001 diff --git a/data/fonts/txt_icon.png b/data/fonts/txt_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5724f777fc0b8ec16de8d183b258d50f6fbb71 GIT binary patch literal 3506 zcmeHK`8(A67oWw9ZJ1jZWh>bVHP%rQb1T)1We6dRB_YDd(kR;W*~%8->;871=YF5>{r&;p_w$_hdCvRwyw7=`_Ydd1PJ#{ooP?O1 z7zhNCz+ug7cl5%aA_CvpO)m)OJ4z_@6wY2`XGMuzzPn?K24gRVfzbSNAFe8Cy_7mabLM6rcae3`R%A|?+k*^w_4)%DSj_XP1(u%YuXrw{qw^Z2sE zv+v|{P;pA3T#Os`=Fk9pMWV7ovAdlYxi+POJG!~;%{yCizCj-tpit|!souJRx$pkT ze!%$G{ey*zereAu0j0Sdu5HIB*@VZ1~TY3H5Y!(`$Jf_$r7;5c`!b&VAfiuz3J)acuw*=FSeRj>rIii4h(eu2xG&N z&yed3))iotxFLKO^is7FV&RLan5&)B>Lt$ZHrsxuj4NTq+5iKZN2-r;TFg zWdZq|h*1`W>eFZf%9RZ0QA}&0f`i!Zl<(p*D3wtr@o>Me+r_tY#^jlMXN#&l$vGs^ zg}hrgClt;1L!w#=`YI^aQiFCG3CA9O+t> zztrJ>-%i8js~l%}#8sc7g>!e=_uKHrDRJ_no#Zm~0PjztKT8Tei2^QID*L%sa*SA1 zO@VcI?cFho@rxi%)&vG5?DK&DS4g=yh2j)5HuPr7d!7rUn)XFRpKSZMAo=J-YeRu{ zxDnK~&ylQM-%PV06C0b6FmG~&G4;a{Ri|VNA+#jab#%eDeS7fZTw75&z12U+k?}Kh%cn+LckOeaH&9^YXd;s89e7PQF>~8 z_epdYh+9E>R!m7osg620WT$|uxIX|tX`Ykix}+;>v-tgIL~wLIjM-7r=qALhC}|x0 z7b4i?KBygGSzgJ<4vmUV zBrYL}yMp0@^V;FfO*L+%0VMR@y?if!#WPv5Ma6gY^=3w4%+i{E8kB}&@XEsI*n_ z&c&d#PyVIJx>X6?Fp0okNRVbg>GbM2OKQbfgjsNDEZgY=L8B|`S{^v&l+Rf5=b3t6 zVogfhYB5`b`)nKiJ1dnflEK{0xDbIHz^yK63P@-HjplsNlI(l_dhF-%5F)k7HF@5ea@M$I|YX$KE|0W=iD33QoCm29F3?>jfL!B8W#_AOuMH{ZHB58g8B zoJT2J<(x3B>${DmNk_{GXdYE#VA`QFVF4xZDwznv2nDGJu5Mtt6~3wY*O)2wZTD3O zw?%bSJ)kwK8Nc!8t!ow=;%61&jU)Pd6B5abB+0-uqcMp2EH|Mi}yaXd7M` zrY*93<)J*~QT^jFmJjo~h9E)gx#_9zv~5@XO$Ylb0pLBU{bcB*^Rr{a&gjqer46A- zgus|{c|I;Dy2w{BK0DThv5ZB0_N9(x4etRIy4hup8Xc&;zSW}PA|?T6{y6qD-5sr4 z62spUH;X9^(3ip()hyYp=f2!B?`a!soqQ%Ahgd-H<9loEHF#NTx0gC!Hg?#z%Vt`& zR6F20xYA2p;i$@*4$iY?kK8uZ%Ve;m3L!s}f#f{7)~T2AEcJv-SL`$&ODtQ+$7_Ef z9v3_)Pi+`=ch>22+dZx7P-s;|#k!l|Hj_!pGp~Thus{?vNL!x0O~7X_+Fz3WA$}N6 zw!?9+96ZeyHA6qPTLE6JV^a;xk!m?dya=aF(!(l`HMO~v6UR4k(x14n!g?;l_kns# zeMqbOMbD@Cuv z(aFua&W>YWGug0Ci~t4e2vmC!oF|mLBK=+-wns8lg1)kr=P_gBSK0x6w0AL9m|k_j zT51hvy z&zhWCFp4f}o6Z9$S$UG6F=&;rAvjXAU9>!72}%Xipm=p+zj%#w=D^VE~l-Y#$9PirL)kRK(j^t{`t6F~jM@P`3l1otZimf3TCwDQmuh|Bghd#D-fMfJVo<4zEkI5rxknUHFsE*is+WaM??D$ j;4yd=bZa00x(&tuu!?g~G&{93Ux9FE@Mcw~JZ}9PXlh;1 literal 0 HcmV?d00001 diff --git a/doc/DymoPrint_example_1.png b/doc/DymoPrint_example_1.png new file mode 100644 index 0000000000000000000000000000000000000000..69cc4a89c6eda3a429203d29c57d1241fd82d0de GIT binary patch literal 34893 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%0DhRBhis#Ag>RQ1(iYe9+ak9+%*d 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%0ANm~?=$_rs=fVz{7!;M?z*u@FN)hm*cY8#=8J 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 literal 0 HcmV?d00001 diff --git a/doc/DymoPrint_example_3.png b/doc/DymoPrint_example_3.png new file mode 100644 index 0000000000000000000000000000000000000000..b3951d574c184ca48e2d86403d6716518011d28e GIT binary patch literal 37194 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 ZGxxulI5A?<=1.2.1,<2 python-barcode>=0.13.1,<1 pyusb + PyQt6 + PyQt6-tools python_requires = >=3.7,<4 setup_requires = setuptools_scm @@ -49,3 +51,4 @@ universal = 1 [options.entry_points] console_scripts = dymoprint = dymoprint.command_line:main + dymoprint_gui = dymoprint.gui:main diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py index bd1bdef..1e6ab72 100644 --- a/src/dymoprint/dymo_print_engines.py +++ b/src/dymoprint/dymo_print_engines.py @@ -170,17 +170,16 @@ def render_picture(self, picture_path: str): with Image.open(picture_path) as img: if img.height > label_height: ratio = label_height / img.height - img.thumbnail( - (int(math.ceil(img.width * ratio)), label_height), Image.ANTIALIAS - ) + img = img.resize((int(math.ceil(img.width * ratio)), label_height)) + + img = img.convert("L", palette=Image.ANTIALIAS) return ImageOps.invert(img).convert("1") else: die(f"picture path:{picture_path} doesn't exist ") label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 return Image.new("1", (1, label_height)) - @staticmethod - def merge_render(bitmaps): + def merge_render(self,bitmaps): """ Merges multiple images into a single image. @@ -203,6 +202,8 @@ def merge_render(bitmaps): for bitmap in bitmaps: label_bitmap.paste(bitmap, box=(offset, 0)) offset += bitmap.width + padding + elif len(bitmaps) == 0: + return self.render_empty() else: label_bitmap = bitmaps[0] return label_bitmap diff --git a/src/dymoprint/font_config.py b/src/dymoprint/font_config.py index 61d7860..630ba55 100644 --- a/src/dymoprint/font_config.py +++ b/src/dymoprint/font_config.py @@ -1,4 +1,5 @@ import os +import re from configparser import ConfigParser from appdirs import user_config_dir @@ -26,8 +27,19 @@ def font_filename(flag): if conf.read(CONFIG_FILE): # reading FONTS section if not "FONTS" in conf.sections(): - die('! config file "%s" not valid. Please change or remove.' % CONFIG_FILE) + die('! config file "%s" not valid. Please change or remove.' % + CONFIG_FILE) for style in style_to_file.keys(): style_to_file[style] = conf.get("FONTS", style) return style_to_file[FLAG_TO_STYLE.get(flag, DEFAULT_FONT_STYLE)] + + +def parse_fonts() -> dict: + DEFAULT_FONT_DIR = os.path.dirname(dymoprint_fonts.__file__) + fonts = list() + for f in os.listdir(DEFAULT_FONT_DIR): + m = re.match(r'(.*-.*).ttf', f) + if m: + fonts.append((m.group(1), os.path.join(DEFAULT_FONT_DIR, f))) + return fonts diff --git a/src/dymoprint/gui.py b/src/dymoprint/gui.py new file mode 100644 index 0000000..2beade0 --- /dev/null +++ b/src/dymoprint/gui.py @@ -0,0 +1,147 @@ +import sys + +from PyQt6 import QtCore +from PyQt6.QtCore import QSize, Qt +from PyQt6.QtGui import QPixmap, QIcon, QColor, QPainter +from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QHBoxLayout +from PyQt6.QtWidgets import QGraphicsDropShadowEffect, QMessageBox, QSpinBox, QToolBar, QComboBox +from usb.core import USBError + +from PIL import ImageOps, Image, ImageQt + +from .dymo_print_engines import DymoPrinterServer, DymoRenderEngine +from .q_dymo_labels_list import QDymoLabelList + + +class DymoPrintWindow(QWidget): + def __init__(self): + super().__init__() + self.print_server = DymoPrinterServer() + self.render_engine = DymoRenderEngine(12) + self.label_bitmap = Image + + self.window_layout = QVBoxLayout() + self.list = QDymoLabelList(self.render_engine) + self.label_render = QLabel() + self.print_button = QPushButton() + self.margin = QSpinBox() + self.tape_size = QComboBox() + self.foreground_color = QComboBox() + self.background_color = QComboBox() + + self.init_elements() + self.init_connections() + self.init_layout() + + self.list.render_label() + + def init_elements(self): + self.setWindowTitle("DymoPrint GUI") + self.setGeometry(200, 200, 1000, 400) + printer_icon = QIcon.fromTheme('printer') + self.print_button.setIcon(printer_icon) + self.print_button.setFixedSize(64, 64) + self.print_button.setIconSize(QSize(48, 48)) + + shadow = QGraphicsDropShadowEffect() + shadow.setBlurRadius(15) + self.label_render.setGraphicsEffect(shadow) + + self.margin.setMinimum(20) + self.margin.setMaximum(1000) + self.margin.setValue(56) + self.tape_size.addItem('12', 12) + self.tape_size.addItem('9', 9) + self.tape_size.addItem('6', 6) + + self.foreground_color.addItems([ + 'black', + 'white', + 'yellow', + 'blue', + 'red', + "green" + ]) + self.background_color.addItems([ + 'white', + 'black', + 'yellow', + 'blue', + 'red', + "green" + ]) + + def init_connections(self): + self.margin.valueChanged.connect(self.list.render_label) + self.tape_size.currentTextChanged.connect(self.update_render_engine) + self.foreground_color.currentTextChanged.connect( + self.list.render_label) + self.background_color.currentTextChanged.connect( + self.list.render_label) + self.list.renderSignal.connect(self.update_label_render) + self.print_button.clicked.connect(self.print_label) + + def init_layout(self): + settings_widget = QToolBar(self) + settings_widget.addWidget(QLabel("Margin:")) + settings_widget.addWidget(self.margin) + settings_widget.addSeparator() + settings_widget.addWidget(QLabel("Tape Size:")) + settings_widget.addWidget(self.tape_size) + settings_widget.addSeparator() + settings_widget.addWidget(QLabel("Tape Colors: ")) + settings_widget.addWidget(self.foreground_color) + settings_widget.addWidget(QLabel(" on ")) + settings_widget.addWidget(self.background_color) + + render_widget = QWidget(self) + render_layout = QHBoxLayout(render_widget) + render_layout.addWidget(self.label_render) + render_layout.addWidget(self.print_button) + render_layout.setAlignment( + self.label_render, QtCore.Qt.AlignmentFlag.AlignCenter) + + self.window_layout.addWidget(settings_widget) + self.window_layout.addWidget(self.list) + self.window_layout.addWidget(render_widget) + self.setLayout(self.window_layout) + + def update_render_engine(self): + self.render_engine = DymoRenderEngine(self.tape_size.currentData()) + self.list.update_render_engine(self.render_engine) + + def update_label_render(self, label_bitmap): + self.label_bitmap = label_bitmap + label_image = Image.new("L", ( + self.margin.value() + self.label_bitmap.width + self.margin.value(), self.label_bitmap.height)) + label_image.paste(self.label_bitmap, (self.margin.value(), 0)) + label_image_inv = ImageOps.invert(label_image).copy() + qim = ImageQt.ImageQt(label_image_inv) + q_image = QPixmap.fromImage(qim) + + mask = q_image.createMaskFromColor( + QColor("255, 255, 255"), Qt.MaskMode.MaskOutColor) + q_image.fill(QColor(self.background_color.currentText())) + p = QPainter(q_image) + p.setPen(QColor(self.foreground_color.currentText())) + p.drawPixmap(q_image.rect(), mask, mask.rect()) + p.end() + + self.label_render.setPixmap(q_image) + self.label_render.adjustSize() + + def print_label(self): + try: + self.print_server.print_label( + self.label_bitmap, self.margin.value()) + except RuntimeError as err: + QMessageBox.warning(self, "Printing Failed!", f"{err}") + except USBError as err: + QMessageBox.warning(self, "Printing Failed!", f"{err}") + + +def main(): + app = QApplication(sys.argv) + window = DymoPrintWindow() + window.show() + sys.exit(app.exec()) diff --git a/src/dymoprint/q_dymo_label_widgets.py b/src/dymoprint/q_dymo_label_widgets.py index 3f05a6e..6462c50 100644 --- a/src/dymoprint/q_dymo_label_widgets.py +++ b/src/dymoprint/q_dymo_label_widgets.py @@ -1,9 +1,9 @@ import os import dymoprint_fonts -from PyQt5 import QtCore -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QSpinBox, QPlainTextEdit, QLineEdit, QPushButton, \ +from PyQt6 import QtCore +from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QSpinBox, QPlainTextEdit, QLineEdit, QPushButton, \ QFileDialog, QMessageBox from .font_config import parse_fonts @@ -59,11 +59,12 @@ def __init__(self, render_engine, parent=None): self.render_engine = render_engine self.label = QPlainTextEdit("text") - self.label.setFixedHeight(15 * (len(self.label.toPlainText().splitlines()) + 2)) + self.label.setFixedHeight( + 15 * (len(self.label.toPlainText().splitlines()) + 2)) self.setFixedHeight(self.label.height() + 10) self.font_style = QComboBox() self.font_size = QSpinBox() - self.font_size.setMaximum(100) + self.font_size.setMaximum(150) self.font_size.setMinimum(0) self.font_size.setSingleStep(1) self.font_size.setValue(90) @@ -76,7 +77,8 @@ def __init__(self, render_engine, parent=None): layout = QHBoxLayout() ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) item_icon = QLabel() - item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "txt_icon.png")).pixmap(32, 32)) + item_icon.setPixmap( + QIcon(os.path.join(ICON_DIR, "txt_icon.png")).pixmap(32, 32)) item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) layout.addWidget(item_icon) layout.addWidget(self.label) @@ -96,7 +98,8 @@ def content_changed(self): """ Updates the height of the label and emits the itemRenderSignal when the content of the label changes. """ - self.label.setFixedHeight(15 * (len(self.label.toPlainText().splitlines()) + 2)) + self.label.setFixedHeight( + 15 * (len(self.label.toPlainText().splitlines()) + 2)) self.setFixedHeight(self.label.height() + 10) self.itemRenderSignal.emit() @@ -115,7 +118,8 @@ def render_label(self): font_size_ratio=self.font_size.value() / 100.0) return render except BaseException as err: - QMessageBox.warning(self, "TextDymoLabelWidget render fail!", f"{err}") + QMessageBox.warning( + self, "TextDymoLabelWidget render fail!", f"{err}") return self.render_engine.render_empty() @@ -141,7 +145,8 @@ def __init__(self, render_engine, parent=None): layout = QHBoxLayout() ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) item_icon = QLabel() - item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "qr_icon.png")).pixmap(32, 32)) + item_icon.setPixmap( + QIcon(os.path.join(ICON_DIR, "qr_icon.png")).pixmap(32, 32)) item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) layout.addWidget(item_icon) layout.addWidget(self.label) @@ -161,7 +166,8 @@ def render_label(self): return render except BaseException as err: - QMessageBox.warning(self, "QrDymoLabelWidget render fail!", f"{err}") + QMessageBox.warning( + self, "QrDymoLabelWidget render fail!", f"{err}") return self.render_engine.render_empty() @@ -190,7 +196,8 @@ def __init__(self, render_engine, parent=None): layout = QHBoxLayout() ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) item_icon = QLabel() - item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "barcode_icon.png")).pixmap(32, 32)) + item_icon.setPixmap( + QIcon(os.path.join(ICON_DIR, "barcode_icon.png")).pixmap(32, 32)) item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) self.codding = QComboBox() self.codding.addItems([ @@ -227,11 +234,13 @@ def render_label(self): QPixmap: A QPixmap object representing the rendered barcode label. """ try: - render = self.render_engine.render_barcode(self.label.text(), self.codding.currentText()) + render = self.render_engine.render_barcode( + self.label.text(), self.codding.currentText()) return render except BaseException as err: - QMessageBox.warning(self, "BarcodeDymoLabelWidget render fail!", f"{err}") + QMessageBox.warning( + self, "BarcodeDymoLabelWidget render fail!", f"{err}") return self.render_engine.render_empty() @@ -257,12 +266,14 @@ def __init__(self, render_engine, parent=None): layout = QHBoxLayout() ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) item_icon = QLabel() - item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "img_icon.png")).pixmap(32, 32)) + item_icon.setPixmap( + QIcon(os.path.join(ICON_DIR, "img_icon.png")).pixmap(32, 32)) item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) button = QPushButton('Select file') file_dialog = QFileDialog() - button.clicked.connect(lambda: self.label.setText(file_dialog.getOpenFileName()[0])) + button.clicked.connect(lambda: self.label.setText( + os.path.abspath(file_dialog.getOpenFileName()[0]))) layout.addWidget(item_icon) layout.addWidget(self.label) @@ -281,5 +292,6 @@ def render_label(self): render = self.render_engine.render_picture(self.label.text()) return render except BaseException as err: - QMessageBox.warning(self, "ImageDymoLabelWidget render fail!", f"{err}") + QMessageBox.warning( + self, "ImageDymoLabelWidget render fail!", f"{err}") return self.render_engine.render_empty() diff --git a/src/dymoprint/q_dymo_labels_list.py b/src/dymoprint/q_dymo_labels_list.py index 4c82ee0..d6580a0 100644 --- a/src/dymoprint/q_dymo_labels_list.py +++ b/src/dymoprint/q_dymo_labels_list.py @@ -1,10 +1,11 @@ -import PIL -from PIL.Image import Image -from PyQt5 import QtCore -from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QAbstractItemView, QMenu + +from PyQt6 import QtCore +from PyQt6.QtWidgets import QListWidget, QListWidgetItem, QAbstractItemView, QMenu from .q_dymo_label_widgets import TextDymoLabelWidget, QrDymoLabelWidget, BarcodeDymoLabelWidget, \ ImageDymoLabelWidget +from PIL import Image + class QDymoLabelList(QListWidget): """ @@ -23,14 +24,14 @@ class QDymoLabelList(QListWidget): contextMenuEvent(self, event): Overrides the default context menu event to add or delete label widgets. """ - renderSignal = QtCore.pyqtSignal(PIL.Image.Image, name='renderSignal') + renderSignal = QtCore.pyqtSignal(Image.Image, name='renderSignal') def __init__(self, render_engine, parent=None): super(QDymoLabelList, self).__init__(parent) self.render_engine = render_engine self.setAlternatingRowColors(True) - self.setDragDropMode(QAbstractItemView.InternalMove) - for item_widget in [TextDymoLabelWidget(self.render_engine) ]: + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + for item_widget in [TextDymoLabelWidget(self.render_engine)]: item = QListWidgetItem(self) item.setSizeHint(item_widget.sizeHint()) self.addItem(item) diff --git a/src/dymoprint/utils.py b/src/dymoprint/utils.py index 81c5cf7..084420f 100755 --- a/src/dymoprint/utils.py +++ b/src/dymoprint/utils.py @@ -22,12 +22,14 @@ def die(message=None): if message: print(message, file=sys.stderr) + raise RuntimeError(message) sys.exit(1) def pprint(par, fd=sys.stdout): rows, columns = struct.unpack( - "HH", fcntl.ioctl(sys.stderr, termios.TIOCGWINSZ, struct.pack("HH", 0, 0)) + "HH", fcntl.ioctl(sys.stderr, termios.TIOCGWINSZ, + struct.pack("HH", 0, 0)) ) print(textwrap.fill(par, columns), file=fd) @@ -75,20 +77,17 @@ def getDeviceFile(classID, vendorID, productID): def access_error(dev): - pprint("You do not have sufficient access to the device file %s:" % dev, sys.stderr) + die("You do not have sufficient access to the device file %s:" % dev, sys.stderr) subprocess.call(["ls", "-l", dev], stdout=sys.stderr) print(file=sys.stderr) filename = "91-dymo-labelmanager-pnp.rules" - pprint( - "You probably want to add a rule like one of the following in /etc/udev/rules.d/" - + filename, - sys.stderr, + die( + f"You probably want to add a rule like one of the following in /etc/udev/rules.d/{filename}" ) with open(filename, "r") as fin: print(fin.read(), file=sys.stderr) - pprint( + die( "Following that, restart udev and re-plug your device. See README.md for details", - sys.stderr, ) From cf78767add6845e9456c2dd1b91550c95438eef4 Mon Sep 17 00:00:00 2001 From: MooVx Date: Wed, 3 May 2023 20:47:32 +0200 Subject: [PATCH 08/17] right margin print fix --- src/dymoprint/dymo_print_engines.py | 2 +- src/dymoprint/gui.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py index 1e6ab72..4aea92f 100644 --- a/src/dymoprint/dymo_print_engines.py +++ b/src/dymoprint/dymo_print_engines.py @@ -211,7 +211,7 @@ def merge_render(self,bitmaps): class DymoPrinterServer: @staticmethod - def print_label(label_bitmap, margin=56, tape_size: int = 12): + def print_label(label_bitmap, margin=56*2, tape_size: int = 12): """ Prints a label using a Dymo labeler object. diff --git a/src/dymoprint/gui.py b/src/dymoprint/gui.py index 2beade0..6132637 100644 --- a/src/dymoprint/gui.py +++ b/src/dymoprint/gui.py @@ -133,7 +133,7 @@ def update_label_render(self, label_bitmap): def print_label(self): try: self.print_server.print_label( - self.label_bitmap, self.margin.value()) + self.label_bitmap, self.margin.value()*2) except RuntimeError as err: QMessageBox.warning(self, "Printing Failed!", f"{err}") except USBError as err: From aa2fc64fab0fbf8be1a099a569a8e7c2f525a7b0 Mon Sep 17 00:00:00 2001 From: MooVx Date: Wed, 3 May 2023 22:05:58 +0200 Subject: [PATCH 09/17] python3.7 compatibility fix --- src/dymoprint/dymo_print_engines.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py index 4aea92f..6574849 100644 --- a/src/dymoprint/dymo_print_engines.py +++ b/src/dymoprint/dymo_print_engines.py @@ -1,3 +1,4 @@ +from __future__ import annotations import array import math import os @@ -172,7 +173,7 @@ def render_picture(self, picture_path: str): ratio = label_height / img.height img = img.resize((int(math.ceil(img.width * ratio)), label_height)) - img = img.convert("L", palette=Image.ANTIALIAS) + img = img.convert("L", palette=Image.AFFINE) return ImageOps.invert(img).convert("1") else: die(f"picture path:{picture_path} doesn't exist ") From 148ed060b0c4c6f1fb602200d2014d90197a3214 Mon Sep 17 00:00:00 2001 From: MooVx Date: Wed, 3 May 2023 22:10:16 +0200 Subject: [PATCH 10/17] GUI application Icon added --- data/fonts/gui_icon.png | Bin 0 -> 2733 bytes src/dymoprint/gui.py | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 data/fonts/gui_icon.png diff --git a/data/fonts/gui_icon.png b/data/fonts/gui_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ef08a208a87490bafe95c7dbc1382ce5b45885e2 GIT binary patch literal 2733 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 literal 0 HcmV?d00001 diff --git a/src/dymoprint/gui.py b/src/dymoprint/gui.py index 6132637..8c86e2b 100644 --- a/src/dymoprint/gui.py +++ b/src/dymoprint/gui.py @@ -1,4 +1,5 @@ import sys +import os from PyQt6 import QtCore from PyQt6.QtCore import QSize, Qt @@ -11,6 +12,7 @@ from .dymo_print_engines import DymoPrinterServer, DymoRenderEngine from .q_dymo_labels_list import QDymoLabelList +import dymoprint_fonts class DymoPrintWindow(QWidget): @@ -37,6 +39,8 @@ def __init__(self): def init_elements(self): self.setWindowTitle("DymoPrint GUI") + ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) + self.setWindowIcon(QIcon(os.path.join(ICON_DIR, "gui_icon.png"))) self.setGeometry(200, 200, 1000, 400) printer_icon = QIcon.fromTheme('printer') self.print_button.setIcon(printer_icon) From 2e34dced5545e412d1ebdeea26e3fda29dfd6ad5 Mon Sep 17 00:00:00 2001 From: MooVx Date: Wed, 3 May 2023 22:22:34 +0200 Subject: [PATCH 11/17] arg parser percent sign problem fix --- src/dymoprint/command_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dymoprint/command_line.py b/src/dymoprint/command_line.py index abe3958..0f4f683 100755 --- a/src/dymoprint/command_line.py +++ b/src/dymoprint/command_line.py @@ -90,7 +90,7 @@ def parse_args(): ) parser.add_argument("-p", "--picture", help="Print the specified picture") parser.add_argument("-m", type=int, help="Override margin (default is 56*2)") - parser.add_argument("--scale", type=int, default=90, help="Scaling font factor, [0,10] %") + parser.add_argument("--scale", type=int, default=90,help="Scaling font factor, [0,10] [%%]") parser.add_argument('-t', type=int, choices=[6, 9, 12], default=12, help='Tape size: 6,9,12 mm, default=12mm') return parser.parse_args() From 659d74e99c4499f69f9ddd7b8dc680f4ea2beabc Mon Sep 17 00:00:00 2001 From: MooVx Date: Wed, 3 May 2023 23:58:00 +0200 Subject: [PATCH 12/17] GUI add min_payload_len & Justify merged features --- src/dymoprint/command_line.py | 5 +++-- src/dymoprint/dymo_print_engines.py | 6 ------ src/dymoprint/gui.py | 26 +++++++++++++++++++++++--- src/dymoprint/q_dymo_labels_list.py | 12 +++++++++--- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/dymoprint/command_line.py b/src/dymoprint/command_line.py index 511d008..f01ab5f 100755 --- a/src/dymoprint/command_line.py +++ b/src/dymoprint/command_line.py @@ -59,6 +59,7 @@ def parse_args(): parser.add_argument( "-l", type=int, + default=0, help="Specify minimum label length in mm" ) parser.add_argument( @@ -163,9 +164,9 @@ def main(): bitmaps.append(render_engine.render_picture(args.picture)) margin = args.m - min_payload_len = max(0, (args.l * 7) - margin * 2) justify = args.j - + min_label_mm_len: int = args.l + min_payload_len = max(0, (min_label_mm_len * 7) - margin * 2) label_bitmap = render_engine.merge_render( bitmaps, min_payload_len, justify) diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py index b518d70..ed4809f 100644 --- a/src/dymoprint/dymo_print_engines.py +++ b/src/dymoprint/dymo_print_engines.py @@ -219,17 +219,11 @@ def merge_render(self, bitmaps, min_payload_len=0, justify='center'): if min_payload_len > label_bitmap.width: offset = 0 - print(f'label_bitmap.width {label_bitmap.width}') - print(f'min_payload_len {min_payload_len}') - print(f'L {offset}') if (justify == "center"): offset = max( 0, int((min_payload_len - label_bitmap.width) / 2)) - print(f'C {offset}') if (justify == "right"): offset = max(0, int(min_payload_len - label_bitmap.width)) - print(f'R {offset}') - out_label_bitmap = Image.new( "1", ( diff --git a/src/dymoprint/gui.py b/src/dymoprint/gui.py index 8c86e2b..562b08d 100644 --- a/src/dymoprint/gui.py +++ b/src/dymoprint/gui.py @@ -30,6 +30,8 @@ def __init__(self): self.tape_size = QComboBox() self.foreground_color = QComboBox() self.background_color = QComboBox() + self.min_label_len = QSpinBox() + self.justify = QComboBox() self.init_elements() self.init_connections() @@ -57,6 +59,13 @@ def init_elements(self): self.tape_size.addItem('12', 12) self.tape_size.addItem('9', 9) self.tape_size.addItem('6', 6) + self.min_label_len.setMinimum(0) + self.min_label_len.setMaximum(1000) + self.justify.addItems([ + 'center', + 'left', + 'right' + ]) self.foreground_color.addItems([ 'black', @@ -77,7 +86,9 @@ def init_elements(self): def init_connections(self): self.margin.valueChanged.connect(self.list.render_label) - self.tape_size.currentTextChanged.connect(self.update_render_engine) + self.tape_size.currentTextChanged.connect(self.update_params) + self.min_label_len.valueChanged.connect(self.update_params) + self.justify.currentTextChanged.connect(self.update_params) self.foreground_color.currentTextChanged.connect( self.list.render_label) self.background_color.currentTextChanged.connect( @@ -93,6 +104,12 @@ def init_layout(self): settings_widget.addWidget(QLabel("Tape Size:")) settings_widget.addWidget(self.tape_size) settings_widget.addSeparator() + settings_widget.addWidget(QLabel("Min Label Len [mm]:")) + settings_widget.addWidget(self.min_label_len) + settings_widget.addSeparator() + settings_widget.addWidget(QLabel("Justify:")) + settings_widget.addWidget(self.justify) + settings_widget.addSeparator() settings_widget.addWidget(QLabel("Tape Colors: ")) settings_widget.addWidget(self.foreground_color) settings_widget.addWidget(QLabel(" on ")) @@ -110,9 +127,12 @@ def init_layout(self): self.window_layout.addWidget(render_widget) self.setLayout(self.window_layout) - def update_render_engine(self): + def update_params(self): self.render_engine = DymoRenderEngine(self.tape_size.currentData()) - self.list.update_render_engine(self.render_engine) + justify = self.justify.currentText() + min_label_mm_len: int = self.min_label_len.value() + min_payload_len = max(0, (min_label_mm_len * 7) - self.margin.value() * 2) + self.list.update_params(self.render_engine, min_payload_len, justify) def update_label_render(self, label_bitmap): self.label_bitmap = label_bitmap diff --git a/src/dymoprint/q_dymo_labels_list.py b/src/dymoprint/q_dymo_labels_list.py index d6580a0..b49f904 100644 --- a/src/dymoprint/q_dymo_labels_list.py +++ b/src/dymoprint/q_dymo_labels_list.py @@ -26,8 +26,10 @@ class QDymoLabelList(QListWidget): renderSignal = QtCore.pyqtSignal(Image.Image, name='renderSignal') - def __init__(self, render_engine, parent=None): + def __init__(self, render_engine, min_payload_len=0, justify='center', parent=None): super(QDymoLabelList, self).__init__(parent) + self.min_payload_len = min_payload_len + self.justify = justify self.render_engine = render_engine self.setAlternatingRowColors(True) self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) @@ -48,12 +50,16 @@ def dropEvent(self, e) -> None: super().dropEvent(e) self.render_label() - def update_render_engine(self, render_engine): + def update_params(self, render_engine, min_payload_len=0, justify='center'): """ Updates the render engine used for rendering the label. Args: + justify: justification [center,left,right] + min_payload_len: minimum payload size render_engine (RenderEngine): The new render engine to use. """ + self.min_payload_len = min_payload_len + self.justify = justify self.render_engine = render_engine for i in range(self.count()): item_widget = self.itemWidget(self.item(i)) @@ -71,7 +77,7 @@ def render_label(self): if item_widget and item: item.setSizeHint(item_widget.sizeHint()) bitmaps.append(item_widget.render_label()) - label_bitmap = self.render_engine.merge_render(bitmaps) + label_bitmap = self.render_engine.merge_render(bitmaps, self.min_payload_len, self.justify) self.renderSignal.emit(label_bitmap) From 417b261d7e2907e8e90f21d5ab7c14ab3b544d13 Mon Sep 17 00:00:00 2001 From: MooVx Date: Thu, 4 May 2023 00:13:30 +0200 Subject: [PATCH 13/17] added merged "align" feature to GUI --- src/dymoprint/gui.py | 2 +- src/dymoprint/q_dymo_label_widgets.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/dymoprint/gui.py b/src/dymoprint/gui.py index 562b08d..2e5bac7 100644 --- a/src/dymoprint/gui.py +++ b/src/dymoprint/gui.py @@ -43,7 +43,7 @@ def init_elements(self): self.setWindowTitle("DymoPrint GUI") ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) self.setWindowIcon(QIcon(os.path.join(ICON_DIR, "gui_icon.png"))) - self.setGeometry(200, 200, 1000, 400) + self.setGeometry(200, 200, 1100, 400) printer_icon = QIcon.fromTheme('printer') self.print_button.setIcon(printer_icon) self.print_button.setFixedSize(64, 64) diff --git a/src/dymoprint/q_dymo_label_widgets.py b/src/dymoprint/q_dymo_label_widgets.py index 6462c50..b311e2b 100644 --- a/src/dymoprint/q_dymo_label_widgets.py +++ b/src/dymoprint/q_dymo_label_widgets.py @@ -3,8 +3,8 @@ import dymoprint_fonts from PyQt6 import QtCore from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QSpinBox, QPlainTextEdit, QLineEdit, QPushButton, \ - QFileDialog, QMessageBox +from PyQt6.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QSpinBox, QPlainTextEdit, QLineEdit +from PyQt6.QtWidgets import QPushButton, QFileDialog, QMessageBox, QVBoxLayout from .font_config import parse_fonts @@ -69,6 +69,10 @@ def __init__(self, render_engine, parent=None): self.font_size.setSingleStep(1) self.font_size.setValue(90) self.draw_frame = QSpinBox() + self.align = QComboBox() + + self.align.addItems(['left', 'center', 'right']) + for (name, font_path) in parse_fonts(): self.font_style.addItem(name, font_path) if "Regular" in name: @@ -88,10 +92,13 @@ def __init__(self, render_engine, parent=None): layout.addWidget(self.font_size) layout.addWidget(QLabel("Frame Width:")) layout.addWidget(self.draw_frame) + layout.addWidget(QLabel("Alignment:")) + layout.addWidget(self.align) self.label.textChanged.connect(self.content_changed) self.draw_frame.valueChanged.connect(self.content_changed) self.font_size.valueChanged.connect(self.content_changed) self.font_style.currentTextChanged.connect(self.content_changed) + self.align.currentTextChanged.connect(self.content_changed) self.setLayout(layout) def content_changed(self): @@ -115,7 +122,8 @@ def render_label(self): render = self.render_engine.render_text(labeltext=self.label.toPlainText().splitlines(), font_file_name=self.font_style.currentData(), frame_width=self.draw_frame.value(), - font_size_ratio=self.font_size.value() / 100.0) + font_size_ratio=self.font_size.value() / 100.0, + align=self.align.currentText()) return render except BaseException as err: QMessageBox.warning( From aa71a9477e2f74bd2c4fa87ddf4305d45a3a436b Mon Sep 17 00:00:00 2001 From: MooVx Date: Fri, 5 May 2023 08:38:08 +0200 Subject: [PATCH 14/17] re sending usb dispose_resources fix --- src/dymoprint/dymo_print_engines.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py index ed4809f..a196a04 100644 --- a/src/dymoprint/dymo_print_engines.py +++ b/src/dymoprint/dymo_print_engines.py @@ -336,3 +336,5 @@ def print_label(label_bitmap, margin=56 * 2, tape_size: int = 12): lm.printLabel(label_matrix, margin=margin) else: lm.printLabel(label_matrix) + + usb.util.dispose_resources(dev) From f0147a2f090b1681ac23967bddad0ed67c665133 Mon Sep 17 00:00:00 2001 From: MooVx Date: Fri, 5 May 2023 08:38:37 +0200 Subject: [PATCH 15/17] contextMenu miss type fix --- src/dymoprint/q_dymo_labels_list.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dymoprint/q_dymo_labels_list.py b/src/dymoprint/q_dymo_labels_list.py index b49f904..ca40d5a 100644 --- a/src/dymoprint/q_dymo_labels_list.py +++ b/src/dymoprint/q_dymo_labels_list.py @@ -88,10 +88,10 @@ def contextMenuEvent(self, event): event (QContextMenuEvent): The context menu event. """ contextMenu = QMenu(self) - add_text = contextMenu.addAction("AddText") - add_qr = contextMenu.addAction("AddQR") - add_barcode = contextMenu.addAction("AddBarcode") - add_img = contextMenu.addAction("AddImage") + add_text = contextMenu.addAction("Add Text") + add_qr = contextMenu.addAction("Add QR") + add_barcode = contextMenu.addAction("Add Barcode") + add_img = contextMenu.addAction("Add Image") delete = contextMenu.addAction("Delete") menu_click = contextMenu.exec(event.globalPos()) From 367de8fbaf8e335a6798182039539a3cbd8e9a7e Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sat, 27 May 2023 17:23:02 +0200 Subject: [PATCH 16/17] Fix indentation The code to print the label was indented to where it only ran when using PyUSB, and not with the original HID file mode. --- src/dymoprint/dymo_print_engines.py | 31 ++++++++++++++++------------- src/dymoprint/utils.py | 3 ++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py index a196a04..00e3930 100644 --- a/src/dymoprint/dymo_print_engines.py +++ b/src/dymoprint/dymo_print_engines.py @@ -275,6 +275,7 @@ def print_label(label_bitmap, margin=56 * 2, tape_size: int = 12): devin = devout # We are in the normal HID file mode, so no syn_wait is needed. syn_wait = None + in_usb_mode = False else: # We are in the experimental PyUSB mode, if a device can be found. syn_wait = 64 @@ -289,6 +290,7 @@ def print_label(label_bitmap, margin=56 * 2, tape_size: int = 12): die("The device '%s' could not be found on this system." % DEV_NAME) else: print("Entering experimental PyUSB mode.") + in_usb_mode = True try: dev.set_configuration() @@ -321,20 +323,21 @@ def print_label(label_bitmap, margin=56 * 2, tape_size: int = 12): ), ) - if not devout or not devin: - die("The device '%s' could not be found on this system." % DEV_NAME) + if not devout or not devin: + die("The device '%s' could not be found on this system." % DEV_NAME) - # create dymo labeler object - try: - lm = DymoLabeler( - devout, devin, synwait=syn_wait, tape_size=tape_size) - except IOError: - die(access_error(dev)) - - print("Printing label..") - if margin is not None: - lm.printLabel(label_matrix, margin=margin) - else: - lm.printLabel(label_matrix) + # create dymo labeler object + try: + lm = DymoLabeler( + devout, devin, synwait=syn_wait, tape_size=tape_size) + except IOError: + die(access_error(dev)) + + print("Printing label..") + if margin is not None: + lm.printLabel(label_matrix, margin=margin) + else: + lm.printLabel(label_matrix) + if in_usb_mode: usb.util.dispose_resources(dev) diff --git a/src/dymoprint/utils.py b/src/dymoprint/utils.py index 084420f..d1db647 100755 --- a/src/dymoprint/utils.py +++ b/src/dymoprint/utils.py @@ -15,11 +15,12 @@ import sys import termios import textwrap +from typing import NoReturn from PIL import ImageDraw -def die(message=None): +def die(message=None) -> NoReturn: if message: print(message, file=sys.stderr) raise RuntimeError(message) From 5bb3dc361febd2467996e51fcfa801e4d9870099 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sat, 27 May 2023 17:27:30 +0200 Subject: [PATCH 17/17] Apply pre-commit --- README.md | 6 +- src/dymoprint/command_line.py | 43 +++++----- src/dymoprint/dymo_print_engines.py | 107 +++++++++++++++--------- src/dymoprint/font_config.py | 5 +- src/dymoprint/gui.py | 87 ++++++++++---------- src/dymoprint/labeler.py | 5 +- src/dymoprint/q_dymo_label_widgets.py | 113 +++++++++++++++----------- src/dymoprint/q_dymo_labels_list.py | 23 ++++-- src/dymoprint/utils.py | 3 +- 9 files changed, 221 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 04a107f..84886ce 100644 --- a/README.md +++ b/README.md @@ -178,13 +178,13 @@ Take care of the trailing "" - you may enter text here which gets printed in fro * font scaling - the percentage of line-height * frame border width steering * Qr Node: - * payload text + * payload text * BarCode Node: * payload text * codding selector * Image Node: * path to file - + Nodes can be freely arranged, simply drag&drop rows on the list. To add or delete the node from the label - right-click on the list and select the action from the context menu. To print - click the print button. @@ -195,7 +195,7 @@ Example 1: multiple text + QR code ![alt](doc/DymoPrint_example_1.png) -Example 2: two images + text with frame, white on red +Example 2: two images + text with frame, white on red ![alt](doc/DymoPrint_example_2.png) diff --git a/src/dymoprint/command_line.py b/src/dymoprint/command_line.py index f01ab5f..8ca8d4e 100755 --- a/src/dymoprint/command_line.py +++ b/src/dymoprint/command_line.py @@ -12,10 +12,7 @@ from PIL import Image, ImageOps from . import __version__ -from .constants import ( - USE_QR, - e_qrcode, -) +from .constants import USE_QR, e_qrcode from .dymo_print_engines import DymoPrinterServer, DymoRenderEngine from .font_config import font_filename from .metadata import our_metadata @@ -57,10 +54,7 @@ def parse_args(): help="Align multiline text (left,center,right)", ) parser.add_argument( - "-l", - type=int, - default=0, - help="Specify minimum label length in mm" + "-l", type=int, default=0, help="Specify minimum label length in mm" ) parser.add_argument( "-j", @@ -72,8 +66,7 @@ def parse_args(): default="center", help="Justify content of label if minimum label length is specified (left,center,right)", ) - parser.add_argument( - "-u", nargs="?", help='Set user font, overrides "-s" parameter') + parser.add_argument("-u", nargs="?", help='Set user font, overrides "-s" parameter') parser.add_argument( "-n", "--preview", @@ -116,12 +109,19 @@ def parse_args(): help="Printing the first text parameter as barcode", ) parser.add_argument("-p", "--picture", help="Print the specified picture") - parser.add_argument("-m", type=int, default=56, - help="Override margin (default is 56*2)") - parser.add_argument("--scale", type=int, default=90, - help="Scaling font factor, [0,10] [%%]") parser.add_argument( - '-t', type=int, choices=[6, 9, 12], default=12, help='Tape size: 6,9,12 mm, default=12mm') + "-m", type=int, default=56, help="Override margin (default is 56*2)" + ) + parser.add_argument( + "--scale", type=int, default=90, help="Scaling font factor, [0,10] [%%]" + ) + parser.add_argument( + "-t", + type=int, + choices=[6, 9, 12], + default=12, + help="Tape size: 6,9,12 mm, default=12mm", + ) return parser.parse_args() @@ -157,8 +157,11 @@ def main(): bitmaps.append(render_engine.render_barcode(labeltext.pop(0), args.c)) if labeltext: - bitmaps.append(render_engine.render_text( - labeltext, FONT_FILENAME, args.f, int(args.scale) / 100.0, args.a)) + bitmaps.append( + render_engine.render_text( + labeltext, FONT_FILENAME, args.f, int(args.scale) / 100.0, args.a + ) + ) if args.picture: bitmaps.append(render_engine.render_picture(args.picture)) @@ -167,15 +170,15 @@ def main(): justify = args.j min_label_mm_len: int = args.l min_payload_len = max(0, (min_label_mm_len * 7) - margin * 2) - label_bitmap = render_engine.merge_render( - bitmaps, min_payload_len, justify) + label_bitmap = render_engine.merge_render(bitmaps, min_payload_len, justify) # print or show the label if args.preview or args.preview_inverted or args.imagemagick: print("Demo mode: showing label..") # fix size, adding print borders label_image = Image.new( - "L", (margin + label_bitmap.width + margin, label_bitmap.height)) + "L", (margin + label_bitmap.width + margin, label_bitmap.height) + ) label_image.paste(label_bitmap, (margin, 0)) if args.preview or args.preview_inverted: label_rotated = label_bitmap.transpose(Image.ROTATE_270) diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py index 00e3930..bbc5961 100644 --- a/src/dymoprint/dymo_print_engines.py +++ b/src/dymoprint/dymo_print_engines.py @@ -6,16 +6,21 @@ import barcode as barcode_module import usb -from PIL import Image, ImageOps -from PIL import ImageFont +from PIL import Image, ImageFont, ImageOps from . import DymoLabeler from .barcode_writer import BarcodeImageWriter -from .constants import (DEV_CLASS, DEV_LM280_CLASS, DEV_LM280_PRODUCT, - DEV_NAME, DEV_NODE, DEV_PRODUCT, DEV_VENDOR, ) -from .constants import (QRCode, ) -from .utils import access_error, die, getDeviceFile -from .utils import draw_image, scaling +from .constants import ( + DEV_CLASS, + DEV_LM280_CLASS, + DEV_LM280_PRODUCT, + DEV_NAME, + DEV_NODE, + DEV_PRODUCT, + DEV_VENDOR, + QRCode, +) +from .utils import access_error, die, draw_image, getDeviceFile, scaling class DymoRenderEngine: @@ -59,10 +64,12 @@ def render_qr(self, qr_input_text): # create an empty label image qr_scale = label_height // len(qr_text) qr_offset = (label_height - len(qr_text) * qr_scale) // 2 - label_width = (len(qr_text) * qr_scale) + label_width = len(qr_text) * qr_scale if not qr_scale: - die("Error: too much information to store in the QR code, points are smaller than the device resolution") + die( + "Error: too much information to store in the QR code, points are smaller than the device resolution" + ) code_bitmap = Image.new("1", (label_width, label_height)) @@ -72,7 +79,8 @@ def render_qr(self, qr_input_text): for j in range(len(line)): if line[j] == "1": pix = scaling( - (j * qr_scale, i * qr_scale + qr_offset), qr_scale) + (j * qr_scale, i * qr_scale + qr_offset), qr_scale + ) label_draw.point(pix, 255) return code_bitmap @@ -92,12 +100,14 @@ def render_barcode(self, barcode_input_text, bar_code_type): return Image.new("1", (1, label_height)) code = barcode_module.get( - bar_code_type, barcode_input_text, writer=BarcodeImageWriter()) + bar_code_type, barcode_input_text, writer=BarcodeImageWriter() + ) code_bitmap = code.render( { "font_size": 0, "vertical_margin": 8, - "module_height": (DymoLabeler.max_bytes_per_line(self.tape_size) * 8) - 16, + "module_height": (DymoLabeler.max_bytes_per_line(self.tape_size) * 8) + - 16, "module_width": 2, "background": "black", "foreground": "white", @@ -105,7 +115,14 @@ def render_barcode(self, barcode_input_text, bar_code_type): ) return code_bitmap - def render_text(self, labeltext: list[str], font_file_name: str, frame_width, font_size_ratio=0.9, align="left"): + def render_text( + self, + labeltext: list[str], + font_file_name: str, + frame_width, + font_size_ratio=0.9, + align="left", + ): """ Renders a text image from the input text, font file name, frame width, and font size ratio. @@ -122,7 +139,7 @@ def render_text(self, labeltext: list[str], font_file_name: str, frame_width, fo labeltext = [labeltext] if len(labeltext) == 0: - labeltext = [' '] + labeltext = [" "] # create an empty label image label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 @@ -136,8 +153,9 @@ def render_text(self, labeltext: list[str], font_file_name: str, frame_width, fo frame_width = min(frame_width, 3) font = ImageFont.truetype(font_file_name, fontsize) - label_width = max(font.getsize(line)[0] - for line in labeltext) + (font_offset * 2) + label_width = max(font.getsize(line)[0] for line in labeltext) + ( + font_offset * 2 + ) text_bitmap = Image.new("1", (label_width, label_height)) with draw_image(text_bitmap) as label_draw: @@ -149,16 +167,24 @@ def render_text(self, labeltext: list[str], font_file_name: str, frame_width, fo label_draw.rectangle( ( (frame_width, 4 + frame_width), - (label_width - (frame_width + 1), - label_height - (frame_width + 4)), + ( + label_width - (frame_width + 1), + label_height - (frame_width + 4), + ), ), fill=0, ) # write the text into the empty image - multiline_text = '\n'.join(labeltext) - label_draw.multiline_text((label_width / 2, label_height / 2), - multiline_text, align=align, anchor="mm", font=font, fill=255) + multiline_text = "\n".join(labeltext) + label_draw.multiline_text( + (label_width / 2, label_height / 2), + multiline_text, + align=align, + anchor="mm", + font=font, + fill=255, + ) return text_bitmap def render_picture(self, picture_path: str): @@ -173,13 +199,13 @@ def render_picture(self, picture_path: str): """ if len(picture_path): if os.path.exists(picture_path): - label_height = DymoLabeler.max_bytes_per_line( - self.tape_size) * 8 + label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 with Image.open(picture_path) as img: if img.height > label_height: ratio = label_height / img.height img = img.resize( - (int(math.ceil(img.width * ratio)), label_height)) + (int(math.ceil(img.width * ratio)), label_height) + ) img = img.convert("L", palette=Image.AFFINE) return ImageOps.invert(img).convert("1") @@ -188,7 +214,7 @@ def render_picture(self, picture_path: str): label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8 return Image.new("1", (1, label_height)) - def merge_render(self, bitmaps, min_payload_len=0, justify='center'): + def merge_render(self, bitmaps, min_payload_len=0, justify="center"): """ Merges multiple images into a single image. @@ -203,8 +229,7 @@ def merge_render(self, bitmaps, min_payload_len=0, justify='center'): label_bitmap = Image.new( "1", ( - sum(b.width for b in bitmaps) + - padding * (len(bitmaps) - 1), + sum(b.width for b in bitmaps) + padding * (len(bitmaps) - 1), bitmaps[0].height, ), ) @@ -219,10 +244,9 @@ def merge_render(self, bitmaps, min_payload_len=0, justify='center'): if min_payload_len > label_bitmap.width: offset = 0 - if (justify == "center"): - offset = max( - 0, int((min_payload_len - label_bitmap.width) / 2)) - if (justify == "right"): + if justify == "center": + offset = max(0, int((min_payload_len - label_bitmap.width) / 2)) + if justify == "right": offset = max(0, int(min_payload_len - label_bitmap.width)) out_label_bitmap = Image.new( "1", @@ -257,13 +281,17 @@ def print_label(label_bitmap, margin=56 * 2, tape_size: int = 12): labelstream = label_rotated.tobytes() 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!") + die( + "An internal problem was encountered while processing the label " + "bitmap!" + ) label_rows = [ - labelstream[i: i + label_stream_row_length] + 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] + label_matrix = [ + array.array("B", label_row).tolist() for label_row in label_rows + ] # get device file name if not DEV_NODE: dev = getDeviceFile(DEV_CLASS, DEV_VENDOR, DEV_PRODUCT) @@ -282,7 +310,7 @@ def print_label(label_bitmap, margin=56 * 2, tape_size: int = 12): # Find and prepare device communication endpoints. dev = usb.core.find( custom_match=lambda d: ( - d.idVendor == DEV_VENDOR and d.idProduct == DEV_LM280_PRODUCT + d.idVendor == DEV_VENDOR and d.idProduct == DEV_LM280_PRODUCT ) ) @@ -312,14 +340,14 @@ def print_label(label_bitmap, margin=56 * 2, tape_size: int = 12): intf, custom_match=( lambda e: usb.util.endpoint_direction(e.bEndpointAddress) - == usb.util.ENDPOINT_OUT + == usb.util.ENDPOINT_OUT ), ) devin = usb.util.find_descriptor( intf, custom_match=( lambda e: usb.util.endpoint_direction(e.bEndpointAddress) - == usb.util.ENDPOINT_IN + == usb.util.ENDPOINT_IN ), ) @@ -328,8 +356,7 @@ def print_label(label_bitmap, margin=56 * 2, tape_size: int = 12): # create dymo labeler object try: - lm = DymoLabeler( - devout, devin, synwait=syn_wait, tape_size=tape_size) + lm = DymoLabeler(devout, devin, synwait=syn_wait, tape_size=tape_size) except IOError: die(access_error(dev)) diff --git a/src/dymoprint/font_config.py b/src/dymoprint/font_config.py index 630ba55..e9e2924 100644 --- a/src/dymoprint/font_config.py +++ b/src/dymoprint/font_config.py @@ -27,8 +27,7 @@ def font_filename(flag): if conf.read(CONFIG_FILE): # reading FONTS section if not "FONTS" in conf.sections(): - die('! config file "%s" not valid. Please change or remove.' % - CONFIG_FILE) + die('! config file "%s" not valid. Please change or remove.' % CONFIG_FILE) for style in style_to_file.keys(): style_to_file[style] = conf.get("FONTS", style) @@ -39,7 +38,7 @@ def parse_fonts() -> dict: DEFAULT_FONT_DIR = os.path.dirname(dymoprint_fonts.__file__) fonts = list() for f in os.listdir(DEFAULT_FONT_DIR): - m = re.match(r'(.*-.*).ttf', f) + m = re.match(r"(.*-.*).ttf", f) if m: fonts.append((m.group(1), os.path.join(DEFAULT_FONT_DIR, f))) return fonts diff --git a/src/dymoprint/gui.py b/src/dymoprint/gui.py index 2e5bac7..a2f61b2 100644 --- a/src/dymoprint/gui.py +++ b/src/dymoprint/gui.py @@ -1,18 +1,29 @@ -import sys import os +import sys +from PIL import Image, ImageOps, ImageQt from PyQt6 import QtCore from PyQt6.QtCore import QSize, Qt -from PyQt6.QtGui import QPixmap, QIcon, QColor, QPainter -from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QHBoxLayout -from PyQt6.QtWidgets import QGraphicsDropShadowEffect, QMessageBox, QSpinBox, QToolBar, QComboBox +from PyQt6.QtGui import QColor, QIcon, QPainter, QPixmap +from PyQt6.QtWidgets import ( + QApplication, + QComboBox, + QGraphicsDropShadowEffect, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QSpinBox, + QToolBar, + QVBoxLayout, + QWidget, +) from usb.core import USBError -from PIL import ImageOps, Image, ImageQt +import dymoprint_fonts from .dymo_print_engines import DymoPrinterServer, DymoRenderEngine from .q_dymo_labels_list import QDymoLabelList -import dymoprint_fonts class DymoPrintWindow(QWidget): @@ -31,7 +42,7 @@ def __init__(self): self.foreground_color = QComboBox() self.background_color = QComboBox() self.min_label_len = QSpinBox() - self.justify = QComboBox() + self.justify = QComboBox() self.init_elements() self.init_connections() @@ -44,7 +55,7 @@ def init_elements(self): ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) self.setWindowIcon(QIcon(os.path.join(ICON_DIR, "gui_icon.png"))) self.setGeometry(200, 200, 1100, 400) - printer_icon = QIcon.fromTheme('printer') + printer_icon = QIcon.fromTheme("printer") self.print_button.setIcon(printer_icon) self.print_button.setFixedSize(64, 64) self.print_button.setIconSize(QSize(48, 48)) @@ -56,43 +67,27 @@ def init_elements(self): self.margin.setMinimum(20) self.margin.setMaximum(1000) self.margin.setValue(56) - self.tape_size.addItem('12', 12) - self.tape_size.addItem('9', 9) - self.tape_size.addItem('6', 6) + self.tape_size.addItem("12", 12) + self.tape_size.addItem("9", 9) + self.tape_size.addItem("6", 6) self.min_label_len.setMinimum(0) self.min_label_len.setMaximum(1000) - self.justify.addItems([ - 'center', - 'left', - 'right' - ]) - - self.foreground_color.addItems([ - 'black', - 'white', - 'yellow', - 'blue', - 'red', - "green" - ]) - self.background_color.addItems([ - 'white', - 'black', - 'yellow', - 'blue', - 'red', - "green" - ]) + self.justify.addItems(["center", "left", "right"]) + + self.foreground_color.addItems( + ["black", "white", "yellow", "blue", "red", "green"] + ) + self.background_color.addItems( + ["white", "black", "yellow", "blue", "red", "green"] + ) def init_connections(self): self.margin.valueChanged.connect(self.list.render_label) self.tape_size.currentTextChanged.connect(self.update_params) self.min_label_len.valueChanged.connect(self.update_params) self.justify.currentTextChanged.connect(self.update_params) - self.foreground_color.currentTextChanged.connect( - self.list.render_label) - self.background_color.currentTextChanged.connect( - self.list.render_label) + self.foreground_color.currentTextChanged.connect(self.list.render_label) + self.background_color.currentTextChanged.connect(self.list.render_label) self.list.renderSignal.connect(self.update_label_render) self.print_button.clicked.connect(self.print_label) @@ -120,7 +115,8 @@ def init_layout(self): render_layout.addWidget(self.label_render) render_layout.addWidget(self.print_button) render_layout.setAlignment( - self.label_render, QtCore.Qt.AlignmentFlag.AlignCenter) + self.label_render, QtCore.Qt.AlignmentFlag.AlignCenter + ) self.window_layout.addWidget(settings_widget) self.window_layout.addWidget(self.list) @@ -136,15 +132,21 @@ def update_params(self): def update_label_render(self, label_bitmap): self.label_bitmap = label_bitmap - label_image = Image.new("L", ( - self.margin.value() + self.label_bitmap.width + self.margin.value(), self.label_bitmap.height)) + label_image = Image.new( + "L", + ( + self.margin.value() + self.label_bitmap.width + self.margin.value(), + self.label_bitmap.height, + ), + ) label_image.paste(self.label_bitmap, (self.margin.value(), 0)) label_image_inv = ImageOps.invert(label_image).copy() qim = ImageQt.ImageQt(label_image_inv) q_image = QPixmap.fromImage(qim) mask = q_image.createMaskFromColor( - QColor("255, 255, 255"), Qt.MaskMode.MaskOutColor) + QColor("255, 255, 255"), Qt.MaskMode.MaskOutColor + ) q_image.fill(QColor(self.background_color.currentText())) p = QPainter(q_image) p.setPen(QColor(self.foreground_color.currentText())) @@ -156,8 +158,7 @@ def update_label_render(self, label_bitmap): def print_label(self): try: - self.print_server.print_label( - self.label_bitmap, self.margin.value()*2) + self.print_server.print_label(self.label_bitmap, self.margin.value() * 2) except RuntimeError as err: QMessageBox.warning(self, "Printing Failed!", f"{err}") except USBError as err: diff --git a/src/dymoprint/labeler.py b/src/dymoprint/labeler.py index c665049..b05ddfa 100755 --- a/src/dymoprint/labeler.py +++ b/src/dymoprint/labeler.py @@ -28,8 +28,7 @@ class DymoLabeler: @staticmethod def max_bytes_per_line(tape_size=12): - return int(8*tape_size/12) - + return int(8 * tape_size / 12) # Max number of print lines to send before waiting for a response. This helps # to avoid timeouts due to differences between data transfer and @@ -39,7 +38,7 @@ def max_bytes_per_line(tape_size=12): # sensible timeout can also be calculated dynamically. synwait: Optional[int] - def __init__(self, devout, devin, synwait=None,tape_size=12): + def __init__(self, devout, devin, synwait=None, tape_size=12): """Initialize the LabelManager object. (HLF)""" self.tape_size = tape_size diff --git a/src/dymoprint/q_dymo_label_widgets.py b/src/dymoprint/q_dymo_label_widgets.py index b311e2b..8d5bcb3 100644 --- a/src/dymoprint/q_dymo_label_widgets.py +++ b/src/dymoprint/q_dymo_label_widgets.py @@ -1,10 +1,22 @@ import os -import dymoprint_fonts from PyQt6 import QtCore from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QSpinBox, QPlainTextEdit, QLineEdit -from PyQt6.QtWidgets import QPushButton, QFileDialog, QMessageBox, QVBoxLayout +from PyQt6.QtWidgets import ( + QComboBox, + QFileDialog, + QHBoxLayout, + QLabel, + QLineEdit, + QMessageBox, + QPlainTextEdit, + QPushButton, + QSpinBox, + QVBoxLayout, + QWidget, +) + +import dymoprint_fonts from .font_config import parse_fonts @@ -23,7 +35,8 @@ class BaseDymoLabelWidget(QWidget): render_label() Abstract method to be implemented by subclasses for rendering the label. """ - itemRenderSignal = QtCore.pyqtSignal(name='itemRenderSignal') + + itemRenderSignal = QtCore.pyqtSignal(name="itemRenderSignal") def content_changed(self): """ @@ -59,8 +72,7 @@ def __init__(self, render_engine, parent=None): self.render_engine = render_engine self.label = QPlainTextEdit("text") - self.label.setFixedHeight( - 15 * (len(self.label.toPlainText().splitlines()) + 2)) + self.label.setFixedHeight(15 * (len(self.label.toPlainText().splitlines()) + 2)) self.setFixedHeight(self.label.height() + 10) self.font_style = QComboBox() self.font_size = QSpinBox() @@ -71,7 +83,7 @@ def __init__(self, render_engine, parent=None): self.draw_frame = QSpinBox() self.align = QComboBox() - self.align.addItems(['left', 'center', 'right']) + self.align.addItems(["left", "center", "right"]) for (name, font_path) in parse_fonts(): self.font_style.addItem(name, font_path) @@ -82,7 +94,8 @@ def __init__(self, render_engine, parent=None): ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) item_icon = QLabel() item_icon.setPixmap( - QIcon(os.path.join(ICON_DIR, "txt_icon.png")).pixmap(32, 32)) + QIcon(os.path.join(ICON_DIR, "txt_icon.png")).pixmap(32, 32) + ) item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) layout.addWidget(item_icon) layout.addWidget(self.label) @@ -105,8 +118,7 @@ def content_changed(self): """ Updates the height of the label and emits the itemRenderSignal when the content of the label changes. """ - self.label.setFixedHeight( - 15 * (len(self.label.toPlainText().splitlines()) + 2)) + self.label.setFixedHeight(15 * (len(self.label.toPlainText().splitlines()) + 2)) self.setFixedHeight(self.label.height() + 10) self.itemRenderSignal.emit() @@ -119,15 +131,16 @@ def render_label(self): QMessageBox.warning: If the rendering fails. """ try: - render = self.render_engine.render_text(labeltext=self.label.toPlainText().splitlines(), - font_file_name=self.font_style.currentData(), - frame_width=self.draw_frame.value(), - font_size_ratio=self.font_size.value() / 100.0, - align=self.align.currentText()) + render = self.render_engine.render_text( + labeltext=self.label.toPlainText().splitlines(), + font_file_name=self.font_style.currentData(), + frame_width=self.draw_frame.value(), + font_size_ratio=self.font_size.value() / 100.0, + align=self.align.currentText(), + ) return render except BaseException as err: - QMessageBox.warning( - self, "TextDymoLabelWidget render fail!", f"{err}") + QMessageBox.warning(self, "TextDymoLabelWidget render fail!", f"{err}") return self.render_engine.render_empty() @@ -153,8 +166,7 @@ def __init__(self, render_engine, parent=None): layout = QHBoxLayout() ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) item_icon = QLabel() - item_icon.setPixmap( - QIcon(os.path.join(ICON_DIR, "qr_icon.png")).pixmap(32, 32)) + item_icon.setPixmap(QIcon(os.path.join(ICON_DIR, "qr_icon.png")).pixmap(32, 32)) item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) layout.addWidget(item_icon) layout.addWidget(self.label) @@ -174,8 +186,7 @@ def render_label(self): return render except BaseException as err: - QMessageBox.warning( - self, "QrDymoLabelWidget render fail!", f"{err}") + QMessageBox.warning(self, "QrDymoLabelWidget render fail!", f"{err}") return self.render_engine.render_empty() @@ -205,26 +216,29 @@ def __init__(self, render_engine, parent=None): ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) item_icon = QLabel() item_icon.setPixmap( - QIcon(os.path.join(ICON_DIR, "barcode_icon.png")).pixmap(32, 32)) + QIcon(os.path.join(ICON_DIR, "barcode_icon.png")).pixmap(32, 32) + ) item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) self.codding = QComboBox() - self.codding.addItems([ - "code39", - "code128", - "ean", - "ean13", - "ean8", - "gs1", - "gtin", - "isbn", - "isbn10", - "isbn13", - "issn", - "jan", - "pzn", - "upc", - "upca", - ]) + self.codding.addItems( + [ + "code39", + "code128", + "ean", + "ean13", + "ean8", + "gs1", + "gtin", + "isbn", + "isbn10", + "isbn13", + "issn", + "jan", + "pzn", + "upc", + "upca", + ] + ) layout.addWidget(item_icon) layout.addWidget(self.label) @@ -243,12 +257,12 @@ def render_label(self): """ try: render = self.render_engine.render_barcode( - self.label.text(), self.codding.currentText()) + self.label.text(), self.codding.currentText() + ) return render except BaseException as err: - QMessageBox.warning( - self, "BarcodeDymoLabelWidget render fail!", f"{err}") + QMessageBox.warning(self, "BarcodeDymoLabelWidget render fail!", f"{err}") return self.render_engine.render_empty() @@ -275,13 +289,17 @@ def __init__(self, render_engine, parent=None): ICON_DIR = os.path.dirname(dymoprint_fonts.__file__) item_icon = QLabel() item_icon.setPixmap( - QIcon(os.path.join(ICON_DIR, "img_icon.png")).pixmap(32, 32)) + QIcon(os.path.join(ICON_DIR, "img_icon.png")).pixmap(32, 32) + ) item_icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) - button = QPushButton('Select file') + button = QPushButton("Select file") file_dialog = QFileDialog() - button.clicked.connect(lambda: self.label.setText( - os.path.abspath(file_dialog.getOpenFileName()[0]))) + button.clicked.connect( + lambda: self.label.setText( + os.path.abspath(file_dialog.getOpenFileName()[0]) + ) + ) layout.addWidget(item_icon) layout.addWidget(self.label) @@ -300,6 +318,5 @@ def render_label(self): render = self.render_engine.render_picture(self.label.text()) return render except BaseException as err: - QMessageBox.warning( - self, "ImageDymoLabelWidget render fail!", f"{err}") + QMessageBox.warning(self, "ImageDymoLabelWidget render fail!", f"{err}") return self.render_engine.render_empty() diff --git a/src/dymoprint/q_dymo_labels_list.py b/src/dymoprint/q_dymo_labels_list.py index ca40d5a..0135f4d 100644 --- a/src/dymoprint/q_dymo_labels_list.py +++ b/src/dymoprint/q_dymo_labels_list.py @@ -1,10 +1,13 @@ - +from PIL import Image from PyQt6 import QtCore -from PyQt6.QtWidgets import QListWidget, QListWidgetItem, QAbstractItemView, QMenu -from .q_dymo_label_widgets import TextDymoLabelWidget, QrDymoLabelWidget, BarcodeDymoLabelWidget, \ - ImageDymoLabelWidget +from PyQt6.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem, QMenu -from PIL import Image +from .q_dymo_label_widgets import ( + BarcodeDymoLabelWidget, + ImageDymoLabelWidget, + QrDymoLabelWidget, + TextDymoLabelWidget, +) class QDymoLabelList(QListWidget): @@ -24,9 +27,9 @@ class QDymoLabelList(QListWidget): contextMenuEvent(self, event): Overrides the default context menu event to add or delete label widgets. """ - renderSignal = QtCore.pyqtSignal(Image.Image, name='renderSignal') + renderSignal = QtCore.pyqtSignal(Image.Image, name="renderSignal") - def __init__(self, render_engine, min_payload_len=0, justify='center', parent=None): + def __init__(self, render_engine, min_payload_len=0, justify="center", parent=None): super(QDymoLabelList, self).__init__(parent) self.min_payload_len = min_payload_len self.justify = justify @@ -50,7 +53,7 @@ def dropEvent(self, e) -> None: super().dropEvent(e) self.render_label() - def update_params(self, render_engine, min_payload_len=0, justify='center'): + def update_params(self, render_engine, min_payload_len=0, justify="center"): """ Updates the render engine used for rendering the label. Args: @@ -77,7 +80,9 @@ def render_label(self): if item_widget and item: item.setSizeHint(item_widget.sizeHint()) bitmaps.append(item_widget.render_label()) - label_bitmap = self.render_engine.merge_render(bitmaps, self.min_payload_len, self.justify) + label_bitmap = self.render_engine.merge_render( + bitmaps, self.min_payload_len, self.justify + ) self.renderSignal.emit(label_bitmap) diff --git a/src/dymoprint/utils.py b/src/dymoprint/utils.py index d1db647..571e4b8 100755 --- a/src/dymoprint/utils.py +++ b/src/dymoprint/utils.py @@ -29,8 +29,7 @@ def die(message=None) -> NoReturn: def pprint(par, fd=sys.stdout): rows, columns = struct.unpack( - "HH", fcntl.ioctl(sys.stderr, termios.TIOCGWINSZ, - struct.pack("HH", 0, 0)) + "HH", fcntl.ioctl(sys.stderr, termios.TIOCGWINSZ, struct.pack("HH", 0, 0)) ) print(textwrap.fill(par, columns), file=fd)