| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- """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
|