# import brother_ql from time import sleep import os.path from PIL import Image, ImageEnhance import numpy as np # Importing the module to manage the connection to the printer. import escpos.printer 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: """ On printers that support it, we check that the printer has paper """ 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 RuntimeError("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): """ Check if the printer online ? Is the communication with the printer successfull ? """ # TODO: This could happen directly when creating a new Printer class if os.getenv("FLASK_DEBUG"): waiting_elapsed = 15 else: waiting_elapsed = 10 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 = escpos.printer.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 %s : %s", waiting_elapsed, str(e), ) pass try: if p.is_online(): self.ready = True self.app.logger.debug("Printer online !") except Exception as e: self.app.logger.error( "Error while getting the printer online %s : %s", waiting_elapsed, str(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): if not isinstance(msg, str): self.app.logger.error( "It is not possible to print a " + str(type(msg)) + ", only strings." ) raise ValueError # We make sure that the signature is not something too goofy clean_msg = str(msg) + "\n" clean_signature = str(signature) # Make checks on the size of the message being printed 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." ) # Do the actual printing # We would pop the next element in the queue here, if it's a sms type 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)) raise RuntimeError( "Unable to print a SMS, the printer couldn't do it." ) from e self.app.logger.info("Printed text") return True def _print_img(self, path, signature="", center=True, process=False): clean_signature = str(signature) if len(signature) > 256: self.app.logger.warning( "Could not print signature of this length: " + str(len(clean_signature)) ) raise ValueError( "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 OSError( "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 RuntimeError as e: self.app.logger.error( "Error while processing the image, aborting print : %s", str(e) ) raise e else: self.app.logger.warning("Not proccessing the image") try: self.printer.open(self.usb_args) self.printer.image(path, center=center) self.printer.textln(signature) self.printer.close() self.app.logger.debug("Printed an image : " + str(path)) except Exception as e: self.app.logger.error(str(e)) raise RuntimeError("Could not print the picture") from e finally: try: os.remove(path) except OSError as e: raise e self.app.logger.debug("Removed image : " + str(path)) try: self.printer.close() except Exception as e: self.app.logger.error( "Could not close the printer connexion %s", str(e) ) raise RuntimeError("Could not close the printer connexion. ") from e 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 print_task(self, task_type, data): """Execute actual print based on task type""" match (task_type.value): case "text": self._print_sms(data["txt"], signature=data["sign"]) case "image": self._print_img( data["img"], signature=data["sign"], process=data["process"] ) case "cut": self._cut() case _: raise RuntimeError("This task type is not supported") 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.Resampling.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) # 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) self.app.logger.debug("Processed and saved image.") 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