Files
wordcloud/backend/tests/test_layout_constraints.py

220 lines
8.1 KiB
Python

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