diff --git a/brother_ql_web/labels.py b/brother_ql_web/labels.py index d41a996..d420bca 100644 --- a/brother_ql_web/labels.py +++ b/brother_ql_web/labels.py @@ -33,6 +33,8 @@ class LabelParameters: font_family: str | None = None font_style: str | None = None text: str = "" + image: bytes | None = None + pdf: bytes | None = None font_size: int = 100 label_size: str = "62" margin: int = 10 @@ -169,6 +171,9 @@ def _determine_text_offsets( def create_label_image(parameters: LabelParameters) -> Image.Image: + if parameters.image: + return Image.open(BytesIO(parameters.image)) + image_font = ImageFont.truetype(parameters.font_path, parameters.font_size) # Workaround for a bug in multiline_textsize() diff --git a/brother_ql_web/web.py b/brother_ql_web/web.py index 5b167c2..35fb374 100644 --- a/brother_ql_web/web.py +++ b/brother_ql_web/web.py @@ -1,10 +1,13 @@ from __future__ import annotations import logging +from io import BytesIO from pathlib import Path from typing import Any, cast, Dict # TODO: Remove `Dict` after dropping Python 3.8. import bottle +from brother_ql import BrotherQLRaster + from brother_ql_web.configuration import Configuration from brother_ql_web.labels import ( LabelParameters, @@ -52,7 +55,18 @@ def labeldesigner() -> dict[str, Any]: } -def get_label_parameters(request: bottle.BaseRequest) -> LabelParameters: +def _save_to_bytes(upload: bottle.FileUpload | None) -> bytes | None: + if upload is None: + return None + output = BytesIO() + upload.save(output) + output.seek(0) + return output.getvalue() + + +def get_label_parameters( + request: bottle.BaseRequest, should_be_file: bool = False +) -> LabelParameters: # As we have strings, *bottle* would try to generate Latin-1 bytes from it # before decoding it back to UTF-8. This seems to break some umlauts, thus # resulting in UnicodeEncodeErrors being raised when going back to UTF-8. @@ -66,10 +80,19 @@ def get_label_parameters(request: bottle.BaseRequest) -> LabelParameters: parameters.recode_unicode = False d = parameters.decode() # UTF-8 decoded form data - font_family = d.get("font_family").rpartition("(")[0].strip() - font_style = d.get("font_family").rpartition("(")[2].rstrip(")") + try: + font_family = d.get("font_family").rpartition("(")[0].strip() + font_style = d.get("font_family").rpartition("(")[2].rstrip(")") + except AttributeError: + if should_be_file: + font_family = "" + font_style = "" + else: + raise context = { "text": d.get("text", ""), + "image": _save_to_bytes(request.files.get("image")), + "pdf": _save_to_bytes(request.files.get("pdf")), "font_size": int(d.get("font_size", 100)), "font_family": font_family, "font_style": font_style, @@ -110,7 +133,7 @@ def get_preview_image() -> bytes: @bottle.get("/api/print/text") # type: ignore[misc] def print_text() -> dict[str, bool | str]: """ - API to print a label + API to print some text returns: JSON """ @@ -132,6 +155,39 @@ def print_text() -> dict[str, bool | str]: save_image_to="sample-out.png" if bottle.DEBUG else None, ) + return _print(parameters=parameters, qlr=qlr) + + +@bottle.post("/api/print/image") # type: ignore[misc] +def print_image() -> dict[str, bool | str]: + """ + API to print an image + + returns: JSON + """ + return_dict: dict[str, bool | str] = {"success": False} + + try: + parameters = get_label_parameters(bottle.request, should_be_file=True) + except (AttributeError, LookupError) as e: + return_dict["error"] = str(e) + return return_dict + + if parameters.image is None or not parameters.image: + return_dict["error"] = "Please provide the label image" + return return_dict + + qlr = generate_label( + parameters=parameters, + configuration=cast(Configuration, get_config("brother_ql_web.configuration")), + ) + + return _print(parameters=parameters, qlr=qlr) + + +def _print(parameters: LabelParameters, qlr: BrotherQLRaster) -> dict[str, bool | str]: + return_dict: dict[str, bool | str] = {"success": False} + if not bottle.DEBUG: try: print_label( diff --git a/pyproject.toml b/pyproject.toml index 7693907..6e6280c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,3 +6,4 @@ requires = [ [tool.mypy] mypy_path = '$MYPY_CONFIG_FILE_DIR/stubs' strict = true +files = "brother_ql_web,tests" diff --git a/stubs/bottle.pyi b/stubs/bottle.pyi index a5bb118..63e7e95 100644 --- a/stubs/bottle.pyi +++ b/stubs/bottle.pyi @@ -201,7 +201,8 @@ class BaseRequest: def forms(self): ... @property def params(self) -> FormsDict: ... - def files(self): ... + @property + def files(self) -> FormsDict: ... def json(self): ... @property def body(self): ... diff --git a/tests/test_web.py b/tests/test_web.py index 7f3ab0d..3244d0c 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -283,5 +283,53 @@ def test_regular_mode(self) -> None: self.assertEqual(expected, fd.read()) +class PrintImageTestCase(TestCase): + def test_error__empty_image(self) -> None: + self.run_server() + response = requests.post( + url="http://localhost:8013/api/print/image", files={"image": b""} + ) + self.assertEqual(200, response.status_code) + self.assertEqual( + b'{"success": false, "error": "Please provide the label image"}', + response.content, + ) + with open(cast(str, self.printer_file), mode="rb") as fd: + self.assertEqual(b"", fd.read()) + + def test_error__no_image(self) -> None: + self.run_server() + response = requests.post(url="http://localhost:8013/api/print/image") + self.assertEqual(200, response.status_code) + self.assertEqual( + b'{"success": false, "error": "Please provide the label image"}', + response.content, + ) + with open(cast(str, self.printer_file), mode="rb") as fd: + self.assertEqual(b"", fd.read()) + + def test_print_image(self) -> None: + image = files("tests") / "data" / "hello_world.png" + reference = ( + files("tests") / "data" / "hello_world__label_size_62__standard.data" + ) + with as_file(image) as path: + image_data = path.read_bytes() + expected = reference.read_bytes() + + self.run_server() + response = requests.post( + url=( + "http://localhost:8013/api/print/image?" + "label_size=62&orientation=standard" + ), + files={"image": image_data}, + ) + self.assertEqual(200, response.status_code) + self.assertEqual(b'{"success": true}', response.content) + with open(cast(str, self.printer_file), mode="rb") as fd: + self.assertEqual(expected, fd.read()) + + class MainTestCase(TestCase): pass