Files
cmbot/tests/test_composer.py
T

212 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for core.composer — no GUI dependency."""
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
from PIL import Image
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from core.composer import compose
from core.models import ExportOptions, TransformState
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
def _rgba(w, h, color=(200, 200, 200, 255)):
return Image.new("RGBA", (w, h), color)
class _TmpDir:
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
def tearDown(self):
shutil.rmtree(str(self.tmp), ignore_errors=True)
def _save(self, img: Image.Image, name: str) -> Path:
p = self.tmp / name
img.save(str(p))
return p
def _compose(self, garment, print_img, state, fmt="PNG", quality=90):
ext = ".jpg" if fmt.upper() in ("JPG", "JPEG") else ".png"
g = self._save(garment, "garment.png") # garment always PNG
p = self._save(print_img, "print.png")
out = self.tmp / ("out" + ext)
opts = ExportOptions(output_format=fmt, quality=quality)
return compose(g, p, state, opts, out), out
# ---------------------------------------------------------------------------
# Transparency tests
# ---------------------------------------------------------------------------
class TestTransparency(_TmpDir, unittest.TestCase):
def test_fully_transparent_print_leaves_garment_unchanged(self):
"""An all-alpha-0 print must not modify the garment pixels."""
garment = _rgba(100, 100, (200, 200, 200, 255))
print_img = Image.new("RGBA", (40, 40), (255, 0, 0, 0)) # fully transparent
state = TransformState(x=30, y=30, width=40, height=40)
result, out_path = self._compose(garment, print_img, state)
self.assertTrue(result.success, result.error)
out = Image.open(str(out_path)).convert("RGBA")
r, g, b, _ = out.getpixel((50, 50))
self.assertEqual((r, g, b), (200, 200, 200))
def test_opaque_print_overwrites_garment_pixels(self):
"""Fully opaque blue print should cover garment in the print area."""
garment = _rgba(100, 100, (200, 200, 200, 255))
print_img = _rgba(20, 20, (0, 0, 255, 255))
state = TransformState(x=40, y=40, width=20, height=20)
result, out_path = self._compose(garment, print_img, state)
self.assertTrue(result.success, result.error)
out = Image.open(str(out_path)).convert("RGBA")
r, g, b, _ = out.getpixel((50, 50)) # center of print area
self.assertEqual((r, g, b), (0, 0, 255))
# ---------------------------------------------------------------------------
# Scaling tests
# ---------------------------------------------------------------------------
class TestScaling(_TmpDir, unittest.TestCase):
def test_print_is_scaled_to_state_dimensions(self):
"""A 10×10 print scaled to 60×80 should fill that region."""
garment = _rgba(200, 200)
print_img = _rgba(10, 10, (255, 0, 0, 255))
state = TransformState(x=70, y=60, width=60, height=80)
result, out_path = self._compose(garment, print_img, state)
self.assertTrue(result.success, result.error)
out = Image.open(str(out_path)).convert("RGBA")
# Inside print area → red
r, g, b, _ = out.getpixel((100, 100))
self.assertEqual((r, g, b), (255, 0, 0))
# Outside print area → garment gray
r2, g2, b2, _ = out.getpixel((10, 10))
self.assertEqual((r2, g2, b2), (200, 200, 200))
def test_output_dimensions_match_garment(self):
"""Output size must always equal the garment size, regardless of print size."""
gw, gh = 320, 480
garment = _rgba(gw, gh)
print_img = _rgba(10, 10, (0, 255, 0, 255))
state = TransformState(x=100, y=100, width=80, height=80)
result, out_path = self._compose(garment, print_img, state)
self.assertTrue(result.success, result.error)
out = Image.open(str(out_path))
self.assertEqual(out.size, (gw, gh))
# ---------------------------------------------------------------------------
# Rotation centre tests
# ---------------------------------------------------------------------------
class TestRotation(_TmpDir, unittest.TestCase):
def test_rotation_centre_fixed_for_square(self):
"""After 90° CW rotation of a square print, its centre stays at (cx, cy)."""
garment = _rgba(200, 200)
print_img = _rgba(20, 20, (255, 0, 0, 255))
# Centre = (90+10, 90+10) = (100, 100)
state = TransformState(x=90, y=90, width=20, height=20, rotation=90)
result, out_path = self._compose(garment, print_img, state)
self.assertTrue(result.success, result.error)
out = Image.open(str(out_path)).convert("RGBA")
r, g, b, _ = out.getpixel((100, 100))
self.assertEqual((r, g, b), (255, 0, 0), "Centre pixel must be from the print")
def test_zero_rotation_same_as_no_rotation(self):
"""rotation=0 should produce the same result as not specifying rotation."""
garment = _rgba(100, 100)
print_img = _rgba(20, 20, (0, 0, 255, 255))
state = TransformState(x=40, y=40, width=20, height=20, rotation=0)
result, out_path = self._compose(garment, print_img, state)
self.assertTrue(result.success, result.error)
out = Image.open(str(out_path)).convert("RGBA")
r, g, b, _ = out.getpixel((50, 50))
self.assertEqual((r, g, b), (0, 0, 255))
# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------
class TestEdgeCases(_TmpDir, unittest.TestCase):
def test_print_partially_outside_canvas_no_error(self):
"""Print whose bounding box extends beyond the canvas should not raise."""
garment = _rgba(50, 50)
print_img = _rgba(20, 20, (255, 0, 0, 255))
state = TransformState(x=45, y=45, width=20, height=20)
result, _ = self._compose(garment, print_img, state)
self.assertTrue(result.success, result.error)
def test_invalid_zero_dimensions_returns_failure(self):
"""width=0 or height=0 must return a ComposeResult with success=False."""
garment = _rgba(100, 100)
print_img = _rgba(10, 10)
g = self._save(garment, "g.png")
p = self._save(print_img, "p.png")
out = self.tmp / "out.png"
state = TransformState(x=0, y=0, width=0, height=0)
result = compose(g, p, state, ExportOptions(), out)
self.assertFalse(result.success)
self.assertTrue(result.error)
def test_missing_garment_file_returns_failure(self):
"""Non-existent garment path must return failure without raising."""
p = self._save(_rgba(10, 10), "p.png")
out = self.tmp / "out.png"
state = TransformState(x=0, y=0, width=10, height=10)
result = compose(self.tmp / "no_such_file.png", p, state, ExportOptions(), out)
self.assertFalse(result.success)
# ---------------------------------------------------------------------------
# JPG output
# ---------------------------------------------------------------------------
class TestJpgOutput(_TmpDir, unittest.TestCase):
def test_jpg_export_creates_file(self):
garment = _rgba(100, 100)
print_img = _rgba(30, 30, (0, 200, 0, 255))
state = TransformState(x=35, y=35, width=30, height=30)
result, out_path = self._compose(garment, print_img, state, fmt="JPG")
self.assertTrue(result.success, result.error)
self.assertTrue(out_path.exists())
def test_jpg_export_no_alpha_error(self):
"""JPG export must not raise due to alpha channel handling."""
garment = _rgba(80, 80, (255, 255, 255, 255))
print_img = Image.new("RGBA", (20, 20), (255, 0, 0, 128)) # semi-transparent
state = TransformState(x=30, y=30, width=20, height=20)
result, _ = self._compose(garment, print_img, state, fmt="JPG")
self.assertTrue(result.success, result.error)
if __name__ == "__main__":
unittest.main()