From 0e757e4193c9ac5c1418f40958ca800cba3c1699 Mon Sep 17 00:00:00 2001 From: Fritiof Hedman Date: Sun, 7 Jun 2026 14:32:36 +0200 Subject: [PATCH] 3.1 PaletteFactory Co-Authored-By: Claude Opus 4.7 (1M context) --- IMPLEMENTATION.md | 2 +- .../ImagePipeline/PaletteFactory.cs | 38 +++++++++++ .../PaletteFactoryTests.cs | 66 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/FrameProcessor/ImagePipeline/PaletteFactory.cs create mode 100644 tests/FrameProcessor.Tests/PaletteFactoryTests.cs diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 3d135f4..b6b3a56 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -96,7 +96,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor. ## Phase 3 — Image pipeline -### [ ] 3.1 `PaletteFactory` +### [x] 3.1 `PaletteFactory` - `static ReadOnlyMemory BuildDisplay(IReadOnlyList)` and `BuildDevice(...)`. - **Tests:** `PaletteFactoryTests` — count, ordering preserved. diff --git a/src/FrameProcessor/ImagePipeline/PaletteFactory.cs b/src/FrameProcessor/ImagePipeline/PaletteFactory.cs new file mode 100644 index 0000000..1683798 --- /dev/null +++ b/src/FrameProcessor/ImagePipeline/PaletteFactory.cs @@ -0,0 +1,38 @@ +using FrameProcessor.Domain; +using SixLabors.ImageSharp; + +namespace FrameProcessor.ImagePipeline; + +/// +/// Builds ImageSharp palettes from a frame's 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. +/// +public static class PaletteFactory +{ + public static ReadOnlyMemory BuildDisplay(IReadOnlyList 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 BuildDevice(IReadOnlyList 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; + } +} diff --git a/tests/FrameProcessor.Tests/PaletteFactoryTests.cs b/tests/FrameProcessor.Tests/PaletteFactoryTests.cs new file mode 100644 index 0000000..c7ac41e --- /dev/null +++ b/tests/FrameProcessor.Tests/PaletteFactoryTests.cs @@ -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 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()).Length); + Assert.Equal(0, PaletteFactory.BuildDevice(Array.Empty()).Length); + } + + [Fact] + public void Build_NullEntries_Throws() + { + Assert.Throws(() => PaletteFactory.BuildDisplay(null!)); + Assert.Throws(() => PaletteFactory.BuildDevice(null!)); + } +}