Browse Source

Merge branch 'main' into js/fix-2974

pull/2975/head
James Jackson-South 6 months ago
committed by GitHub
parent
commit
ef4615348f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 65
      .github/workflows/build-and-test.yml
  2. 25
      .github/workflows/code-coverage.yml
  3. 369
      src/ImageSharp/Color/Color.cs
  4. 40
      src/ImageSharp/Color/ColorHexFormat.cs
  5. 96
      src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs
  6. 110
      tests/ImageSharp.Tests/Color/ColorTests.cs
  7. 38
      tests/ImageSharp.Tests/PixelFormats/Rgba32Tests.cs
  8. 2
      tests/ImageSharp.Tests/PixelFormats/UnPackedPixelTests.cs

65
.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

25
.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

369
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 readonly partial struct Color : IEquatable<Color>
}
/// <summary>
/// Creates a new instance of the <see cref="Color"/> struct
/// from the given hexadecimal string.
/// Gets a <see cref="Color"/> from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// 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.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>
/// The <see cref="Color"/>.
/// The <see cref="Color"/> equivalent of the hexadecimal input.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color ParseHex(string hex)
/// <exception cref="ArgumentException">
/// Thrown when the <paramref name="hex"/> is not in the correct format.
/// </exception>
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;
}
/// <summary>
/// Attempts to creates a new instance of the <see cref="Color"/> struct
/// from the given hexadecimal string.
/// Gets a <see cref="Color"/> from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// 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.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <param name="result">When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.</param>
/// <returns>
/// The <see cref="bool"/>.
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
[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;
}
/// <summary>
/// Creates a new instance of the <see cref="Color"/> struct
/// from the given input string.
/// Gets a <see cref="Color"/> from the given input string.
/// </summary>
/// <param name="input">
/// 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.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>
/// The <see cref="Color"/>.
/// The <see cref="Color"/> equivalent of the input string.
/// </returns>
/// <exception cref="ArgumentException">Input string is not in the correct format.</exception>
public static Color Parse(string input)
/// <exception cref="ArgumentException">
/// Thrown when the <paramref name="input"/> is not in the correct format.
/// </exception>
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 readonly partial struct Color : IEquatable<Color>
}
/// <summary>
/// Attempts to creates a new instance of the <see cref="Color"/> struct
/// from the given input string.
/// Tries to create a new instance of the <see cref="Color"/> struct from the given input string.
/// </summary>
/// <param name="input">
/// 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.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Color"/> equivalent of the input string.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <param name="result">When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.</param>
/// <returns>
/// The <see cref="bool"/>.
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
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 readonly partial struct Color : IEquatable<Color>
return true;
}
return TryParseHex(input, out result);
result = default;
if (string.IsNullOrWhiteSpace(input))
{
return false;
}
return TryParseHex(input, out result, format);
}
/// <summary>
@ -227,6 +262,7 @@ public readonly partial struct Color : IEquatable<Color>
/// </summary>
/// <param name="alpha">The new value of alpha [0..1].</param>
/// <returns>The color having it's alpha channel altered.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Color WithAlpha(float alpha)
{
Vector4 v = this.ToScaledVector4();
@ -235,22 +271,32 @@ public readonly partial struct Color : IEquatable<Color>
}
/// <summary>
/// Gets the hexadecimal representation of the color instance in rrggbbaa form.
/// Gets the hexadecimal string representation of the color instance.
/// </summary>
/// <param name="format">
/// The format of the hexadecimal string to return. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>A hexadecimal string representation of the value.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="format"/> is not supported.</exception>
[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);
}
/// <inheritdoc />
public override string ToString() => this.ToHex();
public override string ToString() => this.ToHex(ColorHexFormat.Rgba);
/// <summary>
/// Converts the color instance to a specified <typeparamref name="TPixel"/> type.
@ -336,4 +382,241 @@ public readonly partial struct Color : IEquatable<Color>
return this.boxedHighPrecisionPixel.GetHashCode();
}
/// <summary>
/// Gets the hexadecimal string representation of the color instance in the format RRGGBBAA.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Rgba32"/> equivalent of the hexadecimal input.
/// </param>
/// <returns>
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryParseRgbaHex(string? hex, out Rgba32 result)
{
result = default;
if (!TryConvertToRgbaUInt32(hex, out uint packedValue))
{
return false;
}
result = Unsafe.As<uint, Rgba32>(ref packedValue);
return true;
}
/// <summary>
/// Gets the hexadecimal string representation of the color instance in the format AARRGGBB.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Argb32"/> equivalent of the hexadecimal input.
/// </param>
/// <returns>
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryParseArgbHex(string? hex, out Argb32 result)
{
result = default;
if (!TryConvertToArgbUInt32(hex, out uint packedValue))
{
return false;
}
result = Unsafe.As<uint, Argb32>(ref packedValue);
return true;
}
private static bool TryConvertToRgbaUInt32(string? value, out uint result)
{
result = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
ReadOnlySpan<char> 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<char> 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;
}
}

