From 77fe0428e0d19a660631d6858c1abd43676501b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 12:25:18 +0000 Subject: [PATCH] Reject a fully transparent --color instead of painting icons black [patch] The output alpha carries the coverage, so the colour's own alpha is discarded. `transparent` (#00000000) and any #RRGGBB00 therefore produced opaque icons in their RGB, solid black for `transparent`, and exited 0. ColorParser.TryParse now rejects a colour whose alpha is zero, KnownNames no longer lists `transparent`, and Validate reports a dedicated message so `iconhelper -c transparent` exits 1 and says why. README updated to match. Fixes ktsu-dev/IconHelper#158 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3n7q9yLquJ2SmJTtZ --- IconHelper.Test/ArgumentsTests.cs | 16 +++++++++++ IconHelper.Test/ColorParserTests.cs | 29 +++++++++++++++++++- IconHelper.Test/ProcessDirectoryTests.cs | 16 +++++++++++ IconHelper/Arguments.cs | 6 ++++- IconHelper/ColorParser.cs | 34 ++++++++++++++++++++---- README.md | 8 +++--- 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/IconHelper.Test/ArgumentsTests.cs b/IconHelper.Test/ArgumentsTests.cs index af801bb..6fba360 100644 --- a/IconHelper.Test/ArgumentsTests.cs +++ b/IconHelper.Test/ArgumentsTests.cs @@ -273,6 +273,22 @@ public void ValidateRejectsAnUnparseableColor() Assert.Contains("is not a color", errors[0]); } + [TestMethod] + [DataRow("transparent")] + [DataRow("#FF880000")] + public void ValidateRejectsAFullyTransparentColor(string color) + { + using TempDirectory temp = new(); + Arguments args = ValidArguments(temp); + args.Color = color; + + bool valid = args.Validate(out Collection errors); + + Assert.IsFalse(valid); + Assert.HasCount(1, errors); + Assert.Contains("is fully transparent", errors[0]); + } + [TestMethod] public void ValidateReportsEveryProblemAtOnce() { diff --git a/IconHelper.Test/ColorParserTests.cs b/IconHelper.Test/ColorParserTests.cs index 2eaf3bf..aa999b7 100644 --- a/IconHelper.Test/ColorParserTests.cs +++ b/IconHelper.Test/ColorParserTests.cs @@ -84,6 +84,33 @@ public void KnownNamesAreListedForErrorMessages() // The keys are stored lower case, and lookup is case insensitive. Assert.Contains("white", ColorParser.KnownNames); Assert.Contains("black", ColorParser.KnownNames); - Assert.Contains("transparent", ColorParser.KnownNames); + } + + [TestMethod] + public void KnownNamesLeaveOutTransparent() + { + // TryParse rejects it, so listing it would offer a name that validation then refuses. + Assert.DoesNotContain("transparent", ColorParser.KnownNames); + } + + [TestMethod] + [DataRow("transparent")] + [DataRow("Transparent")] + [DataRow("#FF880000")] + [DataRow("00000000")] + public void RejectsFullyTransparentColors(string value) + { + // The output alpha is the coverage and the colour's own alpha is ignored, so a transparent + // colour would paint the icon opaque in its RGB: solid black for "transparent". + Assert.IsFalse(ColorParser.TryParse(value, out _), $"'{value}' should be rejected."); + Assert.IsTrue(ColorParser.IsFullyTransparent(value), $"'{value}' should be reported as fully transparent."); + } + + [TestMethod] + public void AcceptsAColorThatIsOnlyPartlyTransparent() + { + Assert.IsTrue(ColorParser.TryParse("#FF880001", out _)); + Assert.IsFalse(ColorParser.IsFullyTransparent("#FF880001")); + Assert.IsFalse(ColorParser.IsFullyTransparent("not-a-color")); } } diff --git a/IconHelper.Test/ProcessDirectoryTests.cs b/IconHelper.Test/ProcessDirectoryTests.cs index 9a7ddf3..d0944a0 100644 --- a/IconHelper.Test/ProcessDirectoryTests.cs +++ b/IconHelper.Test/ProcessDirectoryTests.cs @@ -228,6 +228,22 @@ public void AnOutputFolderUnderAFileExitsWithInvalidArgumentsInsteadOfThrowing() Assert.AreEqual(IconHelper.ExitInvalidArguments, IconHelper.Execute(ArgumentsFor(input, output))); } + [TestMethod] + public void ExecuteRejectsATransparentColorWithoutWritingIcons() + { + using TempDirectory temp = new(); + string input = temp.Combine("in"); + string output = temp.Combine("out"); + Directory.CreateDirectory(input); + WritePng(Path.Join(input, "icon.png"), 64); + + Arguments args = ArgumentsFor(input, output); + args.Color = "transparent"; + + Assert.AreEqual(IconHelper.ExitInvalidArguments, IconHelper.Execute(args)); + Assert.IsFalse(File.Exists(Path.Join(output, "icon.png")), "No solid black icon should be written."); + } + [TestMethod] public void ExecuteReportsInvalidArgumentsForAMissingInput() { diff --git a/IconHelper/Arguments.cs b/IconHelper/Arguments.cs index 946a67c..3411cf1 100644 --- a/IconHelper/Arguments.cs +++ b/IconHelper/Arguments.cs @@ -77,7 +77,11 @@ internal bool Validate(out Collection errors) errors.Add($"--output must not be the --input directory, or the source icons are overwritten: {(string)output!}"); } - if (!ColorParser.TryParse(Color, out _)) + if (ColorParser.IsFullyTransparent(Color)) + { + errors.Add($"'{Color}' is fully transparent. The output alpha is the icon's coverage, so the color's own alpha is ignored and a transparent color would paint the icon opaque in its RGB instead. Give an opaque color."); + } + else if (!ColorParser.TryParse(Color, out _)) { errors.Add($"'{Color}' is not a color. Use a hex value such as #RRGGBB, #RGB or #RRGGBBAA, or one of: {ColorParser.KnownNames}."); } diff --git a/IconHelper/ColorParser.cs b/IconHelper/ColorParser.cs index 9670875..8db47ae 100644 --- a/IconHelper/ColorParser.cs +++ b/IconHelper/ColorParser.cs @@ -2,6 +2,8 @@ namespace ktsu.IconHelper; +using System.Linq; + using ktsu.Semantics.Color; using Color = ktsu.Semantics.Color.Color; @@ -14,9 +16,36 @@ internal static class ColorParser /// /// Accepts a name from , case insensitively, or a hex string in /// #RGB, #RRGGBB or #RRGGBBAA form. The leading # is optional. + /// A fully transparent colour is rejected, see . /// /// when the value was understood. internal static bool TryParse(string? value, out Color color) + { + if (!TryParseAnyAlpha(value, out color) || IsTransparent(color)) + { + color = default; + return false; + } + + return true; + } + + /// + /// Whether the value names a colour whose alpha is zero, such as transparent or + /// #FF880000. The output alpha carries the coverage and the colour's own alpha is + /// ignored, so such a colour would only contribute its RGB and paint the icon opaque. + /// + internal static bool IsFullyTransparent(string? value) => + TryParseAnyAlpha(value, out Color color) && IsTransparent(color); + + /// + /// The colour names understood by , for use in error messages. + /// + internal static string KnownNames => string.Join(", ", NamedColors.All.Where(named => !IsTransparent(named.Value)).Select(named => named.Key)); + + private static bool IsTransparent(Color color) => color.ToBytes().A == 0; + + private static bool TryParseAnyAlpha(string? value, out Color color) { color = default; @@ -47,9 +76,4 @@ internal static bool TryParse(string? value, out Color color) return false; } } - - /// - /// The colour names understood by , for use in error messages. - /// - internal static string KnownNames => string.Join(", ", NamedColors.All.Keys); } diff --git a/README.md b/README.md index c5ed48e..7a1f2ac 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,11 @@ iconhelper -i ./icons -o ./out -c "orange" ``` The known names are `black`, `white`, `red`, `green`, `blue`, `yellow`, `cyan`, `magenta`, `gray`, -`grey`, `orange`, `purple` and `transparent`, matched case insensitively. Any other colour must be -given as hex. An unrecognised value is rejected by validation with the list of names, rather than -being silently misread. +`grey`, `orange` and `purple`, matched case insensitively. Any other colour must be given as hex. +An unrecognised value is rejected by validation with the list of names, rather than being silently +misread. A fully transparent colour, `transparent` or any hex whose alpha is `00`, is rejected too: +its alpha would be ignored and the icon painted opaque in its RGB, black in the case of +`transparent`. ### Setting a Maximum Size