"""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()