# Importing the module to mage the connection to the printer. from escpos.printer import Usb, USBNotFoundError 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 = Usb(self.device_id, self.vendor_id, 0, profile="TM-P80") except USBNotFoundError 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 = 30 # 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