import io import re import tempfile import unittest from pathlib import Path from typing import ClassVar from PIL import Image, ImageColor, ImageDraw from utils.design.logo import ( LAYOUT_LOGO, LAYOUT_LOGO_STACKED, LAYOUT_LOGO_TITLE, LAYOUT_LOGO_TITLE_DOMAIN, LAYOUT_TITLE, choose_layout, is_monochrome, render_assets, ) from . import PROJECT_ROOT LOGO = str(PROJECT_ROOT / "assets" / "img" / "logo.png") TEXT_RE = re.compile(r"]*)>([^<]*)") class TestChooseLayout(unittest.TestCase): def test_slot_format_picks_the_lockup(self) -> None: cases = { (512, 512, False): LAYOUT_LOGO, (320, 64, False): LAYOUT_LOGO_TITLE, (720, 64, False): LAYOUT_LOGO_TITLE_DOMAIN, (960, 160, False): LAYOUT_LOGO_STACKED, (320, 64, True): LAYOUT_TITLE, } for (width, height, text_only), expected in cases.items(): with self.subTest(width=width, height=height, text_only=text_only): self.assertEqual( choose_layout(width, height, text_only, True), expected ) def test_without_title_every_logo_slot_shows_the_logo_only(self) -> None: self.assertEqual(choose_layout(960, 160, False, False), LAYOUT_LOGO) class TestRenderAssets(unittest.TestCase): SLOTS: ClassVar[dict] = { "icon": {"width": 512, "height": 512, "text_only": False, "frame": False}, "header": {"width": 320, "height": 64, "text_only": False, "frame": False}, "banner": {"width": 960, "height": 160, "text_only": False, "frame": False}, "wordmark": {"width": 320, "height": 64, "text_only": True, "frame": False}, "topbar": {"width": 320, "height": 64, "text_only": False, "frame": True}, } COLORS: ClassVar[tuple] = ( "#001f3f", "#ffffff", "#7fb2ff", "#0b1220", "#fefefe", "#00152c", ) def test_every_slot_renders_png_and_svg_at_its_size(self) -> None: assets = render_assets( LOGO, "Gitea", "git.example.org", *self.COLORS, self.SLOTS ) for name, spec in self.SLOTS.items(): with self.subTest(slot=name): image = Image.open(io.BytesIO(assets[f"{name}.png"])) self.assertEqual(image.size, (spec["width"], spec["height"])) self.assertEqual(image.mode, "RGBA") self.assertEqual(image.getpixel((0, 0))[3], 0) svg = assets[f"{name}.svg"].decode("utf-8") self.assertIn(f'width="{spec["width"]}" height="{spec["height"]}"', svg) self.assertTrue(svg.endswith("")) def test_text_carries_fill_and_outline(self) -> None: assets = render_assets( LOGO, "Gitea", "git.example.org", *self.COLORS, self.SLOTS ) texts = TEXT_RE.findall(assets["banner.svg"].decode("utf-8")) self.assertEqual( [content for _, content in texts], ["Gitea", "git.example.org"] ) for attrs, _ in texts: self.assertIn('fill="#001f3f"', attrs) self.assertIn('stroke="#ffffff"', attrs) self.assertIn("textLength=", attrs) def test_frame_slot_takes_the_frame_colors(self) -> None: assets = render_assets( LOGO, "Gitea", "git.example.org", *self.COLORS, self.SLOTS ) texts = TEXT_RE.findall(assets["topbar.svg"].decode("utf-8")) self.assertEqual([content for _, content in texts], ["Gitea"]) for attrs, _ in texts: self.assertIn('fill="#fefefe"', attrs) self.assertIn('stroke="#00152c"', attrs) self.assertNotEqual(assets["topbar.png"], assets["header.png"]) def test_page_slots_switch_text_colors_in_dark_mode(self) -> None: assets = render_assets( LOGO, "Gitea", "git.example.org", *self.COLORS, self.SLOTS ) dark_rule = ( "@media (prefers-color-scheme: dark){text{fill:#7fb2ff;stroke:#0b1220}}" ) for name in ("header", "banner", "wordmark"): with self.subTest(slot=name): self.assertIn(dark_rule, assets[f"{name}.svg"].decode("utf-8")) for name in ("topbar", "icon"): with self.subTest(slot=name): self.assertNotIn(b" None: assets = render_assets( LOGO, "Gitea", "git.example.org", *self.COLORS, self.SLOTS ) icon = Image.open(io.BytesIO(assets["favicon.ico"])) self.assertEqual(icon.info["sizes"], {(16, 16), (32, 32), (48, 48), (64, 64)}) def test_disabled_title_drops_text_only_slots_and_text(self) -> None: assets = render_assets(LOGO, False, "git.example.org", *self.COLORS, self.SLOTS) self.assertNotIn("wordmark.png", assets) self.assertNotIn(b" str: big = Image.new("RGBA", (400, 400), (0, 0, 0, 0)) draw = ImageDraw.Draw(big) for index, ink in enumerate(inks): inset = 20 + index * 90 draw.ellipse((inset, inset, 400 - inset, 400 - inset), fill=ink) big.resize((100, 100), Image.LANCZOS).save(path) return str(path) def _solid_rgbs(image: Image.Image) -> set[tuple[int, int, int]]: raw = image.convert("RGBA").tobytes() return { tuple(raw[index : index + 3]) for index in range(0, len(raw), 4) if raw[index + 3] >= 128 } def _near(rgbs: set[tuple[int, int, int]], color: str) -> bool: target = ImageColor.getrgb(color)[:3] return all( all(abs(channel - goal) <= 6 for channel, goal in zip(rgb, target, strict=True)) for rgb in rgbs ) class TestMonochromeLogo(unittest.TestCase): SLOTS: ClassVar[dict] = { "icon": {"width": 128, "height": 128, "text_only": False, "frame": False}, "mark": {"width": 128, "height": 128, "text_only": False, "frame": True}, } COLORS = TestRenderAssets.COLORS @classmethod def setUpClass(cls) -> None: cls._tmp = tempfile.TemporaryDirectory() root = Path(cls._tmp.name) cls.mono = _disc_logo(root / "mono.png", ("#000000",)) cls.duo = _disc_logo(root / "duo.png", ("#000000", "#ff8800")) @classmethod def tearDownClass(cls) -> None: cls._tmp.cleanup() def test_one_ink_on_transparency_is_monochrome(self) -> None: self.assertTrue(is_monochrome(self.mono)) def test_two_inks_are_not_monochrome(self) -> None: self.assertFalse(is_monochrome(self.duo)) def test_the_default_logo_is_not_monochrome(self) -> None: self.assertFalse(is_monochrome(LOGO)) def test_a_monochrome_logo_takes_the_text_color_of_its_slot(self) -> None: assets = render_assets( self.mono, False, "git.example.org", *self.COLORS, self.SLOTS ) page = _solid_rgbs(Image.open(io.BytesIO(assets["icon.png"]))) frame = _solid_rgbs(Image.open(io.BytesIO(assets["mark.png"]))) self.assertTrue(page) self.assertTrue(_near(page, "#001f3f"), page) self.assertTrue(_near(frame, "#fefefe"), frame) def test_a_monochrome_page_slot_switches_its_logo_in_dark_mode(self) -> None: assets = render_assets( self.mono, False, "git.example.org", *self.COLORS, self.SLOTS ) page = assets["icon.svg"].decode("utf-8") self.assertIn('class="logo-light"', page) self.assertIn('class="logo-dark"', page) self.assertIn(".logo-light{display:none}.logo-dark{display:inline}", page) frame = assets["mark.svg"].decode("utf-8") self.assertEqual(frame.count(" None: assets = render_assets( self.mono, False, "git.example.org", *self.COLORS, self.SLOTS ) icon = Image.open(io.BytesIO(assets["favicon.ico"])) icon.size = (64, 64) self.assertTrue(_near(_solid_rgbs(icon), "#001f3f")) def test_a_colored_logo_keeps_its_colors(self) -> None: assets = render_assets( self.duo, False, "git.example.org", *self.COLORS, self.SLOTS ) page = _solid_rgbs(Image.open(io.BytesIO(assets["icon.png"]))) self.assertFalse(_near(page, "#001f3f")) self.assertEqual(assets["icon.svg"].count(b"