diff --git a/README.md b/README.md index 1d9457f..84886ce 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 0000000..3da207e Binary files /dev/null and b/data/fonts/barcode_icon.png differ diff --git a/data/fonts/gui_icon.png b/data/fonts/gui_icon.png new file mode 100644 index 0000000..ef08a20 Binary files /dev/null and b/data/fonts/gui_icon.png differ diff --git a/data/fonts/img_icon.png b/data/fonts/img_icon.png new file mode 100644 index 0000000..18bd0e5 Binary files /dev/null and b/data/fonts/img_icon.png differ diff --git a/data/fonts/qr_icon.png b/data/fonts/qr_icon.png new file mode 100644 index 0000000..5f8b8fd Binary files /dev/null and b/data/fonts/qr_icon.png differ diff --git a/data/fonts/txt_icon.png b/data/fonts/txt_icon.png new file mode 100644 index 0000000..9a5724f Binary files /dev/null and b/data/fonts/txt_icon.png differ diff --git a/doc/DymoPrint_example_1.png b/doc/DymoPrint_example_1.png new file mode 100644 index 0000000..69cc4a8 Binary files /dev/null and b/doc/DymoPrint_example_1.png differ diff --git a/doc/DymoPrint_example_2.png b/doc/DymoPrint_example_2.png new file mode 100644 index 0000000..7a1ae0b Binary files /dev/null and b/doc/DymoPrint_example_2.png differ diff --git a/doc/DymoPrint_example_3.png b/doc/DymoPrint_example_3.png new file mode 100644 index 0000000..b3951d5 Binary files /dev/null and b/doc/DymoPrint_example_3.png differ diff --git a/setup.cfg b/setup.cfg index 13ff9bd..869294c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,6 +36,8 @@ install_requires = PyQRCode>=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/command_line.py b/src/dymoprint/command_line.py index 2e67c34..8ca8d4e 100755 --- a/src/dymoprint/command_line.py +++ b/src/dymoprint/command_line.py @@ -7,38 +7,20 @@ # === 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 .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 . import __version__ +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 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( @@ -61,6 +43,29 @@ def parse_args(): default="r", help="Set fonts style (regular,bold,italic,narrow)", ) + parser.add_argument( + "-a", + choices=[ + "left", + "center", + "right", + ], + default="left", + help="Align multiline text (left,center,right)", + ) + parser.add_argument( + "-l", type=int, default=0, 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", @@ -104,13 +109,26 @@ 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, 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( + "-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() def main(): args = parse_args() + print_server = DymoPrinterServer() + render_engine = DymoRenderEngine(args.t) # read config file FONT_FILENAME = font_filename(args.s) @@ -133,206 +151,39 @@ 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, args.a + ) + ) 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")) + bitmaps.append(render_engine.render_picture(args.picture)) - 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] - - # 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] + margin = args.m + 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) # 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", (margin + label_bitmap.width + margin, label_bitmap.height) + ) + label_image.paste(label_bitmap, (margin, 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) diff --git a/src/dymoprint/dymo_print_engines.py b/src/dymoprint/dymo_print_engines.py new file mode 100644 index 0000000..bbc5961 --- /dev/null +++ b/src/dymoprint/dymo_print_engines.py @@ -0,0 +1,370 @@ +from __future__ import annotations + +import array +import math +import os + +import barcode as barcode_module +import usb +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, + QRCode, +) +from .utils import access_error, die, draw_image, getDeviceFile, 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, label_len=1): + """ + 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", (label_len, 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, + align="left", + ): + """ + 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 + 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): + """ + 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 = img.resize( + (int(math.ceil(img.width * ratio)), label_height) + ) + + img = img.convert("L", palette=Image.AFFINE) + 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)) + + def merge_render(self, bitmaps, min_payload_len=0, justify="center"): + """ + 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 + elif len(bitmaps) == 0: + label_bitmap = self.render_empty(max(min_payload_len, 1)) + else: + label_bitmap = bitmaps[0] + + 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": + offset = max(0, int(min_payload_len - label_bitmap.width)) + out_label_bitmap = Image.new( + "1", + ( + min_payload_len, + label_bitmap.height, + ), + ) + out_label_bitmap.paste(label_bitmap, box=(offset, 0)) + return out_label_bitmap + + return label_bitmap + + +class DymoPrinterServer: + @staticmethod + def print_label(label_bitmap, margin=56 * 2, 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 + in_usb_mode = False + 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.") + in_usb_mode = True + + 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) + + if in_usb_mode: + usb.util.dispose_resources(dev) diff --git a/src/dymoprint/font_config.py b/src/dymoprint/font_config.py index 61d7860..e9e2924 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 @@ -31,3 +32,13 @@ def font_filename(flag): 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..a2f61b2 --- /dev/null +++ b/src/dymoprint/gui.py @@ -0,0 +1,172 @@ +import os +import sys + +from PIL import Image, ImageOps, ImageQt +from PyQt6 import QtCore +from PyQt6.QtCore import QSize, Qt +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 + +import dymoprint_fonts + +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.min_label_len = QSpinBox() + self.justify = QComboBox() + + self.init_elements() + self.init_connections() + self.init_layout() + + self.list.render_label() + + 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, 1100, 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.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"] + ) + + 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.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("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 ")) + 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_params(self): + self.render_engine = DymoRenderEngine(self.tape_size.currentData()) + 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 + 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() * 2) + 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/labeler.py b/src/dymoprint/labeler.py index 18a3ec6..b05ddfa 100755 --- a/src/dymoprint/labeler.py +++ b/src/dymoprint/labeler.py @@ -26,7 +26,9 @@ 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 +38,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 +121,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 +139,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 +164,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)""" diff --git a/src/dymoprint/q_dymo_label_widgets.py b/src/dymoprint/q_dymo_label_widgets.py new file mode 100644 index 0000000..8d5bcb3 --- /dev/null +++ b/src/dymoprint/q_dymo_label_widgets.py @@ -0,0 +1,322 @@ +import os + +from PyQt6 import QtCore +from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import ( + QComboBox, + QFileDialog, + QHBoxLayout, + QLabel, + QLineEdit, + QMessageBox, + QPlainTextEdit, + QPushButton, + QSpinBox, + QVBoxLayout, + QWidget, +) + +import dymoprint_fonts + +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(150) + self.font_size.setMinimum(0) + 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: + 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) + 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): + """ + 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, + align=self.align.currentText(), + ) + 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( + os.path.abspath(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() diff --git a/src/dymoprint/q_dymo_labels_list.py b/src/dymoprint/q_dymo_labels_list.py new file mode 100644 index 0000000..0135f4d --- /dev/null +++ b/src/dymoprint/q_dymo_labels_list.py @@ -0,0 +1,140 @@ +from PIL import Image +from PyQt6 import QtCore +from PyQt6.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem, QMenu + +from .q_dymo_label_widgets import ( + BarcodeDymoLabelWidget, + ImageDymoLabelWidget, + QrDymoLabelWidget, + TextDymoLabelWidget, +) + + +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(Image.Image, name="renderSignal") + + 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) + 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_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)) + 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.min_payload_len, self.justify + ) + + 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("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()) + + 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() diff --git a/src/dymoprint/utils.py b/src/dymoprint/utils.py index 81c5cf7..571e4b8 100755 --- a/src/dymoprint/utils.py +++ b/src/dymoprint/utils.py @@ -15,13 +15,15 @@ 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) sys.exit(1) @@ -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, )