diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs index b74f5db718..0e8681c429 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs @@ -1,6 +1,8 @@ // 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; using System.Runtime.InteropServices; @@ -165,6 +167,64 @@ public uint PackedValue [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(Argb32 left, Argb32 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, argb, rrggbb, or aarrggbb format. + /// + /// + /// The . + /// + /// Hexadecimal string is not in the correct format. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 ParseHex(string hex) + { + Guard.NotNull(hex, nameof(hex)); + + if (!TryParseHex(hex, out Argb32 rgba)) + { + throw new ArgumentException("Hexadecimal string is not in the correct format.", nameof(hex)); + } + + return rgba; + } + + /// + /// Attempts to create a new instance of the struct + /// from the given hexadecimal string. + /// + /// + /// The hexadecimal representation of the combined color components arranged + /// in rgb, argb, rrggbb, or aarrggbb format. + /// + /// When this method returns, contains the equivalent of the hexadecimal input. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseHex(string? hex, out Argb32 result) + { + result = default; + if (string.IsNullOrWhiteSpace(hex)) + { + return false; + } + + ReadOnlySpan hexSpan = ToArgbHex(hex); + + if (!uint.TryParse(hexSpan, 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() => Rgba32.FromArgb32(this); @@ -269,6 +329,16 @@ public static Argb32 FromRgba64(Rgba64 source) A = ColorNumerics.From16BitTo8Bit(source.A) }; + /// + /// Converts the value of this instance to a hexadecimal string. The format is AARRGGBB. + /// + /// A hexadecimal string representation of the value. + public readonly string ToHex() + { + uint hexOrder = (uint)((this.B << 0) | (this.G << 8) | (this.R << 16) | (this.A << 24)); + return hexOrder.ToString("X8", CultureInfo.InvariantCulture); + } + /// public override readonly bool Equals(object? obj) => obj is Argb32 argb32 && this.Equals(argb32); @@ -298,4 +368,46 @@ private static Argb32 Pack(Vector4 vector) Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); return new Argb32(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); } + + /// + /// Converts the specified hex value to an aarrggbb hex value. + /// + /// The hex value to convert. + /// + /// A aarrggbb hex value. + /// + private static ReadOnlySpan ToArgbHex(string hexText) + { + if (hexText.Length == 8) + { + return hexText; + } + + ReadOnlySpan hex = hexText.AsSpan(); + if (hex[0] == '#') + { + hex = hex[1..]; + } + + if (hex.Length == 8) + { + return hex; + } + + if (hex.Length == 6) + { + return $"FF{hex}"; + } + + if (hex.Length is < 3 or > 4) + { + return null; + } + + var (a, r, g, b) = hex.Length == 3 + ? ('F', hex[0], hex[1], hex[2]) + : (hex[0], hex[1], hex[2], hex[3]); + + return new string(new[] { a, a, r, r, g, g, b, b }); + } } diff --git a/tests/ImageSharp.Tests/PixelFormats/Argb32Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Argb32Tests.cs index bcaf9265a3..967fca611b 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Argb32Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/Argb32Tests.cs @@ -20,9 +20,18 @@ public void AreEqual() Argb32 color2 = new(new Vector4(0.0f)); Argb32 color3 = new(new Vector4(1f, 0.0f, 1f, 1f)); Argb32 color4 = new(1f, 0.0f, 1f, 1f); + Argb32 color5 = new(0, 0, 0, 1F); + Argb32 color6 = Argb32.ParseHex("#000"); + Argb32 color7 = Argb32.ParseHex("#F000"); + Argb32 color8 = Argb32.ParseHex("#000000"); + Argb32 color9 = Argb32.ParseHex("#FF000000"); Assert.Equal(color1, color2); Assert.Equal(color3, color4); + Assert.Equal(color5, color6); + Assert.Equal(color5, color7); + Assert.Equal(color5, color8); + Assert.Equal(color5, color9); } /// @@ -71,6 +80,94 @@ 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. AARRGGBB + Argb32 color = Argb32.ParseHex("#AABBCCDD"); + Assert.Equal(0xAA, color.A); + Assert.Equal(0xBB, color.R); + Assert.Equal(0xCC, color.G); + Assert.Equal(0xDD, color.B); + + Assert.Equal("AABBCCDD", color.ToHex()); + + color.R = 0; + + Assert.Equal("AA00CCDD", color.ToHex()); + + color.A = 255; + + Assert.Equal("FF00CCDD", color.ToHex()); + } + + [Fact] + public void Argb32_TryParseHex() + { + Assert.True(Argb32.TryParseHex("000", out Argb32 color)); + Assert.Equal("FF000000", color.ToHex()); + + Assert.True(Argb32.TryParseHex("#000", out color)); + Assert.Equal("FF000000", color.ToHex()); + + Assert.True(Argb32.TryParseHex("0000", out color)); + Assert.Equal("00000000", color.ToHex()); + + Assert.True(Argb32.TryParseHex("#0000", out color)); + Assert.Equal("00000000", color.ToHex()); + + Assert.True(Argb32.TryParseHex("000000", out color)); + Assert.Equal("FF000000", color.ToHex()); + + Assert.True(Argb32.TryParseHex("#000000", out color)); + Assert.Equal("FF000000", color.ToHex()); + + Assert.True(Argb32.TryParseHex("abc", out color)); + Assert.Equal("FFAABBCC", color.ToHex()); + + Assert.True(Argb32.TryParseHex("ABC", out color)); + Assert.Equal("FFAABBCC", color.ToHex()); + + Assert.True(Argb32.TryParseHex("#ABC", out color)); + Assert.Equal("FFAABBCC", color.ToHex()); + + Assert.True(Argb32.TryParseHex("ABCD", out color)); + Assert.Equal("AABBCCDD", color.ToHex()); + + Assert.True(Argb32.TryParseHex("#ABCD", out color)); + Assert.Equal("AABBCCDD", color.ToHex()); + + Assert.True(Argb32.TryParseHex("AABBCC", out color)); + Assert.Equal("FFAABBCC", color.ToHex()); + + Assert.True(Argb32.TryParseHex("#AABBCC", out color)); + Assert.Equal("FFAABBCC", color.ToHex()); + + Assert.True(Argb32.TryParseHex("AABBCCDD", out color)); + Assert.Equal("AABBCCDD", color.ToHex()); + + Assert.True(Argb32.TryParseHex("#AABBCCDD", out color)); + Assert.Equal("AABBCCDD", color.ToHex()); + + Assert.False(Argb32.TryParseHex(string.Empty, out _)); + Assert.False(Argb32.TryParseHex(null, out _)); + Assert.False(Argb32.TryParseHex("A", out _)); + Assert.False(Argb32.TryParseHex("#A", out _)); + Assert.False(Argb32.TryParseHex("AB", out _)); + Assert.False(Argb32.TryParseHex("#AB", out _)); + Assert.False(Argb32.TryParseHex("ABCDE", out _)); + Assert.False(Argb32.TryParseHex("#ABCDE", out _)); + Assert.False(Argb32.TryParseHex("ABCDEFG", out _)); + Assert.False(Argb32.TryParseHex("#ABCDEFG", out _)); + Assert.False(Argb32.TryParseHex("ABCD123", out _)); + Assert.False(Argb32.TryParseHex("#ABCD123", out _)); + Assert.False(Argb32.TryParseHex("ABCD12345", out _)); + Assert.False(Argb32.TryParseHex("#ABCD12345", out _)); + } + [Fact] public void Argb32_PackedValue() {