3.1 PaletteFactory

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:32:36 +02:00
parent 0a936a9f04
commit 0e757e4193
3 changed files with 105 additions and 1 deletions

View File

@@ -96,7 +96,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor.
## Phase 3 — Image pipeline ## Phase 3 — Image pipeline
### [ ] 3.1 `PaletteFactory` ### [x] 3.1 `PaletteFactory`
- `static ReadOnlyMemory<Color> BuildDisplay(IReadOnlyList<PaletteEntry>)` and `BuildDevice(...)`. - `static ReadOnlyMemory<Color> BuildDisplay(IReadOnlyList<PaletteEntry>)` and `BuildDevice(...)`.
- **Tests:** `PaletteFactoryTests` — count, ordering preserved. - **Tests:** `PaletteFactoryTests` — count, ordering preserved.

View File

@@ -0,0 +1,38 @@
using FrameProcessor.Domain;
using SixLabors.ImageSharp;
namespace FrameProcessor.ImagePipeline;
/// <summary>
/// Builds ImageSharp <see cref="Color"/> palettes from a frame's <see cref="PaletteEntry"/> list.
/// The display palette is what we dither *against* (what the eye sees on the panel); the device
/// palette is the set of RGB values firmware expects in the input PNG. Order is preserved in both.
/// </summary>
public static class PaletteFactory
{
public static ReadOnlyMemory<Color> BuildDisplay(IReadOnlyList<PaletteEntry> entries)
{
ArgumentNullException.ThrowIfNull(entries);
var colors = new Color[entries.Count];
for (var i = 0; i < entries.Count; i++)
{
colors[i] = entries[i].DisplayColor;
}
return colors;
}
public static ReadOnlyMemory<Color> BuildDevice(IReadOnlyList<PaletteEntry> entries)
{
ArgumentNullException.ThrowIfNull(entries);
var colors = new Color[entries.Count];
for (var i = 0; i < entries.Count; i++)
{
colors[i] = entries[i].DeviceColor;
}
return colors;
}
}

View File

@@ -0,0 +1,66 @@
using FrameProcessor.Domain;
using FrameProcessor.ImagePipeline;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace FrameProcessor.Tests;
public class PaletteFactoryTests
{
private static readonly IReadOnlyList<PaletteEntry> Spectra6 = new[]
{
new PaletteEntry("black", Color.FromPixel(new Rgba32(0x1F, 0x22, 0x26, 0xFF)), Color.FromPixel(new Rgba32(0x00, 0x00, 0x00, 0xFF))),
new PaletteEntry("white", Color.FromPixel(new Rgba32(0xB9, 0xC7, 0xC9, 0xFF)), Color.FromPixel(new Rgba32(0xFF, 0xFF, 0xFF, 0xFF))),
new PaletteEntry("blue", Color.FromPixel(new Rgba32(0x23, 0x3F, 0x8E, 0xFF)), Color.FromPixel(new Rgba32(0x00, 0x00, 0xFF, 0xFF))),
new PaletteEntry("green", Color.FromPixel(new Rgba32(0x35, 0x56, 0x3A, 0xFF)), Color.FromPixel(new Rgba32(0x00, 0xFF, 0x00, 0xFF))),
new PaletteEntry("red", Color.FromPixel(new Rgba32(0x62, 0x20, 0x1E, 0xFF)), Color.FromPixel(new Rgba32(0xFF, 0x00, 0x00, 0xFF))),
new PaletteEntry("yellow", Color.FromPixel(new Rgba32(0xC1, 0xBB, 0x1E, 0xFF)), Color.FromPixel(new Rgba32(0xFF, 0xFF, 0x00, 0xFF))),
};
[Fact]
public void BuildDisplay_PreservesOrderAndCount()
{
var palette = PaletteFactory.BuildDisplay(Spectra6).ToArray();
Assert.Equal(Spectra6.Count, palette.Length);
for (var i = 0; i < Spectra6.Count; i++)
{
Assert.Equal(Spectra6[i].DisplayColor, palette[i]);
}
}
[Fact]
public void BuildDevice_PreservesOrderAndCount()
{
var palette = PaletteFactory.BuildDevice(Spectra6).ToArray();
Assert.Equal(Spectra6.Count, palette.Length);
for (var i = 0; i < Spectra6.Count; i++)
{
Assert.Equal(Spectra6[i].DeviceColor, palette[i]);
}
}
[Fact]
public void BuildDisplay_AndBuildDevice_DiffWhenColorsDiffer()
{
var display = PaletteFactory.BuildDisplay(Spectra6).ToArray();
var device = PaletteFactory.BuildDevice(Spectra6).ToArray();
Assert.NotEqual(display, device);
}
[Fact]
public void Build_OnEmptyList_ReturnsEmptyPalette()
{
Assert.Equal(0, PaletteFactory.BuildDisplay(Array.Empty<PaletteEntry>()).Length);
Assert.Equal(0, PaletteFactory.BuildDevice(Array.Empty<PaletteEntry>()).Length);
}
[Fact]
public void Build_NullEntries_Throws()
{
Assert.Throws<ArgumentNullException>(() => PaletteFactory.BuildDisplay(null!));
Assert.Throws<ArgumentNullException>(() => PaletteFactory.BuildDevice(null!));
}
}