from __future__ import annotations import sys import unittest from pathlib import Path import numpy as np from PIL import Image, ImageDraw, ImageFont BACKEND_DIR = Path(__file__).resolve().parents[1] if str(BACKEND_DIR) not in sys.path: sys.path.insert(0, str(BACKEND_DIR)) from core import config # noqa: E402 from core.ewc import EfficientWordCloud # noqa: E402 from core.fonts import get_cached_font # noqa: E402 from core.pipeline import run_generation_pass # noqa: E402 from core.render import count_layout_overlap_pixels, largest_empty_square_size # noqa: E402 from core.weights import merge_weight_maps # noqa: E402 def chinese_names(count: int) -> list[str]: surnames = "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜" given = "子涵宇轩梓萱浩然欣怡雨桐诗涵俊杰思远若曦嘉怡明哲一诺安然沐阳" return [ surnames[index % len(surnames)] + given[(index * 3) % len(given)] + given[(index * 7 + 1) % len(given)] for index in range(count) ] def circle_mask(size: int) -> np.ndarray: image = Image.new("L", (size, size), 255) ImageDraw.Draw(image).ellipse((20, 20, size - 21, size - 21), fill=0) return np.asarray(image) class LayoutConstraintTests(unittest.TestCase): def setUp(self) -> None: self.saved = { key: getattr(config, key) for key in ( "SIZE_RATIO", "WORK_SCALE", "MIN_READABLE_HEIGHT_PX", "MIN_FONT_SIZE", "USER_MIN_FONT_SIZE", "USER_MAX_FONT_SIZE", "N_REPETITIONS", "LAYOUT_SEED", "SEED", "TARGET_FILL_RATIO", "WC_FONT_PATH", "AUTO_REPEAT_TO_FILL", "AUTO_REPEAT_MAX", ) } config.WC_FONT_PATH = str(config.PROJECT_DEFAULT_FONT) config.SIZE_RATIO = 1.0 config.WORK_SCALE = 0.18 config.MIN_READABLE_HEIGHT_PX = 22 config.MIN_FONT_SIZE = 3 config.USER_MIN_FONT_SIZE = None config.USER_MAX_FONT_SIZE = None config.N_REPETITIONS = 1 config.LAYOUT_SEED = 20260718 config.SEED = 20260718 config.TARGET_FILL_RATIO = 0.42 config.AUTO_REPEAT_TO_FILL = False def tearDown(self) -> None: for key, value in self.saved.items(): setattr(config, key, value) def generate(self, count: int, canvas: int = 1600): names = chinese_names(count) frequencies = [(name, 10.0) for name in names] weights = dict(frequencies) result = run_generation_pass( names, frequencies, weights, circle_mask(canvas), canvas, canvas, ) return names, result def test_size_ratio_one_keeps_every_equal_weight_size_identical(self) -> None: names, result = self.generate(40) layout = result["wc"].layout_ self.assertEqual(len(layout), len(names)) self.assertEqual(len({font_size for _, font_size, *_ in layout}), 1) def test_auto_repeat_keeps_hd_layout_overlap_free(self) -> None: config.AUTO_REPEAT_TO_FILL = True config.AUTO_REPEAT_MAX = 2 names, result = self.generate(40) self.assertGreaterEqual(len(result["hd_layout"]), len(names)) self.assertEqual(result["hd_overlap_pixels"], 0) def test_explicit_equal_min_max_is_exact(self) -> None: config.USER_MIN_FONT_SIZE = 12 config.USER_MAX_FONT_SIZE = 12 names, result = self.generate(20) layout = result["wc"].layout_ self.assertEqual(len(layout), len(names)) self.assertEqual({font_size for _, font_size, *_ in layout}, {12}) def test_same_weight_groups_receive_the_same_size(self) -> None: config.SIZE_RATIO = 2.0 names = chinese_names(24) weights = { name: (100.0 if index < 8 else 30.0 if index < 16 else 10.0) for index, name in enumerate(names) } frequencies = [(name, weights[name]) for name in names] result = run_generation_pass( names, frequencies, weights, circle_mask(1800), 1800, 1800, ) sizes_by_weight: dict[float, set[int]] = {} for name, font_size, *_ in result["wc"].layout_: sizes_by_weight.setdefault(weights[name], set()).add(font_size) self.assertEqual(len(result["wc"].layout_), len(names)) self.assertTrue(all(len(sizes) == 1 for sizes in sizes_by_weight.values())) self.assertGreater( max(sizes_by_weight[100.0]), max(sizes_by_weight[10.0]), ) def test_stroke_weight_is_applied_when_excel_weights_are_flat(self) -> None: merged = merge_weight_maps( ["甲", "乙", "丙"], {"甲": 100.0, "乙": 200.0, "丙": 300.0}, {"甲": 1.0, "乙": 1.0, "丙": 1.0}, ) self.assertLess(merged["甲"], merged["乙"]) self.assertLess(merged["乙"], merged["丙"]) def test_largest_empty_square_ignores_space_outside_mask(self) -> None: mask = np.full((7, 7), 255, dtype=np.uint8) mask[1:6, 1:6] = 0 occupancy = np.zeros((7, 7), dtype=np.uint8) occupancy[1:3, 1:6] = 1 self.assertEqual(largest_empty_square_size(occupancy, mask), 3) def test_conflicting_explicit_font_bounds_fail(self) -> None: config.USER_MIN_FONT_SIZE = 13 config.USER_MAX_FONT_SIZE = 12 with self.assertRaisesRegex(ValueError, "字号硬约束冲突"): self.generate(10) def test_explicit_max_overrides_automatic_readability_floor(self) -> None: config.USER_MAX_FONT_SIZE = 2 names, result = self.generate(12) layout = result["wc"].layout_ self.assertEqual(len(layout), len(names)) self.assertEqual({font_size for _, font_size, *_ in layout}, {2}) def test_base_class_never_uses_a_private_fallback_size(self) -> None: words = {name: 1.0 for name in chinese_names(12)} wc = EfficientWordCloud( width=150, height=150, font_path=config.WC_FONT_PATH, max_words=len(words), min_font_size=5, max_font_size=30, prefer_horizontal=1.0, random_state=7, margin=1, ) wc.generate_from_frequencies(words) self.assertGreater(len(wc.layout_), 0) self.assertEqual({font_size for _, font_size, *_ in wc.layout_}, {30}) def test_rendered_ink_stays_inside_mask_and_does_not_overlap(self) -> None: _names, result = self.generate(50) layout = result["wc"].layout_ mask = result["mask_small"] height, width = mask.shape coverage = np.zeros((height, width), dtype=np.uint16) for word, size, (y, x), orient, _color in layout: image = Image.new("L", (width, height), 0) draw = ImageDraw.Draw(image) font = get_cached_font(config.WC_FONT_PATH, size) if orient: font = ImageFont.TransposedFont(font, orientation=orient) draw.text((x, y), word, font=font, fill=255) coverage += (np.asarray(image) > 0).astype(np.uint16) self.assertFalse(np.any((coverage > 0) & (mask != 0))) self.assertLessEqual(int(coverage.max()), 1) def test_hd_rendered_ink_does_not_overlap_after_scaling(self) -> None: names, result = self.generate(50) self.assertEqual(len(result["wc"].layout_), len(names)) hd_layout = result["hd_layout"] overlap_pixels = count_layout_overlap_pixels( hd_layout, (1600, 1600), config.WC_FONT_PATH, ) self.assertEqual(result["hd_overlap_pixels"], 0) self.assertEqual(overlap_pixels, 0) self.assertEqual(result["collision_margin"], 0) self.assertEqual(result["hd_clearance"]["failed_word"], None) self.assertIn(result["hd_clearance"]["clearance_px"], (0, 1)) if __name__ == "__main__": unittest.main()