hs3-baza-zasobow-dashboard/labeler/infra/renderer.py
2026-04-16 13:43:52 +02:00

169 lines
5.3 KiB
Python

import qrcode
import textwrap
from string import ascii_letters
from PIL import ImageFont, ImageDraw, Image as PILImage
from labeler.domain.objects import Image, LabelDefinition
from labeler.interfaces import Renderer
DPI = 360.0
def points_to_pixels(point_size: float):
return int(point_size * (72 / DPI))
class PILRenderer(Renderer):
def __init__(self):
self.font_path = "/Library/Fonts/Arial Unicode.ttf"
def render_label(self, label_definition: LabelDefinition) -> Image:
if label_definition.length is None:
pil_image = self.__render_no_fixed_lenth(label_definition)
else:
pil_image = self.__render_fixed_length(label_definition)
return Image.from_pil(pil_image)
def __render_fixed_length(self, label_definition: LabelDefinition):
width = label_definition.pixel_width
length = label_definition.pixel_length
font, text = self.__get_font(
label_definition.text,
width,
length,
)
im = PILImage.new("1", (length, width), 1)
draw = ImageDraw.Draw(im)
draw.text(
(length / 2, width / 2),
text,
font=font,
fill=0,
anchor="mm",
align="center",
)
return im
def __render_no_fixed_lenth(self, label_definition: LabelDefinition):
lines_to_print = label_definition.text.count("\n") + 1
text = "\n".join([line.strip() for line in label_definition.text.split("\n")])
text_height = label_definition.pixel_width // lines_to_print
while text_height > 0:
font = ImageFont.truetype(
"/Library/Fonts/Arial Unicode.ttf",
text_height,
)
if lines_to_print > 1:
occupied_height = font.getsize_multiline(text)[1]
else:
occupied_height = font.getsize(text)[1]
if occupied_height <= label_definition.pixel_width:
break
text_height -= 1
sizes = [font.getsize(line) for line in text.split("\n")]
length = max(length for length, height in sizes)
im = PILImage.new("1", (length, label_definition.pixel_width), 1)
draw = ImageDraw.Draw(im)
draw.text(
(length / 2, label_definition.pixel_width / 2),
text,
font=font,
fill=0,
anchor="mm",
align="center",
)
return im
def render_qrcode(self, label_definition: LabelDefinition) -> Image:
width = label_definition.pixel_width
length = label_definition.pixel_length
text = label_definition.text
font = self.__get_font_for_qr()
qr = qrcode.QRCode(box_size=7)
qr.add_data(f"https://hs3.pl/db/{text}")
qr.make(fit=True)
qr_img = qr.make_image()
print(width, length)
pil_image = PILImage.new("1", (width, length), 1)
draw = ImageDraw.Draw(pil_image)
pil_image.paste(qr_img)
draw.text(
(width / 2, 232),
"HS3-DB",
font=font,
fill=0,
anchor="mm",
align="center",
)
draw.text(
(20, 232 + 34),
f"ID: {text}",
font=font,
fill=0,
align="left",
)
return Image.from_pil(pil_image.transpose(PILImage.Transpose.ROTATE_90))
def __get_font_for_qr(self):
font_size = int(360 / 9)
return ImageFont.truetype("fonts/SourceCodePro-SemiBold.ttf", font_size)
def __get_font(self, text: str, max_width: int, max_length: int):
font_size = max_width
step = max_width // 2
last_good = None
last_corrected = None
while step > 1:
fits, corrected = self.__will_font_fit(
text, self.font_path, font_size, max_width, max_length
)
if fits:
last_good = font_size
last_corrected = corrected
font_size += step
else:
font_size -= step
step //= 2
return ImageFont.truetype(self.font_path, last_good), last_corrected
def __will_font_fit(
self, text: str, font_path: str, font_size: int, max_width: int, max_length: int
):
font = ImageFont.truetype(font_path, font_size)
if "\n" in text:
text_width, text_height = font.getsize_multiline(text)
else:
text_width, text_height = font.getsize(text)
if text_height > max_width:
return False, None
if text_width <= max_length:
# Now we know that the text fits. We can stop trying
return True, text
avg_char_width = sum(font.getsize(char)[0] for char in ascii_letters) / len(
ascii_letters
)
charachters_per_line = max_length // avg_char_width
if charachters_per_line < max(len(line) for line in text.split(" ")):
return False, None
wrapped = textwrap.fill(text, charachters_per_line)
wrapped_width, wrapped_height = font.getsize_multiline(wrapped)
if wrapped_height <= max_width and wrapped_width <= max_length:
# Now we know that the text fits. We can stop trying
return True, wrapped
return False, None