3.3 IImagePipeline + ImagePipeline
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
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