40
src/ImageSharp/Color/ColorHexFormat.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp;
/// <summary>
/// Specifies the channel order when formatting or parsing a color as a hexadecimal string.
/// </summary>
public enum ColorHexFormat
{
/// <summary>
/// Uses <c>RRGGBBAA</c> 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.
/// <para>
/// When parsing, supports the following formats:
/// <list type="bullet">
/// <item><description><c>#RGB</c> expands to <c>RRGGBBFF</c> (fully opaque)</description></item>
/// <item><description><c>#RGBA</c> expands to <c>RRGGBBAA</c></description></item>
/// <item><description><c>#RRGGBB</c> expands to <c>RRGGBBFF</c> (fully opaque)</description></item>
/// <item><description><c>#RRGGBBAA</c> used as-is</description></item>
/// </list>
/// </para>
/// When formatting, outputs an 8-digit hex string in <c>RRGGBBAA</c> order.
/// </summary>
Rgba,
/// <summary>
/// Uses <c>AARRGGBB</c> channel order where the alpha component comes first,
/// followed by the red, green, and blue components. This matches the Microsoft/XAML convention.
/// <para>
/// When parsing, supports the following formats:
/// <list type="bullet">
/// <item><description><c>#ARGB</c> expands to <c>AARRGGBB</c></description></item>
/// <item><description><c>#AARRGGBB</c> used as-is</description></item>
/// </list>
/// </para>
/// When formatting, outputs an 8-digit hex string in <c>AARRGGBB</c> order.
/// </summary>
Argb
}

96
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 partial struct Rgba32 : IPixel<Rgba32>, IPackedVector<uint>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Rgba32 left, Rgba32 right) => !left.Equals(right);
/// <summary>
/// Creates a new instance of the <see cref="Rgba32"/> struct
/// from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// </param>
/// <returns>
/// The <see cref="Rgba32"/>.
/// </returns>
/// <exception cref="ArgumentException">Hexadecimal string is not in the correct format.</exception>
[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;
}
/// <summary>
/// Attempts to creates a new instance of the <see cref="Rgba32"/> struct
/// from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// </param>
/// <param name="result">When this method returns, contains the <see cref="Rgba32"/> equivalent of the hexadecimal input.</param>
/// <returns>
/// The <see cref="bool"/>.
/// </returns>
[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<uint, Rgba32>(ref packedValue);
return true;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Rgba32 ToRgba32() => this;
@ -409,41 +350,4 @@ public partial struct Rgba32 : IPixel<Rgba32>, IPackedVector<uint>
Vector128<byte> result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte();
return new Rgba32(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12));
}
/// <summary>
/// Converts the specified hex value to an rrggbbaa hex value.
/// </summary>
/// <param name="hex">The hex value to convert.</param>
/// <returns>
/// A rrggbbaa hex value.
/// </returns>
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 });
}
}

110
tests/ImageSharp.Tests/Color/ColorTests.cs

