"""End-to-end export checks; run with uv run python -m unittest discover -s tests.""" import io import re from pathlib import Path import subprocess import sys import tempfile import unittest import xml.etree.ElementTree as ET from PIL import Image from fontTools.pens.boundsPen import BoundsPen from fontTools.svgLib.path import parse_path import resvg_py from cmatrix_avatar import BLUE, Cell, CMatrixAvatar, export, find_font, make_svg from avatar_color import sdr_color class AvatarExportTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.font = find_font() def test_svg_is_portable_and_reproducible(self): svg = make_svg(CMatrixAvatar(42), self.font) self.assertEqual(svg, make_svg(CMatrixAvatar(42), self.font)) self.assertNotEqual(svg, make_svg(CMatrixAvatar(43), self.font)) root = ET.fromstring(svg) ns = "{http://www.w3.org/2000/svg}" self.assertFalse(list(root.iter(ns + "text"))) self.assertFalse(list(root.iter(ns + "image"))) ids = {node.get("id") for node in root.iter() if node.get("id")} for node in root.iter(ns + "use"): self.assertIn(node.get("href")[1:], ids) self.assertTrue(list(root.iter(ns + "path"))) def test_png_matches_svg_and_logo_geometry(self): with tempfile.TemporaryDirectory() as directory: svg_path, png_path = export(CMatrixAvatar(42), Path(directory) / "nested/gwb", self.font, 1024, previews=True) with Image.open(png_path) as image: self.assertEqual(image.size, (1024, 1024)) rgba = image.convert("RGBA") self.assertEqual(rgba.getchannel("A").getextrema(), (255, 255)) raster = resvg_py.svg_to_bytes(svg_string=svg_path.read_text(encoding="utf-8")) with Image.open(io.BytesIO(raster)) as rendered: self.assertEqual(rgba.tobytes(), rendered.convert("RGBA").tobytes()) blue = tuple(bytes.fromhex(sdr_color(BLUE)[1:])) mask = Image.new("L", image.size) pixels = rgba.tobytes() mask.putdata([255 if pixels[i:i + 3] == bytes(blue) else 0 for i in range(0, len(pixels), 4)]) x0, y0, x1, y1 = mask.getbbox() self.assertTrue(0.70 < (x1 - x0) / 1024 < 0.75) self.assertAlmostEqual((x0 + x1) / 2, 512, delta=16) self.assertAlmostEqual((y0 + y1) / 2, 512, delta=16) for size in (128, 64): with Image.open(png_path.with_name(f"gwb-{size}.png")) as preview: self.assertEqual(preview.size, (size, size)) def test_cli_custom_size_and_svg_output_path(self): script = Path(__file__).resolve().parents[1] / "cmatrix_avatar.py" with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "portrait.svg" result = subprocess.run([sys.executable, str(script), "--seed", "7", "--size", "256", "--output", str(output)], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) self.assertTrue(output.exists()) with Image.open(output.with_suffix(".png")) as image: self.assertEqual(image.size, (256, 256)) def test_cli_default_resolution(self): script = Path(__file__).resolve().parents[1] / "cmatrix_avatar.py" with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "default.png" result = subprocess.run([sys.executable, str(script), "--seed", "42", "--output", str(output)], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) with Image.open(output) as image: self.assertEqual(image.size, (2048, 2048)) def test_explicit_logo_font_without_box_glyphs_is_rejected(self): ocr = Path("C:/Windows/Fonts/OCRAEXT.TTF") if not ocr.exists(): self.skipTest("OCR A Extended is not installed") # Automatic fallback succeeds (covered by export tests); explicitly # choosing an incomplete logo font must never silently drop characters. with self.assertRaisesRegex(ValueError, "missing characters"): make_svg(CMatrixAvatar(42), ocr, logo_font_path=ocr) def test_cli_rejects_invalid_options(self): script = Path(__file__).resolve().parents[1] / "cmatrix_avatar.py" for arguments in (("--size", "0"), ("--rows", "7"), ("--columns", "20"), ("--output", "avatar.jpg")): with self.subTest(arguments=arguments): result = subprocess.run([sys.executable, str(script), *arguments], capture_output=True, text=True) self.assertEqual(result.returncode, 2) self.assertNotIn("Traceback", result.stderr) def test_complete_grid_and_edge_glyphs_are_inside_canvas(self): ns = "{http://www.w3.org/2000/svg}" for rows, columns in ((43, 77), (20, 35), (28, 67)): with self.subTest(rows=rows, columns=columns): avatar = CMatrixAvatar(42, rows, columns) # Color every border cell separately: the whole border must # survive, while only central interior cells may be reserved. avatar.cells = [[Cell("l", "#ffff00" if r in (0, rows - 1) or c in (0, columns - 1) else "#00ff00") for c in range(columns)] for r in range(rows)] root = ET.fromstring(make_svg(avatar, self.font)) self.assertFalse(list(root.iter(ns + "clipPath"))) side = float(root.get("viewBox").split()[2]) group = root.find(ns + "g") dx, dy = map(float, re.findall(r"[-\d.]+", group.get("transform"))) border = next(p for p in group if p.get("data-base") == "#ffff00") self.assertEqual(border.get("d").count("M"), 2 * rows + 2 * columns - 4) for path in group: bounds = BoundsPen(None) parse_path(path.get("d"), bounds) x0, y0, x1, y1 = bounds.bounds self.assertGreater(x0 + dx, 0) self.assertGreater(y0 + dy, 0) self.assertLess(x1 + dx, side) self.assertLess(y1 + dy, side) # Interior fill must leave the ENTIRE logo rectangle empty. rendered = resvg_py.svg_to_bytes(svg_string=ET.tostring(root, encoding="unicode")) with Image.open(io.BytesIO(rendered)) as image: box = image.crop((round(2048 * .14) + 2, round(2048 * .37), round(2048 * .86) - 2, round(2048 * .63))).convert("RGB") colors = box.getdata() if not hasattr(box, "get_flattened_data") else box.get_flattened_data() self.assertFalse(any(g > r + 20 and g > b + 20 for r, g, b in colors)) def test_logo_ratio_is_independent_of_background_grid(self): ns = "{http://www.w3.org/2000/svg}" for rows, columns in ((20, 35), (43, 77), (60, 107)): root = ET.fromstring(make_svg(CMatrixAvatar(42, rows, columns), self.font)) side = float(root.get("viewBox").split()[2]) logo = root.find(f"{ns}path[@id='logo']") bounds = BoundsPen(None) parse_path(logo.get("d"), bounds) x0, y0, x1, y1 = bounds.bounds x, y, scale, ox, oy = map(float, re.findall(r"[-\d.]+", logo.get("transform"))) self.assertAlmostEqual((x1 - x0) * scale / side, .72) self.assertAlmostEqual(x + (x1 - x0) * scale / 2, side / 2) self.assertAlmostEqual(y + (y1 - y0) * scale / 2, side / 2) if __name__ == "__main__": unittest.main()