From 3865dcc40250a3ee85bbca01a19b6d7799ec4996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Bry=C5=82kowski?= Date: Sun, 9 Jul 2023 14:24:31 +0200 Subject: [PATCH] init commit with repo structure and basic files --- labeler/adapter/fastapi.py | 0 labeler/app/labeler.py | 25 ++++ labeler/domain/objects.py | 123 +++++++++++++++++ labeler/infra/renderer.py | 125 ++++++++++++++++++ labeler/interfaces.py | 15 +++ tests/conftest.py | 1 + tests/fixtures.py | 25 ++++ tests/labeler/domain/test_objects.py | 29 ++++ .../multiline_label_no_fixed_width.png | Bin 0 -> 1513 bytes .../test_images/multiline_label_test.png | Bin 0 -> 1773 bytes .../infra/test_images/no_fixed_width.png | Bin 0 -> 1586 bytes .../infra/test_images/simple_label_test.png | Bin 0 -> 1676 bytes tests/labeler/infra/test_renderer.py | 50 +++++++ 13 files changed, 393 insertions(+) create mode 100644 labeler/adapter/fastapi.py create mode 100644 labeler/app/labeler.py create mode 100644 labeler/domain/objects.py create mode 100644 labeler/infra/renderer.py create mode 100644 labeler/interfaces.py create mode 100644 tests/conftest.py create mode 100644 tests/fixtures.py create mode 100644 tests/labeler/domain/test_objects.py create mode 100644 tests/labeler/infra/test_images/multiline_label_no_fixed_width.png create mode 100644 tests/labeler/infra/test_images/multiline_label_test.png create mode 100644 tests/labeler/infra/test_images/no_fixed_width.png create mode 100644 tests/labeler/infra/test_images/simple_label_test.png create mode 100644 tests/labeler/infra/test_renderer.py diff --git a/labeler/adapter/fastapi.py b/labeler/adapter/fastapi.py new file mode 100644 index 0000000..e69de29 diff --git a/labeler/app/labeler.py b/labeler/app/labeler.py new file mode 100644 index 0000000..a4ecc72 --- /dev/null +++ b/labeler/app/labeler.py @@ -0,0 +1,25 @@ +from labeler.domain.objects import Label, LabelRequest, LabelDefinition +from labeler.interfaces import Renderer, Printer + + +class Application: + def __init__(self, renderer: Renderer, printer: Printer): + self.renderer = renderer + self.printer = printer + + def render_preview(self, label_request: LabelRequest): + media = self.printer.get_installed_media() + + if label_request.length is not None: + label_length = label_request.length - 2 * media.minimal_margin_horizontal + else: + label_length = media.printable_length + + label_definition = LabelDefinition( + text=label_request.text, + length=label_length, + width=media.printable_width, + dpi=media.dpi, + ) + + self.renderer.render_label(label_definition) diff --git a/labeler/domain/objects.py b/labeler/domain/objects.py new file mode 100644 index 0000000..950efba --- /dev/null +++ b/labeler/domain/objects.py @@ -0,0 +1,123 @@ +import io +from math import inf + +from pydantic import BaseModel, Field + + +class Image(BaseModel): + bytes: bytes + width: int + height: int + + @classmethod + def from_pil(cls, pil_image): + buffer = io.BytesIO() + pil_image.save(buffer, format="PNG") + buffer.seek(0) + + return cls( + bytes=buffer.read(), + width=pil_image.width, + height=pil_image.height, + ) + + +class Dimension(BaseModel): + mm: float + __EPSILON = 0.0001 + + @classmethod + def from_inch(cls, inch: float) -> "Dimension": + return cls(mm=inch * 25.4) + + @classmethod + def from_points(cls, points: float, dpi: int) -> "Dimension": + return cls.from_inch(points / dpi) + + @property + def inch(self) -> float: + return self.mm / 25.4 + + def in_pixels(self, dpi: int) -> int: + return int(self.inch * dpi) + + def __ensure_same_type(self, other): + if type(other) != Dimension: + raise TypeError(f"Cannot use {other} to {self}") + + def __add__(self, other): + self.__ensure_same_type(other) + return Dimension(mm=self.mm + other.mm) + + def __sub__(self, other): + self.__ensure_same_type(other) + return Dimension(mm=self.mm - other.mm) + + def __mul__(self, other): + if type(other) not in (int, float): + raise TypeError(f"Cannot multiply {self} by {other}") + return Dimension(mm=self.mm * other) + + def __rmul__(self, other): + return self.__mul__(other) + + def __truediv__(self, other): + if type(other) not in (int, float): + raise TypeError(f"Cannot divide {self} by {other}") + + return Dimension(mm=self.mm / other) + + def __eq__(self, other): + self.__ensure_same_type(other) + if self.mm == inf and other.mm == inf: + return True + return abs(self.mm - other.mm) < self.__EPSILON + + def __lt__(self, other): + self.__ensure_same_type(other) + return self.mm < other.mm + + def __gt__(self, other): + self.__ensure_same_type(other) + return self.mm > other.mm + + +class LabelRequest(BaseModel): + text: str + length: Dimension | None + + +class LabelDefinition(BaseModel): + text: str + length: Dimension | None = None + width: Dimension + dpi: int + + @property + def pixel_width(self): + return self.width.in_pixels(self.dpi) + + @property + def pixel_length(self): + return self.length.in_pixels(self.dpi) + + +class MediaDefinition(BaseModel): + width: Dimension + length: Dimension + minimal_margin_vertical: Dimension + minimal_margin_horizontal: Dimension + dpi: int + + @property + def printable_width(self) -> Dimension: + return self.width - 2 * self.minimal_margin_horizontal + + @property + def printable_length(self) -> Dimension: + return self.length - 2 * self.minimal_margin_vertical + + +class Label(BaseModel): + dpi: str + image: Image diff --git a/labeler/infra/renderer.py b/labeler/infra/renderer.py new file mode 100644 index 0000000..59a3bcc --- /dev/null +++ b/labeler/infra/renderer.py @@ -0,0 +1,125 @@ +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 + + +class PILRenderer(Renderer): + def __init__(self): + self.font_path = "/Library/Fonts/Arial.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.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 __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 diff --git a/labeler/interfaces.py b/labeler/interfaces.py new file mode 100644 index 0000000..fdbc9d5 --- /dev/null +++ b/labeler/interfaces.py @@ -0,0 +1,15 @@ +import abc + +from labeler.domain.objects import LabelDefinition, Image, MediaDefinition + + +class Renderer(abc.ABC): + @abc.abstractmethod + def render_label(self, label_definition: LabelDefinition) -> Image: + pass + + +class Printer(abc.ABC): + @abc.abstractmethod + def get_installed_media(self) -> MediaDefinition: + pass diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..84fb7c4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +from fixtures import * # noqa diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..5167dfc --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,25 @@ +import os +from typing import Callable + +import pytest + + +@pytest.fixture +def current_dir(request) -> str: + return os.path.dirname(request.module.__file__) + + +@pytest.fixture +def get_test_image(current_dir) -> Callable[[str], bytes]: + def f(name): + return open(os.path.join(current_dir, "test_images", name), "rb").read() + + return f + + +@pytest.fixture +def save_test_image(current_dir) -> Callable[[str, bytes], None]: + def f(name: str, data: bytes): + open(os.path.join(current_dir, "test_images", name), "wb").write(data) + + return f diff --git a/tests/labeler/domain/test_objects.py b/tests/labeler/domain/test_objects.py new file mode 100644 index 0000000..fc20f24 --- /dev/null +++ b/tests/labeler/domain/test_objects.py @@ -0,0 +1,29 @@ +import math + +from labeler.domain.objects import Dimension, MediaDefinition + + +def test_dimension(): + dimension = Dimension(mm=25.4) + + assert dimension.mm == 25.4 + assert dimension.inch == 1.0 + + assert dimension.in_pixels(dpi=300) == 300 + + assert dimension + Dimension(mm=10) == Dimension(mm=35.4) + assert dimension * 2 == Dimension(mm=50.8) + + assert dimension / 2 == Dimension(mm=12.7) + + +def test_infinite_media(): + media = MediaDefinition( + width=Dimension(mm=12), + length=Dimension(mm=math.inf), + minimal_margin_vertical=Dimension(mm=1), + minimal_margin_horizontal=Dimension(mm=2), + dpi=300, + ) + + assert media.printable_length == Dimension(mm=math.inf) diff --git a/tests/labeler/infra/test_images/multiline_label_no_fixed_width.png b/tests/labeler/infra/test_images/multiline_label_no_fixed_width.png new file mode 100644 index 0000000000000000000000000000000000000000..60a35eddbf8d17b7c5c96ed1e7c9cf6591b97906 GIT binary patch literal 1513 zcmeAS@N?(olHy`uVBq!ia0y~yVDw{PV0gpGz`(#@++8%4fq`{{r;B4q#hkY@&*wcc z6Kb~)YEn&T_HdUM+aP!FxGhVDZP)yi**o7B${)<%#519}zphDV^NEA*RR-)|8%`eJw5|u0mx*GfUFnl;M zW22IiQlFb=gWEDalMRB8R}?+>b8&Hc_KM>PH;ar#(z_qRj5>m9mo9V2=}fY|+{w@& z#D3yRtCW=CPG%Et#ui?OsmI!nu?8?QU)Wi&Dm?pOZSmxJ_wF&7hgKEUS1($#Vh4Nm z`X1Jb70O#OCARgc$C&CGybE2?IPW6UD}%zl|8LLc_jtp0Z%d0ulXPN115={q!IQDw zY^=47>mR*ckQ`ljhBfj*!V%kKv22eEE6;z=RqKByyY7|8>hp>3Pn5R)+je1PPn1J+ z|C0h%rijma;jHfZfA%oE-^w$$DTD%7${s(hZdxcvaVxioFdy_~D~)8oQ1CRHj7x%jel<6ld)1duw&&$@B)Rb6ePC zi+8+Je|dc&v*}MJUjEk?qN7$k5Odg@uA5bLAT4j37T4@=OomEI!Kz9^Quj|*a=*IM z_W6DnU!Cu_^%w4QMim^I`t9kQg4{3i>(;CnlsfMjAJ$+m`K5iKgN{pM-=eEKi%tiA z;4~EHGMM=N>3L_{_9G991Gf8A-ftI6i*GJU=l&fi`KusaLG0_QB^%C8{krO#>rqyw zKeLJ&_lrN%Ivpjk=aZU=YSPT?=lV~x;=Hd+P34{LMW!!JZj6hYCUQOW+JT6!!$k*d>UW5o%C67+W+>J@d1}MLfcI-yed-mr=pMUv zYG3FIK5w-SjoTv6en}M{(*DA=*J=HQ{TyF@|GRPb>Tmf#hTYR*x&Pc@Hd=Tj_eiNx zY0}NE2s5R5)||b;NmDx(sASBW7JA}@a*xenWBmh*6OI}vvmH4wcen8M&i6_tLJI{M zRTnnu=A<<3USRlO^OnEr94;IjKY#IC#2qO1NWbG^XUr+k*`RpHpMg)UNbZ}t8@|T6)Tlnw+wd_b zRz0Vg%cr*`>(!BvM`mTpWslz->3Dsll<{LnK8s+}yr>290v@ zfd%(}InLK9yyo_0do_Q@lFi2KA;);P2+xWN7S2A9@pO{bnUBfa9$QPCFB01OL$zM- ztG5HM<#Y?4tN&B-JNP62K5W@|^?>@N@Lc81LArZ+-h|JIstC<;VE*r&+lFYsqjYr5Fp*(LO%`1FdYG366i%Fny~$^3`ap|fW) zZZ*!*V)(;+CUe#8${D2|jsjw?mBD%!z8?A3^TPY{;^4hT5ghFyyG8qc#G8OO}^C&jK` z5iO?nMseJ=fC=V-HNjjDr%rI+|$+1Wt~$(69C?s!eRgb literal 0 HcmV?d00001 diff --git a/tests/labeler/infra/test_images/multiline_label_test.png b/tests/labeler/infra/test_images/multiline_label_test.png new file mode 100644 index 0000000000000000000000000000000000000000..d7a81e250d0bb1ffaec1fbe9c3ba6e28d81e569b GIT binary patch literal 1773 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 literal 0 HcmV?d00001 diff --git a/tests/labeler/infra/test_images/simple_label_test.png b/tests/labeler/infra/test_images/simple_label_test.png new file mode 100644 index 0000000000000000000000000000000000000000..46caf968844a48c30e7206d5ed26add140b6c11d GIT binary patch literal 1676 zcmeAS@N?(olHy`uVBq!ia0y~yVBWyM!0?8Vfq{V`HL1Lgfq^a9)5S5QV$Rz+wt25D z1djjT)NA3|u%bj0g}*jy zoIg;vj02%iPy6VP)7!RtFPWM1sCcX0nZy6$dUfFX>&<4yEHfMBtKJ)1FW4~Mc~RJbX5;wmyA1Y8_J0E=aCN_BjA3Uvu-l|lciS(92UQ2Y zF>KSDm%Y~X!vbrAjTviAZ=ZP2@K5@RIl~Wiv#OtfXDrP3>UgqRP^`G zlUO`!R>o0LnWJ`04$fB_5|(_7Y`!RL`toLzn$o`P#_a3c-H!6Ujn01Z>)MLwTias; zK2+SFm3A}u>XxJs;XOg7Sr_zvEIE|O`@td9rT(c5)2#5t_Wg2)Ke=yI+rz)yuJM&y zoW}8AVMp9p1!a$KyW+~cN_g?CW7k^L%$CWmEVwc2xar1Nhw_=ZGp-B&GF)T4S+C}? z(Dc5`7jNvk@WkPBU&3OykFm}%R`J{XV^^g7uvluzDV?}S{n4euoPe1B8;#RtUa#8T z&ox=_=HayxIgv_lJ#W}HWJmp5<{q+`xgdN?Vv>~WYw=Yfms(e|E+~C$B)y=Z^Y9}M zhu3?3a~K~uZvXb~m6&_NYSTY&Kc0AWgDLvKxs|*HjBf<`PnZ@6s;m<&>s)>J#1?7E zAnV6M26bz_Y-1Q7EIc(eWhHZ%LmH~5F86);C2wK5uASxly0>ieTko<{Kfo z!tB$f8!~lLg>rQgm&BDNMAflK7xI0}66WnaB+Xan6|gdgxoiD99cB^)V}E+qx3h@O`K1HkIEBICXi$j8~HbS4yV) zrOGW+o2RFh6=XDLi`HA0jLA3pzIh7AXdi2oJ3nj2Y2mH9nJ+U+pI9vCZlAkx{=xg# zK5aO%v}m3Al7lx-9#q>jap{dR8-cJZHV-7qsv^&u+jjNFOq=s`=I_YkU6WVdn|FNH z5i{fN^PA+4t2r+`^>KFN)6FjudXHWDarscroAj;(XT|c5?}RsIWoaA*r%uXfBuBdTd!8uygPc==*D@G zgLRzS`r5s-_uX*3-T%h*-0>jE>WF!jPUUQ7caN+&mpr%aug}8`fy`yky6+yeO?k(( z>_%3L-_sXnpJH-Uw05jJpDcUO&Gg~<*d2TJ?B@>7k$hNT`Fa1+M_Jx6*VfI-5xLuR zBXD}x&U3RHGgqFj@cWV+s8BCnq!jUZla!F#eMOzdyGQg&C0tc}pY<6<-@dksrQy=n z%ELFGcy)c%%(}E~vstCJYfkR&2e~~gMb93+J zyH&Y)bB?`dS57;0>)YMx=H}4TN0yyV`gG$K&wTay9~f2C z)q8zsR~x5nm};qcLt^cQ7PY+d#}yB`ZeZB;fM*4hTyK=RS>y@99L_y#>?YX{j;`pM zAvycZ-FUWY&YBeu-(;}vVb0No=(hOwD#`N+!?QCwcMK5y7o+X zX6R8Nk+U%j(Lz@LQow>zzt zmtPd?aop|9oOGLYb4am72q zEhm(2Y*pHNyQ)mXnxXz#($D+MrC-#=bvN|&8LZ#hC;XhFe*c~@SLqezT=h%cWdEI5 z_vQ%y4fT|N`qz|yy8RAn!cwk>ZD{(-b@KLftLS|zw*6<8^82%Bzly^eP&MP}>gTe~ HDWM4f(F`j$ literal 0 HcmV?d00001 diff --git a/tests/labeler/infra/test_renderer.py b/tests/labeler/infra/test_renderer.py new file mode 100644 index 0000000..f3597bc --- /dev/null +++ b/tests/labeler/infra/test_renderer.py @@ -0,0 +1,50 @@ +from labeler.domain.objects import LabelDefinition, Dimension +from labeler.infra.renderer import PILRenderer + + +def test_simple_label(get_test_image): + renderer = PILRenderer() + expected_label = get_test_image("simple_label_test.png") + + definition = LabelDefinition( + text="dolphin", length=Dimension(mm=40), width=Dimension(mm=10), dpi=600 + ) + + label = renderer.render_label(definition) + + assert label.bytes == expected_label + + +def test_multiline_label(get_test_image): + label_text = "dolphin\nis\nawesome" + expected_label = get_test_image("multiline_label_test.png") + + renderer = PILRenderer() + definition = LabelDefinition( + text=label_text, length=Dimension(mm=40), width=Dimension(mm=10), dpi=600 + ) + + label = renderer.render_label(definition) + assert label.bytes == expected_label + + +def test_simple_label_no_fixed_width(get_test_image, save_test_image): + renderer = PILRenderer() + expected_label = get_test_image("no_fixed_width.png") + + definition = LabelDefinition(text="dolphin", width=Dimension(mm=10), dpi=600) + + label = renderer.render_label(definition) + assert label.bytes == expected_label + + +def test_multiline_label_no_fixed_width(get_test_image, save_test_image): + renderer = PILRenderer() + expected_label = get_test_image("multiline_no_fixed_width.png") + + definition = LabelDefinition( + text="dolphin\nis\nawesome", width=Dimension(mm=10), dpi=600 + ) + + label = renderer.render_label(definition) + assert label.bytes == expected_label