@ -67,9 +67,9 @@ public partial class ColorTests
[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 partial class ColorTests
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<RgbaDouble>());
}
string actual = color.ToHex(ColorHexFormat.Argb);
Assert.Equal(expected, actual);
}
[Fact]
public void WebSafePalette_IsCorrect()
{
Rgba32[] actualPalette = Color.WebSafePalette.ToArray().Select(c => c.ToPixel<Rgba32>()).ToArray();
Rgba32[] actualPalette = [.. Color.WebSafePalette.ToArray().Select(c => c.ToPixel<Rgba32>())];
for (int i = 0; i < ReferencePalette.WebSafeColors.Length; i++)
{
@ -95,7 +112,7 @@ public partial class ColorTests
[Fact]
public void WernerPalette_IsCorrect()
{
Rgba32[] actualPalette = Color.WernerPalette.ToArray().Select(c => c.ToPixel<Rgba32>()).ToArray();
Rgba32[] actualPalette = [.. Color.WernerPalette.ToArray().Select(c => c.ToPixel<Rgba32>())];
for (int i = 0; i < ReferencePalette.WernerColors.Length; i++)
{
@ -103,7 +120,7 @@ public partial class ColorTests
}
}
public class FromHex
public class FromHexRgba
{
[Fact]
public void ShortHex()
@ -126,6 +143,23 @@ public partial class ColorTests
Assert.Equal(new Rgba32(0, 0, 0, 255), actual.ToPixel<Rgba32>());
}
[Fact]
public void LongHex()
{
Assert.Equal(new Rgba32(255, 255, 255, 0), Color.ParseHex("#FFFFFF00").ToPixel<Rgba32>());
Assert.Equal(new Rgba32(255, 255, 255, 128), Color.ParseHex("#FFFFFF80").ToPixel<Rgba32>());
}
[Fact]
public void TryLongHex()
{
Assert.True(Color.TryParseHex("#FFFFFF00", out Color actual));
Assert.Equal(new Rgba32(255, 255, 255, 0), actual.ToPixel<Rgba32>());
Assert.True(Color.TryParseHex("#FFFFFF80", out actual));
Assert.Equal(new Rgba32(255, 255, 255, 128), actual.ToPixel<Rgba32>());
}
[Fact]
public void LeadingPoundIsOptional()
{
@ -152,6 +186,72 @@ public partial class ColorTests
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<Rgb24>());
Assert.Equal(new Rgb24(255, 255, 255), Color.ParseHex("fff", ColorHexFormat.Argb).ToPixel<Rgb24>());
Assert.Equal(new Argb32(0, 0, 255, 0), Color.ParseHex("000f", ColorHexFormat.Argb).ToPixel<Argb32>());
}
[Fact]
public void TryShortHex()
{
Assert.True(Color.TryParseHex("#fff", out Color actual, ColorHexFormat.Argb));
Assert.Equal(new Rgb24(255, 255, 255), actual.ToPixel<Rgb24>());
Assert.True(Color.TryParseHex("fff", out actual, ColorHexFormat.Argb));
Assert.Equal(new Rgb24(255, 255, 255), actual.ToPixel<Rgb24>());
Assert.True(Color.TryParseHex("000f", out actual, ColorHexFormat.Argb));
Assert.Equal(new Argb32(0, 0, 255, 0), actual.ToPixel<Argb32>());
}
[Fact]
public void LongHex()
{
Assert.Equal(new Argb32(255, 255, 255, 0), Color.ParseHex("#00FFFFFF", ColorHexFormat.Argb).ToPixel<Argb32>());
Assert.Equal(new Argb32(255, 255, 255, 128), Color.ParseHex("#80FFFFFF", ColorHexFormat.Argb).ToPixel<Argb32>());
}
[Fact]
public void TryLongHex()
{
Assert.True(Color.TryParseHex("#00FFFFFF", out Color actual, ColorHexFormat.Argb));
Assert.Equal(new Argb32(255, 255, 255, 0), actual.ToPixel<Argb32>());
Assert.True(Color.TryParseHex("#80FFFFFF", out actual, ColorHexFormat.Argb));
Assert.Equal(new Argb32(255, 255, 255, 128), actual.ToPixel<Argb32>());
}
[Fact]
public void LeadingPoundIsOptional()
{
Assert.Equal(new Rgb24(0, 128, 128), Color.ParseHex("#008080", ColorHexFormat.Argb).ToPixel<Rgb24>());
Assert.Equal(new Rgb24(0, 128, 128), Color.ParseHex("008080", ColorHexFormat.Argb).ToPixel<Rgb24>());
}
[Fact]
public void ThrowsOnEmpty() => Assert.Throws<ArgumentException>(() => Color.ParseHex(string.Empty, ColorHexFormat.Argb));
[Fact]
public void ThrowsOnInvalid() => Assert.Throws<ArgumentException>(() => Color.ParseHex("!", ColorHexFormat.Argb));
[Fact]
public void ThrowsOnNull() => Assert.Throws<ArgumentNullException>(() => 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]

38
tests/ImageSharp.Tests/PixelFormats/Rgba32Tests.cs

@ -21,10 +21,10 @@ public class Rgba32Tests
{
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>();
Rgba32 color4 = Color.ParseHex("#000F").ToPixel<Rgba32>();
Rgba32 color5 = Color.ParseHex("#000000").ToPixel<Rgba32>();
Rgba32 color6 = Color.ParseHex("#000000FF").ToPixel<Rgba32>();
Assert.Equal(color1, color2);
Assert.Equal(color1, color3);
@ -41,9 +41,9 @@ public class Rgba32Tests
{
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>();
Rgba32 color4 = Color.ParseHex("#000000").ToPixel<Rgba32>();
Rgba32 color5 = Color.ParseHex("#FF000000").ToPixel<Rgba32>();
Assert.NotEqual(color1, color2);
Assert.NotEqual(color1, color3);
@ -82,30 +82,6 @@ public class Rgba32Tests
Assert.Equal(Math.Round(.5f * 255), color5.A);
}
/// <summary>
/// Tests whether FromHex and ToHex work correctly.
/// </summary>
[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());
}
/// <summary>
/// Tests that the individual byte elements are laid out in RGBA order.
/// </summary>

2
tests/ImageSharp.Tests/PixelFormats/UnPackedPixelTests.cs

@ -60,7 +60,7 @@ public class UnPackedPixelTests
[Fact]
public void Color_Types_From_Hex_Produce_Equal_Scaled_Component_OutPut()
{
Rgba32 color = Rgba32.ParseHex("183060C0");
Rgba32 color = Color.ParseHex("183060C0").ToPixel<Rgba32>();
RgbaVector colorVector = RgbaVector.FromHex("183060C0");
Assert.Equal(color.R, (byte)(colorVector.R * 255));

Loading…
Cancel
Save