Update the way a printer is found, update the configuration of Brother

Printers
This commit is contained in:
n07070
2026-11-11 16:58:44 +01:00
parent fd0507ff4f
commit 704b7b83cd
5 changed files with 231 additions and 125 deletions

View File

@@ -11,9 +11,12 @@ from enum import Enum
import uuid
import threading
import usb.core
from PIL import Image, ImageEnhance
from pathlib import Path
from uuid import uuid4
from PIL import Image, ImageEnhance, ImageDraw, ImageFont
import numpy as np
# Importing the modules needed for each supported printer Type
import escpos.printer
from brother_ql.models import ModelsManager
@@ -386,8 +389,9 @@ class BrotherPrinter(Printer):
"""
def __init__(self, app, vendor_id, device_id):
super().__init__(
app, vendor_id, device_id, printer_type=PrinterType.BROTHER
app, vendor_id, device_id, printer_type=PrinterType.BROTHER
)
self.printer = None
self.usb_args = {}
@@ -450,16 +454,21 @@ class BrotherPrinter(Printer):
def _state(self):
return self.ready
def _print_txt(self, msg, signature="", bold=False):
"""Print text after converting it to an image"""
self._print_img(text_to_image(msg))
def _print_img(self, data):
"""
Print a raster image via a Brother QL printer
"""
label_type = "102"
# TODO : make this label type go in the configuration
label_type = "62"
rotate = 0
dither = False
data = _process_image(self, path=data, max_width=720, max_height=720)
self.app.logger.debug("Printing image %s", data)
try:
# Prepare the image for printing
qlr = BrotherQLRaster(self.printer_info["model"])
@@ -545,6 +554,9 @@ class BrotherPrinter(Printer):
case "cut":
# The cut happens by default on Brother QL printers.
pass
case "text":
self._print_txt(data["txt"], signature=data["sign"])
self.ready = True
case _:
raise RuntimeError("This task type is not supported")
except Exception as e:
@@ -559,56 +571,128 @@ class BrotherPrinter(Printer):
def _process_image(self, path, max_width=575, max_height=1000):
brightness_factor = 1.5 # Used only if image is too dark
brightness_threshold = 100 # Brightness threshold (0–255)
contrast_factor = 2 # Less than 1.0 = lower contrast
brightness_threshold = 100
contrast_factor = 2.0
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 from RGBA to RGBA")
original_img = original_img.convert("RGB")
# Resize while maintaining aspect ratio
original_img = original_img.resize((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)
with Image.open(path) as img:
# Preserve aspect ratio while fitting within the maximum dimensions.
img.thumbnail(
(max_width, max_height),
Image.Resampling.LANCZOS,
)
grayscale = img.convert("L")
avg_brightness = np.asarray(grayscale, dtype=np.float32).mean()
self.app.logger.debug(
"Average brightness of the image: %.2f", 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}"
max(
1 + (brightness_threshold - avg_brightness)
/ brightness_threshold,
1.1,
),
2.5,
)
self.app.logger.debug(
"Image is dark; increasing brightness by %.2f",
brightness_factor,
)
grayscale = ImageEnhance.Brightness(grayscale).enhance(
brightness_factor
)
enhancer = ImageEnhance.Brightness(grayscale)
grayscale = enhancer.enhance(brightness_factor)
# Computer current contrast of grayscale image
contrast = np.clip(np.std(np.array(grayscale)), 0, 255)
self.app.logger.debug("Standard deviation of the contrast : %s", contrast)
# # Enhance contrast
contrast_enhancer = ImageEnhance.Contrast(grayscale)
original_img = contrast_enhancer.enhance(contrast_factor)
contrast = float(np.asarray(grayscale, dtype=np.float32).std())
self.app.logger.debug("Grayscale standard deviation: %.2f", contrast)
# Save the enhanced result.
processed = ImageEnhance.Contrast(grayscale).enhance(contrast_factor)
# Convert to JPEG and save
jpeg_path = os.path.splitext(path)[0] + "_processed.jpg"
grayscale.save(jpeg_path, format="JPEG", quality=95, optimize=True)
self.app.logger.debug("Processed and saved image.")
processed.save(jpeg_path, format="JPEG", quality=95, optimize=True)
self.app.logger.debug("Processed and saved image: %s", jpeg_path)
return jpeg_path
def text_to_image(
text,
width=400,
font_path="src/static/font/MonospaceTypewriter.ttf",
font_size=24,
padding=10,
line_spacing=4,
background="white",
fill="black",
output_dir="src/static/uploads",
):
"""Converts a string to an image"""
font = ImageFont.truetype(font_path, font_size)
# Measure and wrap text by pixel width.
measure_img = Image.new("RGB", (1, 1))
draw = ImageDraw.Draw(measure_img)
max_text_width = width - 2 * padding
if max_text_width <= 0:
raise ValueError("width must be greater than twice the padding")
lines = []
for paragraph in text.splitlines() or [""]:
words = paragraph.split()
if not words:
lines.append("")
continue
line = ""
for word in words:
candidate = f"{line} {word}".strip()
if draw.textbbox((0, 0), candidate, font=font)[2] <= max_text_width:
line = candidate
continue
if line:
lines.append(line)
# Split a word that is wider than the available width.
line = ""
piece = ""
for char in word:
if piece and draw.textbbox((0, 0), piece + char, font=font)[2] > max_text_width:
lines.append(piece)
piece = char
else:
piece += char
line = piece
if line:
lines.append(line)
if not lines:
lines = [""]
ascent, descent = font.getmetrics()
line_height = ascent + descent
height = (
2 * padding
+ len(lines) * line_height
+ (len(lines) - 1) * line_spacing
)
image = Image.new("RGB", (width, height), background)
draw = ImageDraw.Draw(image)
y = padding
for line in lines:
draw.text((padding, y), line, font=font, fill=fill)
y += line_height + line_spacing
upload_dir = Path(output_dir)
upload_dir.mkdir(parents=True, exist_ok=True)
image_path = upload_dir / f"{uuid4().hex}.png"
image.save(image_path, format="PNG")
return str(image_path)