diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index cb90793e04..e00757cb7b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -12,8 +12,54 @@ on: - main - release/* types: [ labeled, opened, synchronize, reopened ] + jobs: + # Prime a single LFS cache and expose the exact key for the matrix + WarmLFS: + runs-on: ubuntu-latest + outputs: + lfs_key: ${{ steps.expose-key.outputs.lfs_key }} + steps: + - name: Git Config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.longpaths true + + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + # Deterministic list of LFS object IDs, then compute a portable key: + # - `git lfs ls-files -l` lists all tracked LFS objects with their SHA-256 + # - `awk '{print $1}'` extracts just the SHA field + # - `sort` sorts in byte order (hex hashes sort the same everywhere) + # This ensures the file content is identical regardless of OS or locale + - name: Git Create LFS id list + shell: bash + run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id + + - name: Git Expose LFS cache key + id: expose-key + shell: bash + env: + LFS_KEY: lfs-${{ hashFiles('.lfs-assets-id') }}-v1 + run: echo "lfs_key=$LFS_KEY" >> "$GITHUB_OUTPUT" + + - name: Git Setup LFS Cache + uses: actions/cache@v4 + with: + path: .git/lfs + key: ${{ steps.expose-key.outputs.lfs_key }} + + - name: Git Pull LFS + shell: bash + run: git lfs pull + Build: + needs: WarmLFS strategy: matrix: isARM: @@ -69,14 +115,14 @@ jobs: options: os: buildjet-4vcpu-ubuntu-2204-arm - runs-on: ${{matrix.options.os}} + runs-on: ${{ matrix.options.os }} steps: - name: Install libgdi+, which is required for tests running on ubuntu if: ${{ contains(matrix.options.os, 'ubuntu') }} run: | - sudo apt-get update - sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + sudo apt-get update + sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - name: Git Config shell: bash @@ -90,18 +136,15 @@ jobs: fetch-depth: 0 submodules: recursive - # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 - - name: Git Create LFS FileList - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - + # Use the warmed key from WarmLFS. Do not recompute or recreate .lfs-assets-id here. - name: Git Setup LFS Cache uses: actions/cache@v4 - id: lfs-cache with: path: .git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 + key: ${{ needs.WarmLFS.outputs.lfs_key }} - name: Git Pull LFS + shell: bash run: git lfs pull - name: NuGet Install @@ -168,11 +211,8 @@ jobs: Publish: needs: [Build] - runs-on: ubuntu-latest - if: (github.event_name == 'push') - steps: - name: Git Config shell: bash @@ -213,4 +253,3 @@ jobs: run: | dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate - diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index b4965795c3..a7278a8175 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -4,6 +4,7 @@ on: schedule: # 2AM every Tuesday/Thursday - cron: "0 2 * * 2,4" + jobs: Build: strategy: @@ -14,15 +15,14 @@ jobs: runtime: -x64 codecov: true - runs-on: ${{matrix.options.os}} + runs-on: ${{ matrix.options.os }} steps: - - name: Install libgdi+, which is required for tests running on ubuntu if: ${{ contains(matrix.options.os, 'ubuntu') }} run: | - sudo apt-get update - sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + sudo apt-get update + sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - name: Git Config shell: bash @@ -36,16 +36,21 @@ jobs: fetch-depth: 0 submodules: recursive - # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 - - name: Git Create LFS FileList - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + # Deterministic list of LFS object IDs, then compute a portable key: + # - `git lfs ls-files -l` lists all tracked LFS objects with their SHA-256 + # - `awk '{print $1}'` extracts just the SHA field + # - `sort` sorts in byte order (hex hashes sort the same everywhere) + # This ensures the file content is identical regardless of OS or locale + - name: Git Create LFS id list + shell: bash + run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id - name: Git Setup LFS Cache uses: actions/cache@v4 id: lfs-cache with: path: .git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 + key: lfs-${{ hashFiles('.lfs-assets-id') }}-v1 - name: Git Pull LFS run: git lfs pull @@ -69,13 +74,13 @@ jobs: - name: DotNet Build shell: pwsh - run: ./ci-build.ps1 "${{matrix.options.framework}}" + run: ./ci-build.ps1 "${{ matrix.options.framework }}" env: SIXLABORS_TESTING: True - name: DotNet Test shell: pwsh - run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" + run: ./ci-test.ps1 "${{ matrix.options.os }}" "${{ matrix.options.framework }}" "${{ matrix.options.runtime }}" "${{ matrix.options.codecov }}" env: SIXLABORS_TESTING: True XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit diff --git a/src/ImageSharp/Color/Color.cs b/src/ImageSharp/Color/Color.cs index 1dfbf0a243..bb78dcbbc3 100644 --- a/src/ImageSharp/Color/Color.cs +++ b/src/ImageSharp/Color/Color.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; @@ -126,66 +127,91 @@ public static void FromPixel(ReadOnlySpan source, Span de } /// - /// Creates a new instance of the struct - /// from the given hexadecimal string. + /// Gets a from the given hexadecimal string. /// /// - /// The hexadecimal representation of the combined color components arranged - /// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. + /// The hexadecimal representation of the combined color components. + /// + /// + /// The format of the hexadecimal string to parse, if applicable. Defaults to . /// /// - /// The . + /// The equivalent of the hexadecimal input. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Color ParseHex(string hex) + /// + /// Thrown when the is not in the correct format. + /// + public static Color ParseHex(string hex, ColorHexFormat format = ColorHexFormat.Rgba) { - Rgba32 rgba = Rgba32.ParseHex(hex); - return FromPixel(rgba); + Guard.NotNull(hex, nameof(hex)); + + if (!TryParseHex(hex, out Color color, format)) + { + throw new ArgumentException("Hexadecimal string is not in the correct format.", nameof(hex)); + } + + return color; } /// - /// Attempts to creates a new instance of the struct - /// from the given hexadecimal string. + /// Gets a from the given hexadecimal string. /// /// - /// The hexadecimal representation of the combined color components arranged - /// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. + /// The hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// + /// The format of the hexadecimal string to parse, if applicable. Defaults to . /// - /// When this method returns, contains the equivalent of the hexadecimal input. /// - /// The . + /// if the parsing was successful; otherwise, . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryParseHex(string hex, out Color result) + public static bool TryParseHex(string hex, out Color result, ColorHexFormat format = ColorHexFormat.Rgba) { result = default; - if (Rgba32.TryParseHex(hex, out Rgba32 rgba)) + if (format == ColorHexFormat.Argb) { - result = FromPixel(rgba); - return true; + if (TryParseArgbHex(hex, out Argb32 argb)) + { + result = FromPixel(argb); + return true; + } + } + else if (format == ColorHexFormat.Rgba) + { + if (TryParseRgbaHex(hex, out Rgba32 rgba)) + { + result = FromPixel(rgba); + return true; + } } return false; } /// - /// Creates a new instance of the struct - /// from the given input string. + /// Gets a from the given input string. /// /// - /// The name of the color or the hexadecimal representation of the combined color components arranged - /// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. + /// The name of the color or the hexadecimal representation of the combined color components. + /// + /// + /// The format of the hexadecimal string to parse, if applicable. Defaults to . /// /// - /// The . + /// The equivalent of the input string. /// - /// Input string is not in the correct format. - public static Color Parse(string input) + /// + /// Thrown when the is not in the correct format. + /// + public static Color Parse(string input, ColorHexFormat format = ColorHexFormat.Rgba) { Guard.NotNull(input, nameof(input)); - if (!TryParse(input, out Color color)) + if (!TryParse(input, out Color color, format)) { throw new ArgumentException("Input string is not in the correct format.", nameof(input)); } @@ -194,18 +220,21 @@ public static Color Parse(string input) } /// - /// Attempts to creates a new instance of the struct - /// from the given input string. + /// Tries to create a new instance of the struct from the given input string. /// /// - /// The name of the color or the hexadecimal representation of the combined color components arranged - /// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. + /// The name of the color or the hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the input string. + /// + /// + /// The format of the hexadecimal string to parse, if applicable. Defaults to . /// - /// When this method returns, contains the equivalent of the hexadecimal input. /// - /// The . + /// if the parsing was successful; otherwise, . /// - public static bool TryParse(string input, out Color result) + public static bool TryParse(string input, out Color result, ColorHexFormat format = ColorHexFormat.Rgba) { result = default; @@ -219,7 +248,13 @@ public static bool TryParse(string input, out Color result) return true; } - return TryParseHex(input, out result); + result = default; + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + return TryParseHex(input, out result, format); } /// @@ -227,6 +262,7 @@ public static bool TryParse(string input, out Color result) /// /// The new value of alpha [0..1]. /// The color having it's alpha channel altered. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Color WithAlpha(float alpha) { Vector4 v = this.ToScaledVector4(); @@ -235,22 +271,32 @@ public Color WithAlpha(float alpha) } /// - /// Gets the hexadecimal representation of the color instance in rrggbbaa form. + /// Gets the hexadecimal string representation of the color instance. /// + /// + /// The format of the hexadecimal string to return. Defaults to . + /// /// A hexadecimal string representation of the value. + /// Thrown when the is not supported. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ToHex() + public string ToHex(ColorHexFormat format = ColorHexFormat.Rgba) { - if (this.boxedHighPrecisionPixel is not null) + Rgba32 rgba = (this.boxedHighPrecisionPixel is not null) + ? this.boxedHighPrecisionPixel.ToRgba32() + : Rgba32.FromScaledVector4(this.data); + + uint hexOrder = format switch { - return this.boxedHighPrecisionPixel.ToRgba32().ToHex(); - } + ColorHexFormat.Argb => (uint)((rgba.B << 0) | (rgba.G << 8) | (rgba.R << 16) | (rgba.A << 24)), + ColorHexFormat.Rgba => (uint)((rgba.A << 0) | (rgba.B << 8) | (rgba.G << 16) | (rgba.R << 24)), + _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported color hex format.") + }; - return Rgba32.FromScaledVector4(this.data).ToHex(); + return hexOrder.ToString("X8", CultureInfo.InvariantCulture); } /// - public override string ToString() => this.ToHex(); + public override string ToString() => this.ToHex(ColorHexFormat.Rgba); /// /// Converts the color instance to a specified type. @@ -336,4 +382,241 @@ public override int GetHashCode() return this.boxedHighPrecisionPixel.GetHashCode(); } + + /// + /// Gets the hexadecimal string representation of the color instance in the format RRGGBBAA. + /// + /// + /// The hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// + /// if the parsing was successful; otherwise, . + /// + private static bool TryParseRgbaHex(string? hex, out Rgba32 result) + { + result = default; + + if (!TryConvertToRgbaUInt32(hex, out uint packedValue)) + { + return false; + } + + result = Unsafe.As(ref packedValue); + return true; + } + + /// + /// Gets the hexadecimal string representation of the color instance in the format AARRGGBB. + /// + /// + /// The hexadecimal representation of the combined color components. + /// + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// + /// if the parsing was successful; otherwise, . + /// + private static bool TryParseArgbHex(string? hex, out Argb32 result) + { + result = default; + + if (!TryConvertToArgbUInt32(hex, out uint packedValue)) + { + return false; + } + + result = Unsafe.As(ref packedValue); + return true; + } + + private static bool TryConvertToRgbaUInt32(string? value, out uint result) + { + result = default; + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + ReadOnlySpan hex = value.AsSpan(); + + if (hex[0] == '#') + { + hex = hex[1..]; + } + + byte a = 255, r, g, b; + + switch (hex.Length) + { + case 8: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b) || + !TryParseByte(hex[6], hex[7], out a)) + { + return false; + } + + break; + + case 6: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b)) + { + return false; + } + + break; + + case 4: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b) || + !TryExpand(hex[3], out a)) + { + return false; + } + + break; + + case 3: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b)) + { + return false; + } + + break; + + default: + return false; + } + + result = (uint)(r | (g << 8) | (b << 16) | (a << 24)); // RGBA layout + return true; + } + + private static bool TryConvertToArgbUInt32(string? value, out uint result) + { + result = default; + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + ReadOnlySpan hex = value.AsSpan(); + + if (hex[0] == '#') + { + hex = hex[1..]; + } + + byte a = 255, r, g, b; + + switch (hex.Length) + { + case 8: + if (!TryParseByte(hex[0], hex[1], out a) || + !TryParseByte(hex[2], hex[3], out r) || + !TryParseByte(hex[4], hex[5], out g) || + !TryParseByte(hex[6], hex[7], out b)) + { + return false; + } + + break; + + case 6: + if (!TryParseByte(hex[0], hex[1], out r) || + !TryParseByte(hex[2], hex[3], out g) || + !TryParseByte(hex[4], hex[5], out b)) + { + return false; + } + + break; + + case 4: + if (!TryExpand(hex[0], out a) || + !TryExpand(hex[1], out r) || + !TryExpand(hex[2], out g) || + !TryExpand(hex[3], out b)) + { + return false; + } + + break; + + case 3: + if (!TryExpand(hex[0], out r) || + !TryExpand(hex[1], out g) || + !TryExpand(hex[2], out b)) + { + return false; + } + + break; + + default: + return false; + } + + result = (uint)((b << 24) | (g << 16) | (r << 8) | a); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryParseByte(char hi, char lo, out byte value) + { + if (TryConvertHexCharToByte(hi, out byte high) && TryConvertHexCharToByte(lo, out byte low)) + { + value = (byte)((high << 4) | low); + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryExpand(char c, out byte value) + { + if (TryConvertHexCharToByte(c, out byte nibble)) + { + value = (byte)((nibble << 4) | nibble); + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryConvertHexCharToByte(char c, out byte value) + { + if ((uint)(c - '0') <= 9) + { + value = (byte)(c - '0'); + return true; + } + + char lower = (char)(c | 0x20); // Normalize to lowercase + + if ((uint)(lower - 'a') <= 5) + { + value = (byte)(lower - 'a' + 10); + return true; + } + + value = 0; + return false; + } } diff --git a/src/ImageSharp/Color/ColorHexFormat.cs b/src/ImageSharp/Color/ColorHexFormat.cs new file mode 100644 index 0000000000..e1cd898c70 --- /dev/null +++ b/src/ImageSharp/Color/ColorHexFormat.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp; + +/// +/// Specifies the channel order when formatting or parsing a color as a hexadecimal string. +/// +public enum ColorHexFormat +{ + /// + /// Uses RRGGBBAA channel order where the red, green, and blue components come first, + /// followed by the alpha component. This matches the CSS Color Module Level 4 and common web standards. + /// + /// When parsing, supports the following formats: + /// + /// #RGB expands to RRGGBBFF (fully opaque) + /// #RGBA expands to RRGGBBAA + /// #RRGGBB expands to RRGGBBFF (fully opaque) + /// #RRGGBBAA used as-is + /// + /// + /// When formatting, outputs an 8-digit hex string in RRGGBBAA order. + /// + Rgba, + + /// + /// Uses AARRGGBB channel order where the alpha component comes first, + /// followed by the red, green, and blue components. This matches the Microsoft/XAML convention. + /// + /// When parsing, supports the following formats: + /// + /// #ARGB expands to AARRGGBB + /// #AARRGGBB used as-is + /// + /// + /// When formatting, outputs an 8-digit hex string in AARRGGBB order. + /// + Argb +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs index 8980700c97..199754c690 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers.Binary; using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; @@ -211,64 +210,6 @@ public uint PackedValue [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(Rgba32 left, Rgba32 right) => !left.Equals(right); - /// - /// Creates a new instance of the struct - /// from the given hexadecimal string. - /// - /// - /// The hexadecimal representation of the combined color components arranged - /// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. - /// - /// - /// The . - /// - /// Hexadecimal string is not in the correct format. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rgba32 ParseHex(string hex) - { - Guard.NotNull(hex, nameof(hex)); - - if (!TryParseHex(hex, out Rgba32 rgba)) - { - throw new ArgumentException("Hexadecimal string is not in the correct format.", nameof(hex)); - } - - return rgba; - } - - /// - /// Attempts to creates a new instance of the struct - /// from the given hexadecimal string. - /// - /// - /// The hexadecimal representation of the combined color components arranged - /// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. - /// - /// When this method returns, contains the equivalent of the hexadecimal input. - /// - /// The . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryParseHex(string? hex, out Rgba32 result) - { - result = default; - if (string.IsNullOrWhiteSpace(hex)) - { - return false; - } - - hex = ToRgbaHex(hex); - - if (hex is null || !uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint packedValue)) - { - return false; - } - - packedValue = BinaryPrimitives.ReverseEndianness(packedValue); - result = Unsafe.As(ref packedValue); - return true; - } - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Rgba32 ToRgba32() => this; @@ -409,41 +350,4 @@ private static Rgba32 Pack(Vector4 vector) Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); return new Rgba32(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); } - - /// - /// Converts the specified hex value to an rrggbbaa hex value. - /// - /// The hex value to convert. - /// - /// A rrggbbaa hex value. - /// - private static string? ToRgbaHex(string hex) - { - if (hex[0] == '#') - { - hex = hex[1..]; - } - - if (hex.Length == 8) - { - return hex; - } - - if (hex.Length == 6) - { - return hex + "FF"; - } - - if (hex.Length is < 3 or > 4) - { - return null; - } - - char a = hex.Length == 3 ? 'F' : hex[3]; - char b = hex[2]; - char g = hex[1]; - char r = hex[0]; - - return new string(new[] { r, r, g, g, b, b, a, a }); - } } diff --git a/tests/ImageSharp.Tests/Color/ColorTests.cs b/tests/ImageSharp.Tests/Color/ColorTests.cs index d430df5b44..c482fc9986 100644 --- a/tests/ImageSharp.Tests/Color/ColorTests.cs +++ b/tests/ImageSharp.Tests/Color/ColorTests.cs @@ -67,9 +67,9 @@ public void Equality_WhenFalse(bool highPrecision) [Theory] [InlineData(false)] [InlineData(true)] - public void ToHex(bool highPrecision) + public void ToHexRgba(bool highPrecision) { - string expected = "ABCD1234"; + const string expected = "AABBCCDD"; Color color = Color.ParseHex(expected); if (highPrecision) @@ -81,10 +81,27 @@ public void ToHex(bool highPrecision) Assert.Equal(expected, actual); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ToHexArgb(bool highPrecision) + { + const string expected = "AABBCCDD"; + Color color = Color.ParseHex(expected, ColorHexFormat.Argb); + + if (highPrecision) + { + color = Color.FromPixel(color.ToPixel()); + } + + string actual = color.ToHex(ColorHexFormat.Argb); + Assert.Equal(expected, actual); + } + [Fact] public void WebSafePalette_IsCorrect() { - Rgba32[] actualPalette = Color.WebSafePalette.ToArray().Select(c => c.ToPixel()).ToArray(); + Rgba32[] actualPalette = [.. Color.WebSafePalette.ToArray().Select(c => c.ToPixel())]; for (int i = 0; i < ReferencePalette.WebSafeColors.Length; i++) { @@ -95,7 +112,7 @@ public void WebSafePalette_IsCorrect() [Fact] public void WernerPalette_IsCorrect() { - Rgba32[] actualPalette = Color.WernerPalette.ToArray().Select(c => c.ToPixel()).ToArray(); + Rgba32[] actualPalette = [.. Color.WernerPalette.ToArray().Select(c => c.ToPixel())]; for (int i = 0; i < ReferencePalette.WernerColors.Length; i++) { @@ -103,7 +120,7 @@ public void WernerPalette_IsCorrect() } } - public class FromHex + public class FromHexRgba { [Fact] public void ShortHex() @@ -126,6 +143,23 @@ public void TryShortHex() Assert.Equal(new Rgba32(0, 0, 0, 255), actual.ToPixel()); } + [Fact] + public void LongHex() + { + Assert.Equal(new Rgba32(255, 255, 255, 0), Color.ParseHex("#FFFFFF00").ToPixel()); + Assert.Equal(new Rgba32(255, 255, 255, 128), Color.ParseHex("#FFFFFF80").ToPixel()); + } + + [Fact] + public void TryLongHex() + { + Assert.True(Color.TryParseHex("#FFFFFF00", out Color actual)); + Assert.Equal(new Rgba32(255, 255, 255, 0), actual.ToPixel()); + + Assert.True(Color.TryParseHex("#FFFFFF80", out actual)); + Assert.Equal(new Rgba32(255, 255, 255, 128), actual.ToPixel()); + } + [Fact] public void LeadingPoundIsOptional() { @@ -152,6 +186,72 @@ public void LeadingPoundIsOptional() public void FalseOnNull() => Assert.False(Color.TryParseHex(null, out Color _)); } + public class FromHexArgb + { + [Fact] + public void ShortHex() + { + Assert.Equal(new Rgb24(255, 255, 255), Color.ParseHex("#fff", ColorHexFormat.Argb).ToPixel()); + Assert.Equal(new Rgb24(255, 255, 255), Color.ParseHex("fff", ColorHexFormat.Argb).ToPixel()); + Assert.Equal(new Argb32(0, 0, 255, 0), Color.ParseHex("000f", ColorHexFormat.Argb).ToPixel()); + } + + [Fact] + public void TryShortHex() + { + Assert.True(Color.TryParseHex("#fff", out Color actual, ColorHexFormat.Argb)); + Assert.Equal(new Rgb24(255, 255, 255), actual.ToPixel()); + + Assert.True(Color.TryParseHex("fff", out actual, ColorHexFormat.Argb)); + Assert.Equal(new Rgb24(255, 255, 255), actual.ToPixel()); + + Assert.True(Color.TryParseHex("000f", out actual, ColorHexFormat.Argb)); + Assert.Equal(new Argb32(0, 0, 255, 0), actual.ToPixel()); + } + + [Fact] + public void LongHex() + { + Assert.Equal(new Argb32(255, 255, 255, 0), Color.ParseHex("#00FFFFFF", ColorHexFormat.Argb).ToPixel()); + Assert.Equal(new Argb32(255, 255, 255, 128), Color.ParseHex("#80FFFFFF", ColorHexFormat.Argb).ToPixel()); + } + + [Fact] + public void TryLongHex() + { + Assert.True(Color.TryParseHex("#00FFFFFF", out Color actual, ColorHexFormat.Argb)); + Assert.Equal(new Argb32(255, 255, 255, 0), actual.ToPixel()); + + Assert.True(Color.TryParseHex("#80FFFFFF", out actual, ColorHexFormat.Argb)); + Assert.Equal(new Argb32(255, 255, 255, 128), actual.ToPixel()); + } + + [Fact] + public void LeadingPoundIsOptional() + { + Assert.Equal(new Rgb24(0, 128, 128), Color.ParseHex("#008080", ColorHexFormat.Argb).ToPixel()); + Assert.Equal(new Rgb24(0, 128, 128), Color.ParseHex("008080", ColorHexFormat.Argb).ToPixel()); + } + + [Fact] + public void ThrowsOnEmpty() => Assert.Throws(() => Color.ParseHex(string.Empty, ColorHexFormat.Argb)); + + [Fact] + public void ThrowsOnInvalid() => Assert.Throws(() => Color.ParseHex("!", ColorHexFormat.Argb)); + + [Fact] + public void ThrowsOnNull() => Assert.Throws(() => Color.ParseHex(null, ColorHexFormat.Argb)); + + [Fact] + public void FalseOnEmpty() => Assert.False(Color.TryParseHex(string.Empty, out Color _, ColorHexFormat.Argb)); + + [Fact] + public void FalseOnInvalid() => Assert.False(Color.TryParseHex("!", out Color _, ColorHexFormat.Argb)); + + [Fact] + public void FalseOnNull() => Assert.False(Color.TryParseHex(null, out Color _, ColorHexFormat.Argb)); + } + public class FromString { [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/Rgba32Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Rgba32Tests.cs index 6d56185ecf..3bb1fead1c 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Rgba32Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/Rgba32Tests.cs @@ -21,10 +21,10 @@ public void AreEqual() { Rgba32 color1 = new(0, 0, 0); Rgba32 color2 = new(0, 0, 0, 1F); - Rgba32 color3 = Rgba32.ParseHex("#000"); - Rgba32 color4 = Rgba32.ParseHex("#000F"); - Rgba32 color5 = Rgba32.ParseHex("#000000"); - Rgba32 color6 = Rgba32.ParseHex("#000000FF"); + Rgba32 color3 = Color.ParseHex("#000").ToPixel(); + Rgba32 color4 = Color.ParseHex("#000F").ToPixel(); + Rgba32 color5 = Color.ParseHex("#000000").ToPixel(); + Rgba32 color6 = Color.ParseHex("#000000FF").ToPixel(); Assert.Equal(color1, color2); Assert.Equal(color1, color3); @@ -41,9 +41,9 @@ public void AreNotEqual() { Rgba32 color1 = new(255, 0, 0, 255); Rgba32 color2 = new(0, 0, 0, 255); - Rgba32 color3 = Rgba32.ParseHex("#000"); - Rgba32 color4 = Rgba32.ParseHex("#000000"); - Rgba32 color5 = Rgba32.ParseHex("#FF000000"); + Rgba32 color3 = Color.ParseHex("#000").ToPixel(); + Rgba32 color4 = Color.ParseHex("#000000").ToPixel(); + Rgba32 color5 = Color.ParseHex("#FF000000").ToPixel(); Assert.NotEqual(color1, color2); Assert.NotEqual(color1, color3); @@ -82,30 +82,6 @@ public void ConstructorAssignsProperties() Assert.Equal(Math.Round(.5f * 255), color5.A); } - /// - /// Tests whether FromHex and ToHex work correctly. - /// - [Fact] - public void FromAndToHex() - { - // 8 digit hex matches css4 spec. RRGGBBAA - Rgba32 color = Rgba32.ParseHex("#AABBCCDD"); // 170, 187, 204, 221 - Assert.Equal(170, color.R); - Assert.Equal(187, color.G); - Assert.Equal(204, color.B); - Assert.Equal(221, color.A); - - Assert.Equal("AABBCCDD", color.ToHex()); - - color.R = 0; - - Assert.Equal("00BBCCDD", color.ToHex()); - - color.A = 255; - - Assert.Equal("00BBCCFF", color.ToHex()); - } - /// /// Tests that the individual byte elements are laid out in RGBA order. /// diff --git a/tests/ImageSharp.Tests/PixelFormats/UnPackedPixelTests.cs b/tests/ImageSharp.Tests/PixelFormats/UnPackedPixelTests.cs index 651f6fe7f8..eb93ba230e 100644 --- a/tests/ImageSharp.Tests/PixelFormats/UnPackedPixelTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/UnPackedPixelTests.cs @@ -60,7 +60,7 @@ public void Color_Types_From_Vector3_Produce_Equal_Scaled_Component_OutPut() [Fact] public void Color_Types_From_Hex_Produce_Equal_Scaled_Component_OutPut() { - Rgba32 color = Rgba32.ParseHex("183060C0"); + Rgba32 color = Color.ParseHex("183060C0").ToPixel(); RgbaVector colorVector = RgbaVector.FromHex("183060C0"); Assert.Equal(color.R, (byte)(colorVector.R * 255));