# Importing the module to manage the connection to the printer. import escpos.printer as escp import brother-ql-inventree from time import sleep, gmtime, strftime import os.path from PIL import Image, ImageEnhance, ImageOps import numpy as np class Printer(object): """ # The connection is based on the ESC/POS library ## Connection to the USB printer ## Making sure the printer is alive ## Making sure it has paper ## Define default print settings ## Print starting message, log time of first print, cut. ## Annonce readyness : return a positive pong message. """ # Is the printer ready to accept a new print ? ready = False def __init__(self, app, device_id, vendor_id): super(Printer, self).__init__() self.app = app self.ready = False self.printer = None self.device_id = device_id self.vendor_id = vendor_id self.usb_args = {} self.usb_args["idVendor"] = self.device_id self.usb_args["idProduct"] = self.vendor_id def check_paper(self) -> bool: # Let's check paper status self.app.logger.debug("Checking paper status...") self.printer.open(self.usb_args) status = self.printer.paper_status() match status: case 0: self.app.logger.error("Printer has no more paper, aborting...") self.printer.close() raise Exception("No more paper in the printer") case 1: self.app.logger.warning( "Printer needs paper to be changed very soon ! " ) self.printer.close() case 2: self.app.logger.debug("Printer has paper, good to go") self.printer.close() def init_printer(self): # Is the printer online ? Is the communication with the printer successfull ? waiting_elapsed = 30 self.app.logger.debug("Waiting for printer to get online...") while not self.ready: try: # This also calls open(), which we need to close() # or else the device will appear as busy. p = escp.Usb(self.device_id, self.vendor_id, 0, profile="TM-P80") except Exception as e: self.app.logger.error( "The USB device is not plugged in, trying again : " + str(e) ) pass try: if p.is_online(): self.ready = True self.app.logger.debug("Printer online !") except Exception as e: pass sleep(1) waiting_elapsed -= 1 if waiting_elapsed < 1: self.app.logger.error( "Printer took more than 30 seconds to get online, aborting..." ) waiting_elapsed = 1 # Reset the waiting time for the next print. return False # Setting up the printing options. p.set( align="center", font="a", bold=False, underline=0, width=1, height=1, density=9, invert=False, smooth=False, flip=False, double_width=False, double_height=False, custom_size=False, ) # Beware : if we print every time the printer becomes ready, it means # we are printing before and after every print ! self.printer = p self.ready = True self.printer.close() self.check_paper() return True def print_sms(self, msg, signature="", bold=False): clean_msg = str(msg) + "\n" clean_signature = str(signature) if len(clean_msg) > 4096: self.app.logger.warning( "Could not print message of this length: " + str(len(clean_msg)) ) raise Exception( "Could not print message of this length :" + str(len(clean_msg)) + ", needs to be below 4096 caracters long." ) if len(signature) > 256: self.app.logger.warning( "Could not print signature of this length: " + str(len(clean_signature)) ) raise Exception( "Could not print signature of this length :" + str(len(clean_signature)) + ", needs to be below 256 caracters long." ) try: self.printer.open(self.usb_args) self.printer.set(align="center", font="a", bold=bold) self.printer.textln(clean_msg) if clean_signature: self.printer.textln(clean_signature) self.printer.close() except Exception as e: self.app.logger.error("Unable to print because : " + str(e)) self.app.logger.info("Printed text") return True def print_img(self, path, sign="", center=True, process=False): clean_signature = str(sign) if len(sign) > 256: self.app.logger.warning( "Could not print signature of this length: " + str(len(clean_signature)) ) raise Exception( "Could not print signature of this length :" + str(len(clean_signature)) + ", needs to be below 256 caracters long." ) if not os.path.isfile(str(path)): self.app.logger.warning("File does not exist : " + str(path)) raise Exception( "The file path for this image :" + str(path) + " wasn't found. Please try again." ) else: self.app.logger.debug("Printing file from " + str(path)) if process: try: self.app.logger.debug("Proccessing the image") path = process_image(self, path) except Exception as e: self.app.logger.error(str(e)) return False else: self.app.logger.warning("Not proccessing the image") try: self.printer.open(self.usb_args) self.printer.image(path, center=center) self.printer.close() self.app.logger.debug("Printed an image : " + str(path)) os.remove(path) self.app.logger.debug("Removed image : " + str(path)) except Exception as e: self.printer.close() self.app.logger.error(str(e)) return False self.app.logger.info("Printed a picture") return True def qr(self, content): try: self.printer.open(self.usb_args) self.printer.qr(content, center=True) self.printer.close() except Exception as e: self.printer.close() self.app.logger.error(str(e)) return False self.app.logger.info("Printed a QR") return True def cut(self): try: self.printer.open(self.usb_args) self.printer.cut() self.printer.close() except Exception as e: self.printer.close() self.app.logger.error(str(e)) raise e self.app.logger.info("Did a cut") return True def process_image(self, path): brightness_factor = 1.5 # Used only if image is too dark brightness_threshold = 100 # Brightness threshold (0–255) contrast_factor = 0.6 # Less than 1.0 = lower contrast max_width = 575 max_height = 1000 with Image.open(path) as original_img: # Convert to RGB if needed (JPEG doesn't support alpha) if original_img.mode in ("RGBA", "P"): self.app.logger.debug("Converting the image to RGB from RGBA") original_img = original_img.convert("RGB") # Resize while maintaining aspect ratio original_img.thumbnail((max_width, max_height), Image.LANCZOS) self.app.logger.debug("Resized the image") # # Convert to grayscale for dithering # dithered_img = original_img.convert("L").convert("1") # Dithering using default method (Floyd–Steinberg) # self.app.logger.debug("Dithered the image") # Compute brightness of original image (grayscale average) grayscale = original_img.convert("L") avg_brightness = np.array(grayscale).mean() self.app.logger.debug( "Average brightness of the image : " + str(avg_brightness) ) # Dynamically compute brightness factor if too dark if avg_brightness < brightness_threshold: brightness_factor = ( 1 + (brightness_threshold - avg_brightness) / brightness_threshold ) brightness_factor = min( max(brightness_factor, 1.1), 2.5 ) # Clamp between 1.1 and 2.5 self.app.logger.debug( f"Image too dark, increasing brightness by a factor of {brightness_factor:.2f}" ) enhancer = ImageEnhance.Brightness(original_img) original_img = enhancer.enhance(brightness_factor) # # Reduce contrast # contrast_enhancer = ImageEnhance.Contrast(original_img) # original_img = contrast_enhancer.enhance(contrast_factor) # Final resize check if original_img.height > max_height: raise ValueError("Image is too long, sorry! Keep it below 575×1000 pixels.") self.app.logger.error( "Image is too long, sorry! Keep it below 575×1000 pixels." ) return False # Convert to JPEG and save jpeg_path = os.path.splitext(path)[0] + "_processed.jpg" original_img.save(jpeg_path, format="JPEG", quality=95, optimize=True) return jpeg_path def discover_printers(): """ We try to find all the connected printers ( 0 or n ) to this system. For every type of supported printer, we try to autodiscover them. http://www.linux-usb.org/usb.ids A list of USB vendor IDs 04b8 Seiko Epson Corp. 04f9 Brother Industries, Ltd """ def find_and_parse_borther_ql_printer(): ## We might be able to no use this because there is a `discover` command in https://github.com/pklaus/brother_ql#usage ## Code stolen from https://framagit.org/stickoeur/diagnostickoeur/-/blob/no-masters/printit.py?ref_type=heads """Find and parse Brother QL printer information.""" model_manager = ModelsManager() # Debug print to show we're searching # print("Searching for Brother QL printer...") for backend_name in ["pyusb", "linux_kernel"]: try: #print(f"Trying backend: {backend_name}") backend = backend_factory(backend_name) available_devices = backend["list_available_devices"]() #print(f"Found {len(available_devices)} devices with {backend_name} backend") for printer in available_devices: #print(f"Found device: {printer}") identifier = printer["identifier"] parts = identifier.split("/") if len(parts) < 4: #print(f"Skipping device with invalid identifier format: {identifier}") continue protocol = parts[0] device_info = parts[2] serial_number = parts[3] try: vendor_id, product_id = device_info.split(":") except ValueError: #print(f"Invalid device info format: {device_info}") continue # Default model model = "QL-570" # Try to match product ID to determine actual model try: product_id_int = int(product_id, 16) for m in model_manager.iter_elements(): if m.product_id == product_id_int: model = m.identifier break #print(f"Matched printer model: {model}") except ValueError: #print(f"Invalid product ID format: {product_id}") continue printer_info = { "identifier": identifier, "backend": backend_name, "model": model, "protocol": protocol, "vendor_id": vendor_id, "product_id": product_id, "serial_number": serial_number, } #print(f"Found printer: {printer_info}") return printer_info except Exception as e: #print(f"Error with backend {backend_name}: {str(e)}") continue print("No Brother QL printer found") return None def fint_and_parse_epson_printer(): pass