The MSIX package carried a single Square44x44Logo.png, so every taskbar and
Start slot on a HiDPI Windows 11 machine was Windows upscaling a 44px bitmap,
and with no altform variants Windows plated the icon itself. The generator now
emits the qualified catalog Windows resolves by itself: <Logo>.scale-{125,150,
200,400} for all four manifest logos at Microsoft's pixel sizes, and
Square44x44Logo.targetsize-{16..256} in the three required theme forms —
unqualified and altform-lightunplated carry the light tile, altform-unplated
(dark theme) the dark tile, the same split macOS dark mode gets. before-build
stages the whole assets/appx directory; app-builder-lib runs makepri and
`makeappx /l` as soon as it sees a qualified asset, so nothing else changes.
The hand-listed *-dark.png logos, generated but never staged, are gone.
Exposed along the way: the commit badge sat outside the portrait's silhouette
clip, and at 16–30px its anti-aliased edge stacked coverage on the plate's
corner pixels and changed the alpha channel between build flavors. The badge
now shares the clip, is plain black, and is left off direct renders below
32px where no SHA is legible anyway.
161 lines
8.5 KiB
Python
161 lines
8.5 KiB
Python
"""Icon generation reports per-target failures without hiding later targets."""
|
|
import importlib.util
|
|
import io
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
from types import ModuleType, SimpleNamespace
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
|
|
@pytest.mark.parametrize("bom", [b"", b"\xef\xbb\xbf"])
|
|
@pytest.mark.parametrize("editor_export", [False, True])
|
|
def test_svg_readers_accept_bom_without_rewriting_assets(tmp_path, monkeypatch, bom, editor_export):
|
|
monkeypatch.setitem(sys.modules, "resvg_py", ModuleType("resvg_py"))
|
|
script = Path(__file__).resolve().parents[2] / "scripts/generate_icons.py"
|
|
spec = importlib.util.spec_from_file_location("icon_readers_under_test", script)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
path = tmp_path / "art.svg"
|
|
element = '<path d="M0 0 L1 1" aria-label="café 東京"/>'
|
|
declaration = '<?xml version="1.0" encoding="UTF-8"?>\n' if editor_export else ""
|
|
namespaces = ' xmlns="http://www.w3.org/2000/svg" xmlns:editor="urn:editor"'
|
|
metadata = '<editor:namedview editor:zoom="1"/>' if editor_export else ""
|
|
document = f'<svg{namespaces} viewBox="0 0 20 30">{metadata}{element}</svg>'
|
|
raw = bom + (declaration + document).encode("utf-8")
|
|
path.write_bytes(raw)
|
|
art = SimpleNamespace(girls={"black": path}, paths={}, backgrounds=tmp_path, colors=None)
|
|
assert module.girl_path(art, "black") == element
|
|
inner, width, height = module.background_inner(art, path.name)
|
|
composed = ET.fromstring(f'<svg xmlns="http://www.w3.org/2000/svg">{inner}</svg>')
|
|
assert (width, height) == (20, 30)
|
|
assert [ET.tostring(child) for child in composed] == [
|
|
ET.tostring(child) for child in ET.fromstring(document)
|
|
]
|
|
assert path.read_bytes() == raw
|
|
path.write_bytes(bom + b"<svg/>")
|
|
art.paths.clear()
|
|
with pytest.raises(AssertionError, match="no <path>"):
|
|
module.girl_path(art, "black")
|
|
with pytest.raises(AssertionError, match="viewBox"):
|
|
module.background_inner(art, path.name)
|
|
|
|
|
|
@pytest.mark.parametrize("failure", [None, "render", "directory", "verify"])
|
|
def test_write_status_includes_every_target(tmp_path, monkeypatch, capsys, failure):
|
|
# The renderer is build-only. This test injects failures at its byte boundary.
|
|
monkeypatch.setitem(sys.modules, "resvg_py", ModuleType("resvg_py"))
|
|
script = Path(__file__).resolve().parents[2] / "scripts" / "generate_icons.py"
|
|
spec = importlib.util.spec_from_file_location("icon_generator_under_test", script)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
source = tmp_path / "immutable source"
|
|
source.mkdir()
|
|
monkeypatch.setattr(module, "IconArt", lambda root: root)
|
|
monkeypatch.setattr(sys, "argv", [str(script), "--source", str(source), "--out", str(tmp_path)])
|
|
|
|
image = io.BytesIO()
|
|
Image.new("RGBA", (2, 2), (0, 0, 0, 0)).save(image, "PNG")
|
|
good_bytes = image.getvalue()
|
|
first = "blocked/icon.png" if failure == "directory" else "first.png"
|
|
if failure == "directory":
|
|
(tmp_path / "blocked").write_text("not a directory", encoding="utf-8")
|
|
monkeypatch.setattr(module, "TARGETS", [(first, "png", "first"), ("last.png", "png", "last")])
|
|
|
|
def target_bytes(art, kind, target):
|
|
assert art == source
|
|
assert not list(source.iterdir())
|
|
if target == "first":
|
|
if failure == "render":
|
|
raise RuntimeError("injected render failure")
|
|
if failure == "verify":
|
|
return b"not an image"
|
|
return good_bytes
|
|
|
|
monkeypatch.setattr(module, "target_bytes", target_bytes)
|
|
code = 0
|
|
try:
|
|
module.main()
|
|
except SystemExit as stopped:
|
|
code = stopped.code
|
|
assert bool(code) is (failure is not None)
|
|
assert (tmp_path / "last.png").read_bytes() == good_bytes
|
|
output = capsys.readouterr().out
|
|
assert "last.png: PNG (2, 2)" in output
|
|
assert ("FAILED" in output) is (failure is not None)
|
|
|
|
|
|
@pytest.mark.parametrize("platform", ["", "mac-"])
|
|
@pytest.mark.parametrize("appearance,girl", [("light", "black"), ("dark", "white")])
|
|
@pytest.mark.parametrize("colors", [None, ("#f5cc32", "#443808"), ("#e34850", "#4a1117")])
|
|
def test_icon_portrait_sits_on_the_plain_tile_inside_the_outer_silhouette(monkeypatch, platform, appearance, girl, colors):
|
|
"""The tile keeps the background's own geometry and fill with no stroke
|
|
(the ring is disabled), the portrait is clipped to that outline and its
|
|
lowest nodes are dragged past the bottom edge so the clip crops her."""
|
|
monkeypatch.setitem(sys.modules, "resvg_py", ModuleType("resvg_py"))
|
|
source = Path(__file__).resolve().parents[2]
|
|
spec = importlib.util.spec_from_file_location("icon_geometry_under_test", source / "scripts/generate_icons.py")
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
assert module.BORDER_ENABLED is False
|
|
art = SimpleNamespace(
|
|
backgrounds=source / "assets/backgrounds", colors=colors, commit="0123456",
|
|
bboxes={girl: (0, 0, 100, 90)}, paths={girl: '<path d="M 0 0 L 100 0 L 100 90 L 0 90 z"/>'},
|
|
)
|
|
name = f"squircle-{platform}{appearance}.svg"
|
|
ns = {"svg": "http://www.w3.org/2000/svg"}
|
|
original = ET.parse(art.backgrounds / name).find("svg:rect", ns)
|
|
result = ET.fromstring(module.compose_svg(art, girl, name))
|
|
tile = result.find("svg:rect", ns)
|
|
assert original is not None and tile is not None
|
|
geometry = tuple(float(original.attrib[key]) for key in ("x", "y", "width", "height", "rx"))
|
|
assert tuple(float(tile.attrib[key]) for key in ("x", "y", "width", "height", "rx")) == geometry
|
|
assert tile.get("stroke") is None and tile.get("stroke-width") is None
|
|
expected_fill = (colors or ("#ffffff", module.DARK_HEX))[appearance == "dark"]
|
|
assert tile.get("fill") == expected_fill
|
|
clip = result.find("svg:defs/svg:clipPath/svg:rect", ns)
|
|
assert clip is not None
|
|
assert tuple(float(clip.attrib[key]) for key in ("x", "y", "width", "height", "rx")) == geometry
|
|
group = result[-1]
|
|
portrait = group[-1]
|
|
portraits = [child for child in group if child.tag == f"{{{ns['svg']}}}svg"]
|
|
assert portraits == [portrait], "extend the existing contour, not a duplicate strip"
|
|
# The commit badge rides inside the same clip, so its anti-aliased edge can
|
|
# never add alpha outside the plate (visible at targetsize-16..30).
|
|
assert [child.tag for child in group[:-1]] == [f"{{{ns['svg']}}}g"]
|
|
assert portrait.get("preserveAspectRatio") == "xMidYMax meet"
|
|
assert tuple(float(portrait.attrib[key]) for key in ("x", "y", "width", "height")) == module.GIRL_BOXES[name]
|
|
clip_path = result.find("svg:defs/svg:clipPath", ns)
|
|
assert clip_path is not None
|
|
assert group.get("clip-path") == f"url(#{clip_path.attrib['id']})"
|
|
assert portrait.get("overflow") == "visible"
|
|
assert len(result.findall(".//svg:path", ns)) == 2 # one badge and one portrait
|
|
# The dragged bottom node lands below the tile: the clip, not a gap, ends her.
|
|
x, y, width, height, _ = geometry
|
|
_, by, bw, bh = art.bboxes[girl]
|
|
box_y, box_h = module.GIRL_BOXES[name][1], module.GIRL_BOXES[name][3]
|
|
scale = min(module.GIRL_BOXES[name][2] / bw, box_h / bh)
|
|
lowest_raw_y = max(float(v) for v in re.findall(r"[-+]?\d*\.?\d+", portrait[0].attrib["d"])[1::2])
|
|
lowest_tile_y = box_y + box_h - bh * scale + (lowest_raw_y - by) * scale # xMidYMax: bottom-aligned
|
|
assert lowest_tile_y == pytest.approx(y + height + width * module.EDGE_OVERSHOOT)
|
|
|
|
|
|
def test_bottom_node_drag_preserves_upper_geometry_and_path_transform(monkeypatch):
|
|
monkeypatch.setitem(sys.modules, "resvg_py", ModuleType("resvg_py"))
|
|
script = Path(__file__).resolve().parents[2] / "scripts/generate_icons.py"
|
|
spec = importlib.util.spec_from_file_location("icon_nodes_under_test", script)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
path = ET.fromstring('<path transform="matrix(2,0,0,2,10,-100)" d="M 0 0 C 0 90 10 98 20 100 L 0 100 z"/>')
|
|
module.drag_bottom_nodes(path, cutoff=80, band=20, distance=10)
|
|
assert path.get("transform") == "matrix(2,0,0,2,10,-100)"
|
|
assert path.get("d") == "M 0 0 C 0 90 10 102.48 20 105 L 0 105 z"
|
|
with pytest.raises(ValueError, match="absolute M/L/C"):
|
|
module.drag_bottom_nodes(ET.fromstring('<path d="m 0 0 l 1 1"/>'), cutoff=0, band=1, distance=1)
|