From 256aa29cf5d32b6a60b12a582f79e07ede0185a1 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 02:47:09 -0500 Subject: [PATCH 1/8] Fit custom transparent icons by their alpha bounds Custom icon art (--icon, showcase.yml icon, assets/icon.png) is cropped to its alpha bounds in the compositor so transparent margins no longer shrink the art. Bundled brand marks and logos, opaque or full-bleed icons, pixel art and blank images are unchanged. Source files are never modified. Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/iconbox.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/ctshowcase/iconbox.py b/scripts/ctshowcase/iconbox.py index 1ec8560..5055ab6 100644 --- a/scripts/ctshowcase/iconbox.py +++ b/scripts/ctshowcase/iconbox.py @@ -8,6 +8,10 @@ Plate rule (review P1.2): an icon that brings its own opaque ground (four corners opaque and within ΔE 3 of each other) fills the card with that ground and is drawn full-bleed, clipped to the card radius, so there is never a plate inside a plate. Dark plates get a 2 px family-accent edge at 60% so they read on the navy canvas and on GitHub dark. + +Custom transparent icons (--icon, showcase.yml icon, assets/icon.png) are fitted by their alpha bounds so transparent +margins do not shrink the art; bundled brand marks and logos, opaque or full-bleed icons, pixel art and blank images +are unchanged. """ from __future__ import annotations @@ -27,6 +31,7 @@ GROUND_DE = 3.0 # corner colours within this ΔE are one ground DARK_PLATE = 0.2 # relative luminance under which a plate gets the accent edge PIXEL_GRIDS = (16, 24, 32, 48, 64, 96, 128) +CUSTOM_ICON_SOURCES = frozenset({"cli", "override", "repo"}) # ctx.light_icon sources that are user art, not bundled def corner_spread(im: Image.Image) -> float | None: @@ -79,6 +84,19 @@ def native_pixels(im: Image.Image) -> Image.Image | None: return None +def alpha_fit(im: Image.Image) -> Image.Image | None: + """Crop custom transparent art to its alpha bounds, or None when no crop applies. + + Fits custom transparent art by its alpha bounds so transparent margins do not shrink it. Compositor only: the + source file is never modified, and the input image is neither mutated nor reassigned. Returns None when the image + is fully transparent (blank) or when the alpha bounds are the whole image (opaque or full-bleed art).""" + rgba = im.convert("RGBA") + bbox = rgba.getchannel("A").getbbox() + if bbox is None or bbox == (0, 0, rgba.width, rgba.height): + return None + return rgba.crop(bbox) + + def feather(im: Image.Image, sides: bool, ends: bool, frac: float = 0.14) -> Image.Image: """Blend a full-bleed icon's own ground into the card where the icon does not reach the card edge: an alpha ramp over the outer 14% of the ground only (glyphs sit inside the avatar live area, so the mark itself is untouched). @@ -96,7 +114,7 @@ def feather(im: Image.Image, sides: bool, ends: bool, frac: float = 0.14) -> Ima return rgba -def _icon_nodes(ctx: RepoContext, im: Image.Image, box, clip=None) -> tuple[list, float | None]: +def _icon_nodes(ctx: RepoContext, im: Image.Image, box, clip=None, fit_alpha: bool = False) -> tuple[list, float | None]: x, y, w, h = box resample = "lanczos" if ctx.style.get("icon_pixel_art"): @@ -109,6 +127,9 @@ def _icon_nodes(ctx: RepoContext, im: Image.Image, box, clip=None) -> tuple[list pic = Picture(x + (w - iw) / 2, y + (h - ih) / 2, iw, ih, im, "webp", 84, 2, resample) # pixel art is meant to be enlarged: no icon-resolution warning (fit None) return ([Clip(clip[2], clip[3], clip[4], [pic], clip[0], clip[1])] if clip else [pic]), None + if fit_alpha: + trimmed = alpha_fit(im) + im = trimmed if trimmed is not None else im k = min(w / im.width, h / im.height) iw, ih = im.width * k, im.height * k if clip and (iw < w - 1 or ih < h - 1): @@ -147,7 +168,8 @@ def plate(ctx: RepoContext, fonts: Fonts, card: tuple[float, float, float, float except ValueError: pass box = (x, y, w, h) if full_bleed else inner - icon_nodes, fit = _icon_nodes(ctx, im, box, (x, y, w, h, radius) if full_bleed else None) + icon_nodes, fit = _icon_nodes(ctx, im, box, (x, y, w, h, radius) if full_bleed else None, + fit_alpha=src in CUSTOM_ICON_SOURCES and not full_bleed) nodes += icon_nodes info = {"path": shown, "source": src, "plate": fill, "full_bleed": full_bleed} else: From 83b86966cf19c861242b1b1f384eb0ae27544219 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 02:48:13 -0500 Subject: [PATCH 2/8] test: cover alpha-bounds fit for custom icon art (#130) Synthetic regression and control tests for iconbox.alpha_fit and the plate geometry: palette transparency, RGBA, blank, fully covered, opaque and full-bleed, pixel-art sizing, bundled brand marks and logos, org mark, and source PNG bytes unchanged. Fixtures are generated under tmp_path. Co-Authored-By: Claude Haiku 5.5 --- tests/test_iconbox_alpha_fit.py | 232 ++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 tests/test_iconbox_alpha_fit.py diff --git a/tests/test_iconbox_alpha_fit.py b/tests/test_iconbox_alpha_fit.py new file mode 100644 index 0000000..a07aa9c --- /dev/null +++ b/tests/test_iconbox_alpha_fit.py @@ -0,0 +1,232 @@ +"""Issue #130: custom transparent icon art is fitted by its alpha bounds inside the icon plate (compositor only). + +Every image is synthetic and generated under tmp_path; no committed raster is read or edited. Geometry is derived +from the card (56, 70, 420, 220), radius 24 and inset 28 used by the banner, so the inner box is 364 x 164. +""" +import pytest +from PIL import Image, ImageDraw + +from conftest import EX + +CARD = (56, 70, 420, 220) +RADIUS = 24 +INSET = 28 +INNER_W, INNER_H = 364, 164 +BBOX = (168, 236, 856, 787) # PIL-style (left, top, right exclusive, bottom exclusive): 688 x 551 +BBOX_SIZE = (688, 551) + + +def pics(nodes): + return [c for n in nodes for c in getattr(n, "children", [n]) if type(c).__name__ == "Picture"] + + +def save_and_reopen(im, path): + im.save(path, format="PNG") + reopened = Image.open(path) + reopened.load() + return reopened + + +def palette_art(size=1024): + """Mode P with palette transparency: index 0 is the transparent background, index 1 the coloured artwork.""" + im = Image.new("P", (size, size), 0) + palette = [255, 255, 255, 200, 30, 30] + [0, 0, 0] * 254 + im.putpalette(palette) + im.paste(1, BBOX) + im.info["transparency"] = 0 + return im + + +def rgba_art(size=1024): + im = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + im.paste((200, 30, 30, 255), BBOX) + return im + + +def render(ctx, fonts, path): + from ctshowcase import iconbox + return iconbox.plate(ctx, fonts, CARD, RADIUS, INSET, cli_icon=str(path)) + + +def assert_fits_inner_box(pic): + assert abs(pic.w / pic.h - BBOX_SIZE[0] / BBOX_SIZE[1]) < 0.01 + assert pic.w >= INNER_W - 1 or pic.h >= INNER_H - 1 + # The old whole-canvas behaviour drew the 1024 px square at 164 px, so the 688 px content was 110.2 px wide. + assert pic.w > 164 * 688 / 1024 + 1 + + +def test_alpha_fit_palette_transparency(tmp_path, make_ctx, fonts): + from ctshowcase import iconbox + path = tmp_path / "palette-icon.png" + im = save_and_reopen(palette_art(), path) + assert im.mode == "P" + assert im.info.get("transparency") == 0 + + fitted = iconbox.alpha_fit(im) + assert fitted is not None + assert fitted.mode == "RGBA" + assert fitted.size == BBOX_SIZE + + nodes, info, _ = render(make_ctx(), fonts, path) + assert info["full_bleed"] is False + found = pics(nodes) + assert len(found) == 1 + assert_fits_inner_box(found[0]) + + +def test_alpha_fit_rgba(tmp_path, make_ctx, fonts): + from ctshowcase import iconbox + path = tmp_path / "rgba-icon.png" + im = save_and_reopen(rgba_art(), path) + assert im.mode == "RGBA" + + fitted = iconbox.alpha_fit(im) + assert fitted is not None + assert fitted.mode == "RGBA" + assert fitted.size == BBOX_SIZE + + nodes, info, _ = render(make_ctx(), fonts, path) + assert info["full_bleed"] is False + found = pics(nodes) + assert len(found) == 1 + assert_fits_inner_box(found[0]) + + +def test_blank_transparency_unchanged(tmp_path, make_ctx, fonts): + from ctshowcase import iconbox + path = tmp_path / "blank-icon.png" + im = save_and_reopen(Image.new("RGBA", (1024, 1024), (0, 0, 0, 0)), path) + + assert iconbox.alpha_fit(im) is None + + nodes, _, _ = render(make_ctx(), fonts, path) + found = pics(nodes) + assert len(found) == 1 + assert found[0].w == pytest.approx(164, abs=0.01) + assert found[0].h == pytest.approx(164, abs=0.01) + + +def test_opaque_and_full_bleed_unchanged(tmp_path, make_ctx, fonts): + from ctshowcase import iconbox + + # An opaque solid-colour image has no transparent pixels, so there is nothing to fit. + solid_path = tmp_path / "solid-icon.png" + solid = save_and_reopen(Image.new("RGB", (1024, 1024), (30, 120, 200)), solid_path) + assert iconbox.alpha_fit(solid) is None + + # An opaque striped image (no transparency at all) is also left alone. + striped = Image.new("RGB", (1024, 1024), (240, 240, 240)) + draw = ImageDraw.Draw(striped) + for x in range(0, 1024, 64): + draw.rectangle((x, 0, x + 31, 1023), fill=(20, 60, 140)) + striped_path = tmp_path / "striped-icon.png" + striped = save_and_reopen(striped, striped_path) + assert iconbox.alpha_fit(striped) is None + + # Alpha 255 everywhere with a distinctive ground: drawn full-bleed over the whole card, never cropped. + ground_path = tmp_path / "ground-icon.png" + ground = Image.new("RGBA", (1024, 1024), (230, 60, 140, 255)) + ground = save_and_reopen(ground, ground_path) + assert iconbox.alpha_fit(ground) is None + + nodes, info, _ = render(make_ctx(), fonts, ground_path) + assert info["full_bleed"] is True + found = pics(nodes) + assert len(found) == 1 + pic = found[0] + # Whole-canvas geometry: the 1024 px square is scaled to the card height (220) and centred horizontally. + scale = min(CARD[2] / 1024, CARD[3] / 1024) + assert pic.w == pytest.approx(1024 * scale, abs=0.01) + assert pic.h == pytest.approx(1024 * scale, abs=0.01) + assert pic.x == pytest.approx(CARD[0] + (CARD[2] - 1024 * scale) / 2, abs=0.01) + assert pic.y == pytest.approx(CARD[1], abs=0.01) + assert pic.image.size == (1024, 1024) + + +def test_fully_covered_alpha_bbox_returns_none(tmp_path): + from ctshowcase import iconbox + # Transparent corners, but a cross of opaque pixels touches all four edges: the alpha bbox is the whole canvas. + im = Image.new("RGBA", (1024, 1024), (0, 0, 0, 0)) + im.paste((30, 120, 200, 255), (0, 462, 1024, 562)) + im.paste((30, 120, 200, 255), (462, 0, 562, 1024)) + path = tmp_path / "cross-icon.png" + im = save_and_reopen(im, path) + assert im.getpixel((0, 0))[3] == 0 + assert iconbox.alpha_fit(im) is None + + +def test_pixel_art_sizing_unchanged(tmp_path, make_ctx, fonts): + from ctshowcase import iconbox + ctx = make_ctx(repo_dir=EX / "repos" / "jev-minesweeper", owner="jevplay-games", repo="jev-minesweeper", + description="Two-board Minesweeper race against Jev.") + nodes, info, _ = iconbox.plate(ctx, fonts, (56, 70, 420, 220), 24, 28) + found = pics(nodes) + assert info["plate"] == "#F386A1" + assert found[0].resample == "nearest" + assert (found[0].w * 2) % found[0].image.width == 0 + + # A 16 x 16 sprite upscaled x16 to 256 px with a transparent margin of three cells on each side. + sprite = Image.new("RGBA", (16, 16), (0, 0, 0, 0)) + draw = ImageDraw.Draw(sprite) + draw.rectangle((3, 3, 12, 12), fill=(250, 200, 40, 255)) + draw.rectangle((6, 6, 9, 9), fill=(20, 20, 20, 255)) + big = sprite.resize((256, 256), Image.NEAREST) + path = tmp_path / "sprite-icon.png" + big = save_and_reopen(big, path) + + assert ctx.style.get("icon_pixel_art") + nodes, _, fit = render(ctx, fonts, path) + found = pics(nodes) + assert len(found) == 1 + pic = found[0] + assert pic.resample == "nearest" + assert (pic.w * 2) % pic.image.width == 0 + # Native 16-cell grid kept: not cropped to the alpha bbox (which would be 10 cells wide). + assert pic.image.width == 16 + assert fit is None + + +def test_bundled_brand_marks_and_logos_untouched(make_ctx, fonts): + from ctshowcase import iconbox + assert iconbox.CUSTOM_ICON_SOURCES == {"cli", "override", "repo"} + + # No icon argument and no repo: the bundled CompleteTech light logo is drawn at its own size. + ctx = make_ctx() + nodes, info, _ = iconbox.plate(ctx, fonts, CARD, RADIUS, INSET) + assert info["source"] == "brand-logo" + assert info["source"] not in iconbox.CUSTOM_ICON_SOURCES + bundled = ctx.brand.logo_light() + assert pics(nodes)[0].image.size == Image.open(bundled).size + + # Small icon boxes use the bundled light mark, which must also stay uncropped. + small = (56, 70, 120, 120) + nodes, info, _ = iconbox.plate(ctx, fonts, small, RADIUS, INSET) + assert info["source"] == "brand-mark" + assert info["source"] not in iconbox.CUSTOM_ICON_SOURCES + assert pics(nodes)[0].image.size == Image.open(ctx.brand.mark_light()).size + + +def test_org_mark_untouched(tmp_path, make_ctx, fonts): + from ctshowcase import iconbox + # A synthetic org mark with transparent margins: the org-mark source is a bundled-brand path, never fitted. + mark = Image.new("RGBA", (512, 512), (0, 0, 0, 0)) + ImageDraw.Draw(mark).rectangle((128, 128, 383, 383), fill=(20, 60, 140, 255)) + mark_path = tmp_path / "org-mark.png" + mark.save(mark_path, format="PNG") + ctx = make_ctx() + ctx.org = dict(ctx.org, mark=str(mark_path)) + nodes, info, _ = iconbox.plate(ctx, fonts, CARD, RADIUS, INSET) + assert info["source"] == "org-mark" + assert info["source"] not in iconbox.CUSTOM_ICON_SOURCES + assert pics(nodes)[0].image.size == (512, 512) + + +def test_source_png_not_modified(tmp_path, make_ctx, fonts): + path = tmp_path / "palette-source.png" + save_and_reopen(palette_art(), path) + before = path.read_bytes() + + render(make_ctx(), fonts, path) + + assert path.read_bytes() == before + assert Image.open(path).mode == "P" From 69b829e0c3e253101c7b4e40d54bc69c9fffb7e2 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 02:49:15 -0500 Subject: [PATCH 3/8] docs: changelog and banner-layout note for alpha-bounds icon fit (#130) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + references/banner-layout.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af911f5..4d3b9a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo ### Fixed +- Custom transparent icon art (`--icon`, `showcase.yml` `icon`, `assets/icon.png`), including PNG mode P with transparency and RGBA, is fitted by its alpha bounds in the icon box in the `iconbox` compositor, so transparent margins no longer shrink it. The raw and normalized PNG are unchanged; bundled brand marks and logos, golden plate geometry, opaque or full-bleed icons, pixel-art sizing and fully blank transparency are unchanged (#130; reuses the #126 normalizer, no new normalizer). - `readme --bootstrap --write` no longer regenerates a marked footer that holds authored content. When the edited-template upgrade is refused by the never-drop-a-line check and the marked footer carries a partner or foreign image, the command now exits with a check failure (`readme-shrink`) and leaves `README.md` unchanged instead of falling back to a header refresh that rewrote the footer; a plain old footer copy still takes the fallback. The same refusal now applies in every bootstrap state (pristine and unmarked as well as edited), checked before any write, so a pristine or unmarked template can no longer silently replace a partner or foreign footer image. - Fork (G) and backup (H) READMEs migrate an unmarked trailing footer whose logo is a configured own-site historical source (`footer.legacy_logo_urls` or `docs/images/logo.svg`) to the single current footer; partner, foreign and look-alike images stay authored and are reported as no allowance. - Own-site historical footer logo recognition is URL-derived: the company-website logo lives in `footer.legacy_logo_urls` in `references/brand.yaml` (lock refreshed through `config --write-lock`) and matches only exactly (scheme, host case-insensitively, port, path; no userinfo), so no brand host or path remains in Python. The generic local `docs/images/logo.svg` migration is a file convention, not brand data, and stays distinct. diff --git a/references/banner-layout.md b/references/banner-layout.md index ba69260..84f00d1 100644 --- a/references/banner-layout.md +++ b/references/banner-layout.md @@ -75,7 +75,7 @@ First match wins; the same file serves the banner card and the card's icon tile: 3. the org mark (`orgs..mark`: the approved Neon Night avatar for every T1 org except CompleteTech-LLC), logging `needs-input:repo-icon`; 4. the family fallback, logging `needs-input:repo-icon`: for CompleteTech orgs the light CompleteTech logo (`brand_assets.logo_light`, the golden look), or in an icon box under 200 px (the card tile) its committed cloud-and-bulb crop `brand_assets.mark_light`, because the full wordmark would be about 3 px tall there; for every other family the family `company_label` typeset in its `display` font and `card_ink` colour on the family plate, over a short accent rule (one to three balanced lines, largest size that fits). A T2 card never shows the CompleteTech logo (decision 13; TouchedByAnAgent hold). -**Plate rule.** When the icon's four corner pixels are opaque and within ΔE 3 of each other, the icon brings its own ground: the card is filled with that colour and the icon is drawn full-bleed (inset 0, clipped to the card radius), with its ground feathered into the card where a square icon does not reach the card's sides. There is never a plate inside a plate. T2 families without an icon get a dark plate (`card_fill`: Jev `#0C075C`, LibreEvolve `#032530`, Touched By An Agent `#2F0132`, romgenie `#183B31`); AI Ascension keeps its paper `#EDE4D3`; jevplay.games uses pink `#F386A1`. Every plate darker than 20% luminance gets a 2 px edge in the family accent at 60%. jevplay.games icons (`icon_pixel_art`) are reduced to their native pixel grid and scaled nearest-neighbour at an integer factor (the §9.5 avatar rule), never filtered. +**Plate rule.** When the icon's four corner pixels are opaque and within ΔE 3 of each other, the icon brings its own ground: the card is filled with that colour and the icon is drawn full-bleed (inset 0, clipped to the card radius), with its ground feathered into the card where a square icon does not reach the card's sides. There is never a plate inside a plate. T2 families without an icon get a dark plate (`card_fill`: Jev `#0C075C`, LibreEvolve `#032530`, Touched By An Agent `#2F0132`, romgenie `#183B31`); AI Ascension keeps its paper `#EDE4D3`; jevplay.games uses pink `#F386A1`. Every plate darker than 20% luminance gets a 2 px edge in the family accent at 60%. jevplay.games icons (`icon_pixel_art`) are reduced to their native pixel grid and scaled nearest-neighbour at an integer factor (the §9.5 avatar rule), never filtered. A custom transparent icon (`--icon`, `showcase.yml` `icon`, `assets/icon.png`; PNG mode P with transparency or RGBA) is fitted by its alpha bounds in the icon box, so transparent margins do not shrink it; this is compositor behaviour only (the PNG file is unchanged), and bundled marks and logos, opaque or full-bleed icons, pixel-art icons and fully blank transparency keep their existing sizing. ### Refusals From 2a4da5dbc5dba5be6027f0d9640edac15aa89d80 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 02:57:13 -0500 Subject: [PATCH 4/8] fix(iconbox): skip alpha fit for pixel-art icons on unrecognized grids (#130) When icon_pixel_art is set, the alpha crop must never run, including when native_pixels() does not recognize the grid. Previously a 257x257 transparent icon was cropped to 153x125 instead of keeping the retained 257x257 image at the 100x100 placement. Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/iconbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ctshowcase/iconbox.py b/scripts/ctshowcase/iconbox.py index 5055ab6..0c000f2 100644 --- a/scripts/ctshowcase/iconbox.py +++ b/scripts/ctshowcase/iconbox.py @@ -127,7 +127,8 @@ def _icon_nodes(ctx: RepoContext, im: Image.Image, box, clip=None, fit_alpha: bo pic = Picture(x + (w - iw) / 2, y + (h - ih) / 2, iw, ih, im, "webp", 84, 2, resample) # pixel art is meant to be enlarged: no icon-resolution warning (fit None) return ([Clip(clip[2], clip[3], clip[4], [pic], clip[0], clip[1])] if clip else [pic]), None - if fit_alpha: + # icon_pixel_art keeps the retained image size even when the native grid is not recognized (no alpha crop) + if fit_alpha and not ctx.style.get("icon_pixel_art"): trimmed = alpha_fit(im) im = trimmed if trimmed is not None else im k = min(w / im.width, h / im.height) From 80a1fc5feeb1553cf8fd975f467b580616a67b77 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 02:57:51 -0500 Subject: [PATCH 5/8] test: pin unrecognized pixel grid keeps size without alpha fit (#130) Adds a control for a 257x257 transparent icon with icon_pixel_art set. The native grid is unrecognized, so the image must stay 257x257 at the main placement (no alpha crop). The flag-off contrast still crops by alpha bounds, proving the flag prevents the crop. Co-Authored-By: Claude Haiku 5.5 --- tests/test_iconbox_alpha_fit.py | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_iconbox_alpha_fit.py b/tests/test_iconbox_alpha_fit.py index a07aa9c..273d1b2 100644 --- a/tests/test_iconbox_alpha_fit.py +++ b/tests/test_iconbox_alpha_fit.py @@ -186,6 +186,41 @@ def test_pixel_art_sizing_unchanged(tmp_path, make_ctx, fonts): assert fit is None +def test_pixel_art_unrecognized_grid_keeps_size_no_alpha_fit(tmp_path, make_ctx, fonts): + """Unrecognized pixel grid with icon_pixel_art keeps the retained image and placement of main (#130 adjudication); + flag off still fits by alpha bounds.""" + from ctshowcase import iconbox + ctx = make_ctx(repo_dir=EX / "repos" / "jev-minesweeper", owner="jevplay-games", repo="jev-minesweeper", + description="Two-board Minesweeper race against Jev.") + assert ctx.style.get("icon_pixel_art") + + # Opaque art with transparent margins: alpha_fit would crop it, but no pixel grid matches a 257 px canvas. + im = Image.new("RGBA", (257, 257), (0, 0, 0, 0)) + ImageDraw.Draw(im).rectangle((60, 90, 200, 200), fill=(250, 200, 40, 255)) + path = tmp_path / "odd-grid-icon.png" + im = save_and_reopen(im, path) + assert iconbox.native_pixels(im) is None + + nodes, _, fit = render(ctx, fonts, path) + found = pics(nodes) + assert len(found) == 1 + pic = found[0] + assert pic.image.size == (257, 257) + assert pic.resample == "lanczos" + k = min(INNER_W / 257, INNER_H / 257) + assert pic.w == pytest.approx(257 * k, abs=0.01) + assert pic.h == pytest.approx(257 * k, abs=0.01) + assert pic.w == pytest.approx(pic.h, abs=0.01) + assert fit == pytest.approx(k) + + # Flag-off contrast: the same file is cropped to its alpha bounds, proving the flag prevents the crop. + ctx.style["icon_pixel_art"] = False + nodes, _, _ = render(ctx, fonts, path) + found = pics(nodes) + assert len(found) == 1 + assert found[0].image.width < 257 + + def test_bundled_brand_marks_and_logos_untouched(make_ctx, fonts): from ctshowcase import iconbox assert iconbox.CUSTOM_ICON_SOURCES == {"cli", "override", "repo"} From c342d688acf704563574d734ec2123e01ea09f47 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 02:58:37 -0500 Subject: [PATCH 6/8] docs: alpha fit never applies with icon_pixel_art, incl. unrecognized grid (#130) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- references/banner-layout.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3b9a4..2be963e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo ### Fixed -- Custom transparent icon art (`--icon`, `showcase.yml` `icon`, `assets/icon.png`), including PNG mode P with transparency and RGBA, is fitted by its alpha bounds in the icon box in the `iconbox` compositor, so transparent margins no longer shrink it. The raw and normalized PNG are unchanged; bundled brand marks and logos, golden plate geometry, opaque or full-bleed icons, pixel-art sizing and fully blank transparency are unchanged (#130; reuses the #126 normalizer, no new normalizer). +- Custom transparent icon art (`--icon`, `showcase.yml` `icon`, `assets/icon.png`), including PNG mode P with transparency and RGBA, is fitted by its alpha bounds in the icon box in the `iconbox` compositor, so transparent margins no longer shrink it. The raw and normalized PNG are unchanged; bundled brand marks and logos, golden plate geometry, opaque or full-bleed icons, pixel-art sizing and fully blank transparency are unchanged. With `icon_pixel_art` set, alpha fit never applies, including an unrecognized native grid, which keeps the retained image size and placement (#130; reuses the #126 normalizer, no new normalizer). - `readme --bootstrap --write` no longer regenerates a marked footer that holds authored content. When the edited-template upgrade is refused by the never-drop-a-line check and the marked footer carries a partner or foreign image, the command now exits with a check failure (`readme-shrink`) and leaves `README.md` unchanged instead of falling back to a header refresh that rewrote the footer; a plain old footer copy still takes the fallback. The same refusal now applies in every bootstrap state (pristine and unmarked as well as edited), checked before any write, so a pristine or unmarked template can no longer silently replace a partner or foreign footer image. - Fork (G) and backup (H) READMEs migrate an unmarked trailing footer whose logo is a configured own-site historical source (`footer.legacy_logo_urls` or `docs/images/logo.svg`) to the single current footer; partner, foreign and look-alike images stay authored and are reported as no allowance. - Own-site historical footer logo recognition is URL-derived: the company-website logo lives in `footer.legacy_logo_urls` in `references/brand.yaml` (lock refreshed through `config --write-lock`) and matches only exactly (scheme, host case-insensitively, port, path; no userinfo), so no brand host or path remains in Python. The generic local `docs/images/logo.svg` migration is a file convention, not brand data, and stays distinct. diff --git a/references/banner-layout.md b/references/banner-layout.md index 84f00d1..72dd048 100644 --- a/references/banner-layout.md +++ b/references/banner-layout.md @@ -75,7 +75,7 @@ First match wins; the same file serves the banner card and the card's icon tile: 3. the org mark (`orgs..mark`: the approved Neon Night avatar for every T1 org except CompleteTech-LLC), logging `needs-input:repo-icon`; 4. the family fallback, logging `needs-input:repo-icon`: for CompleteTech orgs the light CompleteTech logo (`brand_assets.logo_light`, the golden look), or in an icon box under 200 px (the card tile) its committed cloud-and-bulb crop `brand_assets.mark_light`, because the full wordmark would be about 3 px tall there; for every other family the family `company_label` typeset in its `display` font and `card_ink` colour on the family plate, over a short accent rule (one to three balanced lines, largest size that fits). A T2 card never shows the CompleteTech logo (decision 13; TouchedByAnAgent hold). -**Plate rule.** When the icon's four corner pixels are opaque and within ΔE 3 of each other, the icon brings its own ground: the card is filled with that colour and the icon is drawn full-bleed (inset 0, clipped to the card radius), with its ground feathered into the card where a square icon does not reach the card's sides. There is never a plate inside a plate. T2 families without an icon get a dark plate (`card_fill`: Jev `#0C075C`, LibreEvolve `#032530`, Touched By An Agent `#2F0132`, romgenie `#183B31`); AI Ascension keeps its paper `#EDE4D3`; jevplay.games uses pink `#F386A1`. Every plate darker than 20% luminance gets a 2 px edge in the family accent at 60%. jevplay.games icons (`icon_pixel_art`) are reduced to their native pixel grid and scaled nearest-neighbour at an integer factor (the §9.5 avatar rule), never filtered. A custom transparent icon (`--icon`, `showcase.yml` `icon`, `assets/icon.png`; PNG mode P with transparency or RGBA) is fitted by its alpha bounds in the icon box, so transparent margins do not shrink it; this is compositor behaviour only (the PNG file is unchanged), and bundled marks and logos, opaque or full-bleed icons, pixel-art icons and fully blank transparency keep their existing sizing. +**Plate rule.** When the icon's four corner pixels are opaque and within ΔE 3 of each other, the icon brings its own ground: the card is filled with that colour and the icon is drawn full-bleed (inset 0, clipped to the card radius), with its ground feathered into the card where a square icon does not reach the card's sides. There is never a plate inside a plate. T2 families without an icon get a dark plate (`card_fill`: Jev `#0C075C`, LibreEvolve `#032530`, Touched By An Agent `#2F0132`, romgenie `#183B31`); AI Ascension keeps its paper `#EDE4D3`; jevplay.games uses pink `#F386A1`. Every plate darker than 20% luminance gets a 2 px edge in the family accent at 60%. jevplay.games icons (`icon_pixel_art`) are reduced to their native pixel grid and scaled nearest-neighbour at an integer factor (the §9.5 avatar rule), never filtered. A custom transparent icon (`--icon`, `showcase.yml` `icon`, `assets/icon.png`; PNG mode P with transparency or RGBA) is fitted by its alpha bounds in the icon box, so transparent margins do not shrink it; this is compositor behaviour only (the PNG file is unchanged), and bundled marks and logos, opaque or full-bleed icons, pixel-art icons and fully blank transparency keep their existing sizing. With `icon_pixel_art` set, alpha fit never applies, including an unrecognized native grid, which keeps the retained image size and placement. ### Refusals From 131d624adbf1855f547b27a29c13222116fbba1e Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 03:27:11 -0500 Subject: [PATCH 7/8] fix: never alpha-fit bundled assets/brand artwork (#130) is_bundled_brand_asset(path, root) returns True for any file under /assets/brand, so bundled marks and logos keep their full canvas whichever selection route (cli, override, repo) picks them. Custom transparent art is still fitted by its alpha bounds. Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/iconbox.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scripts/ctshowcase/iconbox.py b/scripts/ctshowcase/iconbox.py index 0c000f2..87e04ca 100644 --- a/scripts/ctshowcase/iconbox.py +++ b/scripts/ctshowcase/iconbox.py @@ -10,14 +10,15 @@ plate. Dark plates get a 2 px family-accent edge at 60% so they read on the navy canvas and on GitHub dark. Custom transparent icons (--icon, showcase.yml icon, assets/icon.png) are fitted by their alpha bounds so transparent -margins do not shrink the art; bundled brand marks and logos, opaque or full-bleed icons, pixel art and blank images -are unchanged. +margins do not shrink the art. Any file under assets/brand is treated as bundled artwork and is never fitted, even when +it is selected through --icon or showcase.yml icon; opaque or full-bleed icons, pixel art and blank images are unchanged. """ from __future__ import annotations import math import re from itertools import combinations +from pathlib import Path from PIL import Image, ImageChops @@ -97,6 +98,17 @@ def alpha_fit(im: Image.Image) -> Image.Image | None: return rgba.crop(bbox) +def is_bundled_brand_asset(path, root) -> bool: + """True when `path` resolves under /assets/brand, whatever the selection route (cli, override or repo). + + Bundled artwork under assets/brand is never alpha-cropped. Returns False when either path cannot be resolved.""" + try: + Path(path).resolve().relative_to((Path(root) / "assets" / "brand").resolve()) + return True + except (ValueError, OSError): + return False + + def feather(im: Image.Image, sides: bool, ends: bool, frac: float = 0.14) -> Image.Image: """Blend a full-bleed icon's own ground into the card where the icon does not reach the card edge: an alpha ramp over the outer 14% of the ground only (glyphs sit inside the avatar live area, so the mark itself is untouched). @@ -170,7 +182,8 @@ def plate(ctx: RepoContext, fonts: Fonts, card: tuple[float, float, float, float pass box = (x, y, w, h) if full_bleed else inner icon_nodes, fit = _icon_nodes(ctx, im, box, (x, y, w, h, radius) if full_bleed else None, - fit_alpha=src in CUSTOM_ICON_SOURCES and not full_bleed) + fit_alpha=src in CUSTOM_ICON_SOURCES and not full_bleed + and not is_bundled_brand_asset(path, ctx.brand.root)) nodes += icon_nodes info = {"path": shown, "source": src, "plate": fill, "full_bleed": full_bleed} else: From 68b5e6728d58b277ee90714e938b0f1c0a169729 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 1 Nov 2026 03:28:07 -0500 Subject: [PATCH 8/8] test: synthetic bundled-brand alpha-fit guard cases (#130) Adds tests for bundled brand PNGs through the cli and override routes, the path-based guard control, every real assets/brand PNG untouched, and is_bundled_brand_asset path checks. Existing tests unchanged. Co-Authored-By: Claude Haiku 5.5 --- tests/test_iconbox_alpha_fit.py | 94 +++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/test_iconbox_alpha_fit.py b/tests/test_iconbox_alpha_fit.py index 273d1b2..7c6ec0d 100644 --- a/tests/test_iconbox_alpha_fit.py +++ b/tests/test_iconbox_alpha_fit.py @@ -265,3 +265,97 @@ def test_source_png_not_modified(tmp_path, make_ctx, fonts): assert path.read_bytes() == before assert Image.open(path).mode == "P" + + +def _assert_bundled_both_routes(tmp_path, monkeypatch, make_ctx, fonts, name, im): + """A synthetic transparent-padded file under /assets/brand/ is drawn uncropped via cli_icon and via override.""" + from ctshowcase import iconbox + skill = tmp_path / "skill" + path = skill / "assets" / "brand" / name + path.parent.mkdir(parents=True, exist_ok=True) + save_and_reopen(im, path) + ctx = make_ctx() + monkeypatch.setattr(ctx.brand, "root", skill) + assert iconbox.is_bundled_brand_asset(path, ctx.brand.root) + + # CLI route: the bundled file handed in directly as the icon argument. + nodes, info, _ = iconbox.plate(ctx, fonts, CARD, RADIUS, INSET, cli_icon=str(path)) + assert info["source"] == "cli" + found = pics(nodes) + assert len(found) == 1 + assert found[0].image.size == (1024, 1024) + assert found[0].h <= INNER_H + 1 + + # Override route: repo dir is skill/assets and the override icon names brand/ inside it. + ov = make_ctx(repo_dir=skill / "assets") + ov.override = {"icon": f"brand/{name}"} + nodes, info, _ = iconbox.plate(ov, fonts, CARD, RADIUS, INSET) + assert info["source"] == "override" + found = pics(nodes) + assert len(found) == 1 + assert found[0].image.size == (1024, 1024) + assert found[0].h <= INNER_H + 1 + + +def test_synthetic_bundled_brand_png_not_fitted_via_cli_and_override(tmp_path, make_ctx, fonts, monkeypatch): + _assert_bundled_both_routes(tmp_path, monkeypatch, make_ctx, fonts, "synthetic-logo.png", rgba_art()) + _assert_bundled_both_routes(tmp_path, monkeypatch, make_ctx, fonts, "synthetic-palette-logo.png", palette_art()) + + +def test_same_art_outside_bundled_dir_is_still_fitted(tmp_path, make_ctx, fonts): + from ctshowcase import iconbox + path = tmp_path / "custom" / "icon.png" + path.parent.mkdir(parents=True, exist_ok=True) + save_and_reopen(rgba_art(), path) + ctx = make_ctx() + # The guard is path-based: identical art outside /assets/brand/ is still alpha-fitted. + assert not iconbox.is_bundled_brand_asset(path, ctx.brand.root) + + nodes, info, _ = iconbox.plate(ctx, fonts, CARD, RADIUS, INSET, cli_icon=str(path)) + assert info["source"] == "cli" + found = pics(nodes) + assert len(found) == 1 + assert found[0].image.size == BBOX_SIZE + assert_fits_inner_box(found[0]) + + +def test_every_real_bundled_brand_png_untouched_both_routes(make_ctx, fonts): + from ctshowcase import iconbox + ctx = make_ctx() + # Read-only: the real assets/brand PNGs are only opened, never written. + pngs = sorted((ctx.brand.root / "assets" / "brand").glob("*.png")) + assert len(pngs) == 4 + names = {p.name for p in pngs} + assert {"logo.png", "mark-light.png", "logo-dark.png", "logo-dark-full.png"} <= names + + for p in pngs: + with Image.open(p) as src: + expected = src.size + assert iconbox.is_bundled_brand_asset(p, ctx.brand.root) + + # CLI route. Pixel-art or full-bleed cases would be Clip-wrapped; pics() flattens Clip children. + nodes, info, _ = iconbox.plate(ctx, fonts, CARD, RADIUS, INSET, cli_icon=str(p)) + assert info["source"] == "cli" + found = pics(nodes) + assert len(found) == 1, p.name + assert found[0].image.size == expected, p.name + + # Override route: the repo dir is the real assets/ folder and the override icon is brand/. + ov = make_ctx(repo_dir=p.parent.parent) + ov.override = {"icon": f"brand/{p.name}"} + nodes, info, _ = iconbox.plate(ov, fonts, CARD, RADIUS, INSET) + assert info["source"] == "override" + found = pics(nodes) + assert len(found) == 1, p.name + assert found[0].image.size == expected, p.name + + +def test_is_bundled_brand_asset_paths(tmp_path): + from ctshowcase import iconbox + root = tmp_path / "root" + assert iconbox.is_bundled_brand_asset(root / "assets" / "brand" / "a.png", root) is True + assert iconbox.is_bundled_brand_asset(root / "assets" / "icon.png", root) is False + assert iconbox.is_bundled_brand_asset(root / "assets" / "brand-x" / "a.png", root) is False + assert iconbox.is_bundled_brand_asset(tmp_path / "outside" / "assets" / "brand" / "a.png", root) is False + # A traversal that leaves the brand folder resolves to assets/icon.png and is not bundled. + assert iconbox.is_bundled_brand_asset(root / "assets" / "brand" / ".." / "icon.png", root) is False