Files
bambuddy/backend/tests/unit/services/test_label_renderer_fields.py
T

198 lines
6.9 KiB
Python

"""Choosing what a spool label shows, and PNG output (#2981).
Text is checked in an uncompressed PDF, where ReportLab writes each string as
it was drawn.
"""
from __future__ import annotations
import io
from datetime import date
from unittest.mock import patch
import pytest
from PIL import Image
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas as rl_canvas
from backend.app.services.label_renderer import (
ALL_LABEL_FIELDS,
DEFAULT_LABEL_FIELDS,
LabelData,
_draw_label,
label_size_mm,
pdf_to_pngs,
render_label_preview_pdf,
render_labels,
)
ALL = frozenset(ALL_LABEL_FIELDS)
def _data(**overrides) -> LabelData:
values = {
"spool_id": 128,
"name": "Jade White",
"material": "PLA",
"brand": "Bambu Lab",
"subtype": "Matte",
"rgba": "E8F0E0FF",
"storage_location": "Shelf 2",
"deeplink_url": "https://example.test/inventory?spool=128",
"material_number": "MN-15",
"nozzle_temp_min": 190,
"nozzle_temp_max": 230,
"label_weight": 1000,
"note": "Dry first",
"added": date(2026, 9, 30),
}
values.update(overrides)
return LabelData(**values)
def _text(template: str, data: LabelData, fields=DEFAULT_LABEL_FIELDS) -> bytes:
w_mm, h_mm = label_size_mm(template)
buf = io.BytesIO()
c = rl_canvas.Canvas(buf, pagesize=(w_mm * mm, h_mm * mm), pageCompression=0)
_draw_label(c, 0, 0, w_mm * mm, h_mm * mm, data, False, fields)
c.showPage()
c.save()
return buf.getvalue()
def test_default_fields_are_what_labels_always_carried():
assert {"brand", "material", "hex", "name", "location", "qr", "spool_id"} == DEFAULT_LABEL_FIELDS
def test_default_fields_leave_the_new_lines_off():
pdf = _text("ams_holder_75x55", _data())
for present in (b"Bambu Lab", b"Jade White", b"Shelf 2", b"#E8F0E0", b"#128"):
assert present in pdf
for absent in (b"MN-15", b"190", b"1000 g", b"Dry first", b"2026-09-30"):
assert absent not in pdf
def test_every_field_prints_on_a_label_with_room():
pdf = _text("ams_holder_75x55", _data(), ALL)
for present in (b"MN-15", b"190", b"230", b"1000 g", b"Dry first", b"2026-09-30", b"#128"):
assert present in pdf
def test_a_single_temperature_prints_alone():
pdf = _text("ams_holder_75x55", _data(nozzle_temp_max=None), frozenset({"temps"}))
assert b"190 " in pdf
assert b"230" not in pdf
def test_unticked_lines_are_left_out():
pdf = _text("ams_holder_75x55", _data(), ALL - {"brand", "spool_id", "hex"})
assert b"Bambu Lab" not in pdf
assert b"#128" not in pdf
assert b"#E8F0E0" not in pdf
assert b"Jade White" in pdf
def test_no_qr_without_the_qr_field():
with patch("backend.app.services.label_renderer._draw_qr") as draw_qr:
render_labels("box_40x30", [_data()], fields=ALL - {"qr"})
draw_qr.assert_not_called()
with patch("backend.app.services.label_renderer._draw_qr") as draw_qr:
render_labels("box_40x30", [_data()])
draw_qr.assert_called_once()
def test_lines_that_do_not_fit_stop_above_the_spool_id():
"""A 40 x 30 label has no room for all ten lines. The ones that don't fit
are dropped rather than drawn over the spool ID at the bottom."""
drawn: list[tuple[float, str]] = []
real = rl_canvas.Canvas.drawString
def record(self, x, y, text, *args, **kwargs):
drawn.append((y, text))
return real(self, x, y, text, *args, **kwargs)
with patch.object(rl_canvas.Canvas, "drawString", record):
_text("box_40x30", _data(), ALL)
id_y = next(y for y, text in drawn if text == "#128")
other = [y for y, text in drawn if text != "#128"]
assert other, "some lines must still print"
# The ID is 16 pt tall from its baseline; every other baseline sits above it.
assert min(other) >= id_y + 16
assert "2026-09-30" not in [text for _, text in drawn]
def test_name_is_not_repeated_when_it_is_the_subtype():
"""A Spoolman filament named after its colour gives subtype == name."""
pdf = _text("ams_holder_75x55", _data(name="Matte"))
assert pdf.count(b"Matte") == 1
def test_name_prints_when_the_material_line_is_off():
pdf = _text("ams_holder_75x55", _data(name="Matte"), DEFAULT_LABEL_FIELDS - {"material"})
assert b"Matte" in pdf
def test_tight_layout_honours_the_fields():
"""The small layout only has brand, material, hex and ID; they obey the choice too."""
w, h = 40 * mm, 15 * mm
buf = io.BytesIO()
c = rl_canvas.Canvas(buf, pagesize=(w, h), pageCompression=0)
_draw_label(c, 0, 0, w, h, _data(), False, frozenset({"hex"}))
c.showPage()
c.save()
pdf = buf.getvalue()
assert b"#E8F0E0" in pdf
assert b"Bambu Lab" not in pdf
assert b"#128" not in pdf
def _assert_size(image: Image.Image, w_mm: float, h_mm: float, dpi: int) -> None:
"""PDFium rounds a fractional pixel up, so allow one either way."""
expected = (w_mm / 25.4 * dpi, h_mm / 25.4 * dpi)
assert all(abs(got - want) <= 1 for got, want in zip(image.size, expected, strict=True)), (image.size, expected)
@pytest.mark.parametrize(
("template", "size_mm"),
[("box_40x30", (40.0, 30.0)), ("avery_l7160", (63.5, 38.1)), ("avery_5160", (66.675, 25.4))],
)
def test_label_size_is_the_cell_for_a_sheet(template, size_mm):
assert label_size_mm(template) == size_mm
def test_preview_is_one_label_of_the_templates_size():
"""A sheet's preview is one cell, not a whole A4 page."""
pngs = pdf_to_pngs(render_label_preview_pdf("avery_l7160", _data(), fields=ALL), 300)
assert len(pngs) == 1
image = Image.open(io.BytesIO(pngs[0]))
_assert_size(image, 63.5, 38.1, 300)
@pytest.mark.parametrize("dpi", [203, 300, 600])
def test_png_is_printed_at_the_requested_resolution(dpi):
pngs = pdf_to_pngs(render_labels("box_40x30", [_data()]), dpi)
image = Image.open(io.BytesIO(pngs[0]))
assert image.format == "PNG"
_assert_size(image, 40, 30, dpi)
assert tuple(round(v) for v in image.info["dpi"]) == (dpi, dpi)
def test_one_png_per_page():
roll = pdf_to_pngs(render_labels("box_40x30", [_data(spool_id=i) for i in range(1, 4)]), 203)
assert len(roll) == 3
sheets = pdf_to_pngs(render_labels("avery_l7160", [_data(spool_id=i) for i in range(1, 23)]), 72)
assert len(sheets) == 2
def test_qr_stays_black_and_white_in_the_png():
"""Rasterised without image smoothing, so the QR's modules keep hard edges
instead of grey fringes a 203 dpi head can't print (#1870)."""
png = pdf_to_pngs(render_label_preview_pdf("box_40x30", _data(), fields=frozenset({"qr"})), 203)[0]
image = Image.open(io.BytesIO(png)).convert("L")
# Only the QR is drawn besides the swatch and hairline border; crop to it.
w, h = image.size
qr_region = image.crop((w // 2, 0, w, h))
greys = sum(1 for v in qr_region.getdata() if 40 < v < 215)
assert greys / (qr_region.width * qr_region.height) < 0.02