1.5 PaletteEntry value type
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
118
src/FrameProcessor/Domain/PaletteEntry.cs
Normal file
118
src/FrameProcessor/Domain/PaletteEntry.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace FrameProcessor.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// One palette entry mapping a display color (what the eye sees on the panel) to
|
||||
/// a device color (the RGB value firmware expects in the input PNG).
|
||||
/// Hex strings from configuration are parsed once into <see cref="Color"/> values;
|
||||
/// the entry does not retain the original strings.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(PaletteEntryJsonConverter))]
|
||||
public sealed record PaletteEntry(string Name, Color DisplayColor, Color DeviceColor);
|
||||
|
||||
internal sealed class PaletteEntryJsonConverter : JsonConverter<PaletteEntry>
|
||||
{
|
||||
public override PaletteEntry Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
throw new JsonException("Palette entry must be a JSON object.");
|
||||
}
|
||||
|
||||
string? name = null;
|
||||
Color? displayColor = null;
|
||||
Color? deviceColor = null;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
{
|
||||
if (name is null)
|
||||
{
|
||||
throw new JsonException("Palette entry is missing required field 'name'.");
|
||||
}
|
||||
|
||||
if (displayColor is null)
|
||||
{
|
||||
throw new JsonException("Palette entry is missing required field 'color'.");
|
||||
}
|
||||
|
||||
if (deviceColor is null)
|
||||
{
|
||||
throw new JsonException("Palette entry is missing required field 'deviceColor'.");
|
||||
}
|
||||
|
||||
return new PaletteEntry(name, displayColor.Value, deviceColor.Value);
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
{
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
var propertyName = reader.GetString();
|
||||
if (!reader.Read())
|
||||
{
|
||||
throw new JsonException("Unexpected end of palette entry.");
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case "name":
|
||||
var n = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(n))
|
||||
{
|
||||
throw new JsonException("Palette entry 'name' must be a non-empty string.");
|
||||
}
|
||||
|
||||
name = n;
|
||||
break;
|
||||
case "color":
|
||||
displayColor = ParseHex(reader.GetString(), "color");
|
||||
break;
|
||||
case "deviceColor":
|
||||
deviceColor = ParseHex(reader.GetString(), "deviceColor");
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new JsonException("Unexpected end of palette entry.");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, PaletteEntry value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", value.Name);
|
||||
writer.WriteString("color", ToRgbHex(value.DisplayColor));
|
||||
writer.WriteString("deviceColor", ToRgbHex(value.DeviceColor));
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static Color ParseHex(string? s, string field)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
throw new JsonException($"Palette entry '{field}' must be a non-empty hex string.");
|
||||
}
|
||||
|
||||
if (!Color.TryParseHex(s, out var color))
|
||||
{
|
||||
throw new JsonException($"'{s}' is not a valid hex color for '{field}'. Expected '#RRGGBB'.");
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
private static string ToRgbHex(Color color)
|
||||
{
|
||||
// ImageSharp's Color.ToHex() returns 8 characters (RRGGBBAA); SPEC writes 6-digit '#RRGGBB'.
|
||||
var hex = color.ToHex();
|
||||
return "#" + hex[..6];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user