Files
littleprynter/src/printer.py

379 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (0255)
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 (FloydSteinberg)
# 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