# Importing the module to mage the connection to the printer. from flask import flash 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...') flash("No more paper on the printer. Sorry.",category='error') 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 ! ') flash('Printer needs paper to be changed very soon ! ', category='info') 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) -> None: clean_msg = str(msg) clean_signature = str(signature) if len(clean_msg) > 4096 or len(clean_msg) < 3 : 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 between 3 and 4096 caracters long.") if len(signature) > 256 or len(signature) < 3: 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 between 3 and 256 caracters long.") self.check_paper() try: self.printer.open(self.usb_args); self.printer.set(align='left', font='a', bold=False, underline=0, width=1, height=1, density=8, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False) self.printer.set(align='left', font='a', bold=True, underline=1, width=1, height=1, density=6, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False) self.printer.textln(clean_msg) self.printer.textln("") self.printer.set(align='left', font='b', bold=False, underline=1, width=1, height=1, density=9, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False) self.printer.textln("> " + clean_signature + " @ " + strftime("%Y-%m-%d %H:%M:%S", gmtime())) self.printer.textln("") self.printer.textln("Printed by LittlePrinter ") self.printer.textln("n07070.xyz/articles/littleprynter") self.printer.textln("") self.printer.cut() self.printer.close() except Exception as e: self.app.logger.error("Unable to print because : " + str(e)) raise e flash("Message printed : " + clean_msg ,category='info') return True def print_img(self, path, sign) -> None: clean_signature = str(sign) if len(sign) > 256 or len(sign) < 3: 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 between 3 and 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)) try: self.app.logger.debug("Proccessing the image") path = process_image(self, path) except Exception as e: self.app.logger.error(str(e)) raise e try: self.check_paper() self.printer.open(self.usb_args) self.printer.textln("Printed by LittlePrinter ") self.printer.textln("n07070.xyz/articles/littleprynter") self.printer.textln("> " + clean_signature + " @ " + strftime("%Y-%m-%d %H:%M:%S", gmtime())) self.printer.image(path) self.printer.cut() self.printer.close() self.app.logger.debug("Printed an image : " + str(path)) os.remove(path) self.app.logger.debug("Removed image.") except Exception as e: self.printer.close() self.app.logger.error(str(e)) raise e 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: flash("Image is too long, sorry! Keep it below 575×1000 pixels.", 'error') 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