3.3 IImagePipeline + ImagePipeline
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -103,7 +103,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor.
|
|||||||
### [x] 3.2 `DitheringRegistry`
|
### [x] 3.2 `DitheringRegistry`
|
||||||
- `IReadOnlyDictionary<DitherAlgorithm, IDither>` mapping each enum to the corresponding ImageSharp `KnownDitherings.*`.
|
- `IReadOnlyDictionary<DitherAlgorithm, IDither>` mapping each enum to the corresponding ImageSharp `KnownDitherings.*`.
|
||||||
|
|
||||||
### [ ] 3.3 `IImagePipeline` + `ImagePipeline`
|
### [x] 3.3 `IImagePipeline` + `ImagePipeline`
|
||||||
- Single method: `byte[] Process(Stream input, Frame frame)`.
|
- Single method: `byte[] Process(Stream input, Frame frame)`.
|
||||||
- Steps (`SPEC.md` §3.2): decode → resize (cover/center-crop to oriented resolution) → dither against display palette → remap pixels display→device → rotate 90° CW if portrait → encode indexed PNG (`PngColorType.Palette`, smallest bit depth fitting the palette, `PaletteQuantizer(deviceColors)`).
|
- Steps (`SPEC.md` §3.2): decode → resize (cover/center-crop to oriented resolution) → dither against display palette → remap pixels display→device → rotate 90° CW if portrait → encode indexed PNG (`PngColorType.Palette`, smallest bit depth fitting the palette, `PaletteQuantizer(deviceColors)`).
|
||||||
- Output dimensions always match the frame's *declared* `width × height` regardless of orientation.
|
- Output dimensions always match the frame's *declared* `width × height` regardless of orientation.
|
||||||
|
|||||||
13
src/FrameProcessor/ImagePipeline/IImagePipeline.cs
Normal file
13
src/FrameProcessor/ImagePipeline/IImagePipeline.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using FrameProcessor.Domain;
|
||||||
|
|
||||||
|
namespace FrameProcessor.ImagePipeline;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an arbitrary input image into the indexed-PNG bytes a frame's firmware
|
||||||
|
/// expects: resized to the frame's resolution, dithered against the frame's display
|
||||||
|
/// palette, then remapped to the device-color palette.
|
||||||
|
/// </summary>
|
||||||
|
public interface IImagePipeline
|
||||||
|
{
|
||||||
|
byte[] Process(Stream input, Frame frame);
|
||||||
|
}
|
||||||
83
src/FrameProcessor/ImagePipeline/ImagePipeline.cs
Normal file
83
src/FrameProcessor/ImagePipeline/ImagePipeline.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using FrameProcessor.Domain;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Png;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using SixLabors.ImageSharp.Processing.Processors.Quantization;
|
||||||
|
|
||||||
|
namespace FrameProcessor.ImagePipeline;
|
||||||
|
|
||||||
|
public sealed class ImagePipeline : IImagePipeline
|
||||||
|
{
|
||||||
|
public byte[] Process(Stream input, Frame frame)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(input);
|
||||||
|
ArgumentNullException.ThrowIfNull(frame);
|
||||||
|
|
||||||
|
var dither = DitheringRegistry.All[frame.Dithering];
|
||||||
|
var displayPalette = PaletteFactory.BuildDisplay(frame.Palette);
|
||||||
|
var devicePalette = PaletteFactory.BuildDevice(frame.Palette);
|
||||||
|
var oriented = frame.Resolution.ForOrientation(frame.Orientation);
|
||||||
|
|
||||||
|
using var image = Image.Load<Rgba32>(input);
|
||||||
|
|
||||||
|
image.Mutate(ctx => ctx
|
||||||
|
.Resize(new ResizeOptions
|
||||||
|
{
|
||||||
|
Size = new Size(oriented.Width, oriented.Height),
|
||||||
|
Mode = ResizeMode.Crop,
|
||||||
|
Position = AnchorPositionMode.Center,
|
||||||
|
})
|
||||||
|
.Dither(dither, displayPalette));
|
||||||
|
|
||||||
|
RemapDisplayToDevice(image, frame.Palette);
|
||||||
|
|
||||||
|
if (frame.Orientation == Orientation.Portrait)
|
||||||
|
{
|
||||||
|
image.Mutate(ctx => ctx.Rotate(RotateMode.Rotate90));
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoder = new PngEncoder
|
||||||
|
{
|
||||||
|
ColorType = PngColorType.Palette,
|
||||||
|
BitDepth = SmallestBitDepthFor(frame.Palette.Count),
|
||||||
|
Quantizer = new PaletteQuantizer(devicePalette),
|
||||||
|
};
|
||||||
|
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
image.SaveAsPng(output, encoder);
|
||||||
|
return output.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemapDisplayToDevice(Image<Rgba32> image, IReadOnlyList<PaletteEntry> palette)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<Rgba32, Rgba32>(palette.Count);
|
||||||
|
foreach (var entry in palette)
|
||||||
|
{
|
||||||
|
map[entry.DisplayColor.ToPixel<Rgba32>()] = entry.DeviceColor.ToPixel<Rgba32>();
|
||||||
|
}
|
||||||
|
|
||||||
|
image.ProcessPixelRows(accessor =>
|
||||||
|
{
|
||||||
|
for (var y = 0; y < accessor.Height; y++)
|
||||||
|
{
|
||||||
|
var row = accessor.GetRowSpan(y);
|
||||||
|
for (var x = 0; x < row.Length; x++)
|
||||||
|
{
|
||||||
|
if (map.TryGetValue(row[x], out var mapped))
|
||||||
|
{
|
||||||
|
row[x] = mapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PngBitDepth SmallestBitDepthFor(int paletteCount) => paletteCount switch
|
||||||
|
{
|
||||||
|
<= 2 => PngBitDepth.Bit1,
|
||||||
|
<= 4 => PngBitDepth.Bit2,
|
||||||
|
<= 16 => PngBitDepth.Bit4,
|
||||||
|
_ => PngBitDepth.Bit8,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user