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