Skip to content

Add ToHex method to Argb32 #2963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
112 changes: 112 additions & 0 deletions src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -165,6 +167,64 @@ public uint PackedValue
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Argb32 left, Argb32 right) => !left.Equals(right);

/// <summary>
/// Creates a new instance of the <see cref="Argb32"/> struct
/// from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components arranged
/// in rgb, argb, rrggbb, or aarrggbb format.
/// </param>
/// <returns>
/// The <see cref="Argb32"/>.
/// </returns>
/// <exception cref="ArgumentException">Hexadecimal string is not in the correct format.</exception>
[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;
}

/// <summary>
/// Attempts to create a new instance of the <see cref="Argb32"/> struct
/// from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components arranged
/// in rgb, argb, rrggbb, or aarrggbb format.
/// </param>
/// <param name="result">When this method returns, contains the <see cref="Argb32"/> equivalent of the hexadecimal input.</param>
/// <returns>
/// The <see cref="bool"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParseHex(string? hex, out Argb32 result)
{
result = default;
if (string.IsNullOrWhiteSpace(hex))
{
return false;
}

ReadOnlySpan<char> hexSpan = ToArgbHex(hex);

if (!uint.TryParse(hexSpan, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint packedValue))
{
return false;
}

packedValue = BinaryPrimitives.ReverseEndianness(packedValue);
result = Unsafe.As<uint, Argb32>(ref packedValue);
return true;
}

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Rgba32 ToRgba32() => Rgba32.FromArgb32(this);
Expand Down Expand Up @@ -269,6 +329,16 @@ public static Argb32 FromRgba64(Rgba64 source)
A = ColorNumerics.From16BitTo8Bit(source.A)
};

/// <summary>
/// Converts the value of this instance to a hexadecimal string. The format is AARRGGBB.
/// </summary>
/// <returns>A hexadecimal string representation of the value.</returns>
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);
}

/// <inheritdoc/>
public override readonly bool Equals(object? obj) => obj is Argb32 argb32 && this.Equals(argb32);

Expand Down Expand Up @@ -298,4 +368,46 @@ private static Argb32 Pack(Vector4 vector)
Vector128<byte> result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte();
return new Argb32(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12));
}

/// <summary>
/// Converts the specified hex value to an aarrggbb hex value.
/// </summary>
/// <param name="hexText">The hex value to convert.</param>
/// <returns>
/// A aarrggbb hex value.
/// </returns>
private static ReadOnlySpan<char> ToArgbHex(string hexText)
{
if (hexText.Length == 8)
{
return hexText;
}

ReadOnlySpan<char> 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 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allocates more than needed. Instead the string.Create-overload could be used, where you can pass in a state and operate on the string-buffer directly, thus avoiding any intermediate allocation.

Although for .NET 10 the JIT will maybe stack-alloc the intermediate array allocation, but I'm not sure if this is actually the case here.

}
}
97 changes: 97 additions & 0 deletions tests/ImageSharp.Tests/PixelFormats/Argb32Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -71,6 +80,94 @@ public void ConstructorAssignsProperties()
Assert.Equal(Math.Round(.5f * 255), color5.A);
}

/// <summary>
/// Tests whether FromHex and ToHex work correctly.
/// </summary>
[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()
{
Expand Down