1.5 PaletteEntry value type

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:09:15 +02:00
parent 7d9c7ba338
commit fa0081b923
3 changed files with 230 additions and 1 deletions

View 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];
}
}