Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions IconHelper.Test/ArgumentsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> errors);

Assert.IsFalse(valid);
Assert.HasCount(1, errors);
Assert.Contains("is fully transparent", errors[0]);
}

[TestMethod]
public void ValidateReportsEveryProblemAtOnce()
{
Expand Down
29 changes: 28 additions & 1 deletion IconHelper.Test/ColorParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
}
16 changes: 16 additions & 0 deletions IconHelper.Test/ProcessDirectoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
6 changes: 5 additions & 1 deletion IconHelper/Arguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ internal bool Validate(out Collection<string> 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}.");
}
Expand Down
34 changes: 29 additions & 5 deletions IconHelper/ColorParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace ktsu.IconHelper;

using System.Linq;

using ktsu.Semantics.Color;

using Color = ktsu.Semantics.Color.Color;
Expand All @@ -14,9 +16,36 @@ internal static class ColorParser
/// <summary>
/// Accepts a name from <see cref="NamedColors"/>, case insensitively, or a hex string in
/// <c>#RGB</c>, <c>#RRGGBB</c> or <c>#RRGGBBAA</c> form. The leading <c>#</c> is optional.
/// A fully transparent colour is rejected, see <see cref="IsFullyTransparent"/>.
/// </summary>
/// <returns><see langword="true"/> when the value was understood.</returns>
internal static bool TryParse(string? value, out Color color)
{
if (!TryParseAnyAlpha(value, out color) || IsTransparent(color))
{
color = default;
return false;
}

return true;
}

/// <summary>
/// Whether the value names a colour whose alpha is zero, such as <c>transparent</c> or
/// <c>#FF880000</c>. 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.
/// </summary>
internal static bool IsFullyTransparent(string? value) =>
TryParseAnyAlpha(value, out Color color) && IsTransparent(color);

/// <summary>
/// The colour names understood by <see cref="TryParse"/>, for use in error messages.
/// </summary>
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;

Expand Down Expand Up @@ -47,9 +76,4 @@ internal static bool TryParse(string? value, out Color color)
return false;
}
}

/// <summary>
/// The colour names understood by <see cref="TryParse"/>, for use in error messages.
/// </summary>
internal static string KnownNames => string.Join(", ", NamedColors.All.Keys);
}
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading