From fac9e55f69f14aa209d299ac5ab78382bab42d3f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 14 Sep 2023 09:28:00 +0200 Subject: [PATCH] Introduce PixelFormatWriter (#12807) Introduce PixelFormatTranscoder Introduce Bitmap.CopyPixels that transcodes pixel and alpha format --- src/Avalonia.Base/Media/Imaging/Bitmap.cs | 68 +++- .../Media/Imaging/BitmapMemory.cs | 22 +- .../Media/Imaging/PixelFormatReaders.cs | 222 +++++++---- .../Media/Imaging/PixelFormatTranscoder.cs | 154 ++++++++ .../Media/Imaging/PixelFormatWriter.cs | 324 ++++++++++++++++ .../Media/Imaging/WriteableBitmap.cs | 9 +- .../Platform/ILockedFramebuffer.cs | 2 + .../Platform/IReadableBitmapImpl.cs | 11 +- .../Platform/IWriteableBitmapImpl.cs | 2 +- src/Avalonia.Base/Platform/PixelFormat.cs | 12 +- .../HeadlessPlatformRenderInterface.cs | 4 +- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 5 +- .../Avalonia.Skia/RenderTargetBitmapImpl.cs | 2 +- src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs | 2 + .../Media/Imaging/WicBitmapImpl.cs | 10 +- .../Avalonia.Base.UnitTests.csproj | 1 + .../Media/Imaging/PixelFormatWriterTests.cs | 355 ++++++++++++++++++ 17 files changed, 1093 insertions(+), 112 deletions(-) create mode 100644 src/Avalonia.Base/Media/Imaging/PixelFormatTranscoder.cs create mode 100644 src/Avalonia.Base/Media/Imaging/PixelFormatWriter.cs create mode 100644 tests/Avalonia.Base.UnitTests/Media/Imaging/PixelFormatWriterTests.cs diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index fbe9370edcf..215586eef90 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -1,7 +1,7 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Utilities; @@ -108,19 +108,23 @@ public Bitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSiz PlatformImpl = RefCountable.Create(factory.LoadBitmap(format, alphaFormat, data, size, dpi, stride)); else { - var transcoded = Marshal.AllocHGlobal(size.Width * size.Height * 4); - var transcodedStride = size.Width * 4; - try + using (var transcoded = new BitmapMemory(PixelFormat.Rgba8888, Platform.AlphaFormat.Unpremul, size)) { - PixelFormatReader.Transcode(transcoded, data, size, stride, transcodedStride, format); - var transcodedAlphaFormat = format.HasAlpha ? alphaFormat : AlphaFormat.Opaque; - + var transcodedAlphaFormat = format.HasAlpha ? alphaFormat : Platform.AlphaFormat.Opaque; + + PixelFormatTranscoder.Transcode( + data, + size, + stride, + format, + alphaFormat, + transcoded.Address, + transcoded.RowBytes, + transcoded.Format, + transcodedAlphaFormat); + PlatformImpl = RefCountable.Create(factory.LoadBitmap(PixelFormat.Rgba8888, transcodedAlphaFormat, - transcoded, size, dpi, transcodedStride)); - } - finally - { - Marshal.FreeHGlobal(transcoded); + transcoded.Address, size, dpi, transcoded.RowBytes)); } _isTranscoded = true; @@ -173,6 +177,8 @@ public void Save(Stream stream, int? quality = null) public virtual PixelFormat? Format => (PlatformImpl.Item as IReadableBitmapImpl)?.Format; + public virtual AlphaFormat? AlphaFormat => (PlatformImpl.Item as IReadableBitmapWithAlphaImpl)?.AlphaFormat; + private protected unsafe void CopyPixelsCore(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride, ILockedFramebuffer fb) { @@ -222,6 +228,44 @@ public virtual void CopyPixels(PixelRect sourceRect, IntPtr buffer, int bufferSi CopyPixelsCore(sourceRect, buffer, bufferSize, stride, fb); } + /// + /// Copies pixels to the target buffer and transcodes the pixel and alpha format if needed. + /// + /// The target buffer. + /// The alpha format. + /// + public void CopyPixels(ILockedFramebuffer buffer, AlphaFormat alphaFormat) + { + if (PlatformImpl.Item is not IReadableBitmapWithAlphaImpl readable || readable.Format == null || readable.AlphaFormat == null) + { + throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); + } + + if (readable.Format != Format || readable.AlphaFormat != alphaFormat) + { + using (var fb = readable.Lock()) + { + PixelFormatTranscoder.Transcode( + fb.Address, + fb.Size, + fb.RowBytes, + fb.Format, + readable.AlphaFormat.Value, + buffer.Address, + buffer.RowBytes, + buffer.Format, + alphaFormat); + } + } + else + { + using (var fb = readable.Lock()) + { + CopyPixelsCore(new PixelRect(fb.Size), buffer.Address, buffer.RowBytes * buffer.Size.Height, fb.RowBytes, fb); + } + } + } + /// void IImage.Draw( DrawingContext context, diff --git a/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs b/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs index 68ae2e37a5a..1032081b6e8 100644 --- a/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs +++ b/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs @@ -9,9 +9,10 @@ internal class BitmapMemory : IDisposable { private readonly int _memorySize; - public BitmapMemory(PixelFormat format, PixelSize size) + public BitmapMemory(PixelFormat format, AlphaFormat alphaFormat, PixelSize size) { Format = format; + AlphaFormat = alphaFormat; Size = size; RowBytes = (size.Width * format.BitsPerPixel + 7) / 8; _memorySize = RowBytes * size.Height; @@ -44,8 +45,19 @@ public void Dispose() public int RowBytes { get; } public PixelFormat Format { get; } + public AlphaFormat AlphaFormat { get; } - - public void CopyToRgba(IntPtr buffer, int rowBytes) => - PixelFormatReader.Transcode(buffer, Address, Size, RowBytes, rowBytes, Format); -} \ No newline at end of file + public void CopyToRgba(AlphaFormat alphaFormat, IntPtr buffer, int stride) + { + PixelFormatTranscoder.Transcode( + Address, + Size, + RowBytes, + Format, + AlphaFormat, + buffer, + stride, + PixelFormat.Rgba8888, + alphaFormat); + } +} diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs index c90c4cb5ac4..df2614d8ffb 100644 --- a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs @@ -2,22 +2,46 @@ using Avalonia.Platform; namespace Avalonia.Media.Imaging; -internal struct Rgba8888Pixel +internal record struct Rgba64Pixel { + public Rgba64Pixel(ushort r, ushort g, ushort b, ushort a) + { + R = r; + G = g; + B = b; + A = a; + } + + public ushort R; + public ushort G; + public ushort B; + public ushort A; +} + +internal record struct Rgba8888Pixel +{ + public Rgba8888Pixel(byte r, byte g, byte b, byte a) + { + R = r; + G = g; + B = b; + A = a; + } + public byte R; public byte G; public byte B; public byte A; } -static unsafe class PixelFormatReader +internal interface IPixelFormatReader +{ + Rgba8888Pixel ReadNext(); + void Reset(IntPtr address); +} + +internal static unsafe class PixelFormatReader { - public interface IPixelFormatReader - { - Rgba8888Pixel ReadNext(); - void Reset(IntPtr address); - } - private static readonly Rgba8888Pixel s_white = new Rgba8888Pixel { A = 255, @@ -25,7 +49,7 @@ public interface IPixelFormatReader G = 255, R = 255 }; - + private static readonly Rgba8888Pixel s_black = new Rgba8888Pixel { A = 255, @@ -34,7 +58,7 @@ public interface IPixelFormatReader R = 0 }; - public unsafe struct BlackWhitePixelReader : IPixelFormatReader + public unsafe struct BlackWhitePixelFormatReader : IPixelFormatReader { private int _bit; private byte* _address; @@ -58,8 +82,8 @@ public Rgba8888Pixel ReadNext() return value == 1 ? s_white : s_black; } } - - public unsafe struct Gray2PixelReader : IPixelFormatReader + + public unsafe struct Gray2PixelFormatReader : IPixelFormatReader { private int _bit; private byte* _address; @@ -88,7 +112,7 @@ public Rgba8888Pixel ReadNext() { var shift = 6 - _bit; var value = (byte)((*_address >> shift)); - value = (byte)((value & 3)); + value = (byte)((value & 3)); _bit += 2; if (_bit == 8) { @@ -99,8 +123,8 @@ public Rgba8888Pixel ReadNext() return Palette[value]; } } - - public unsafe struct Gray4PixelReader : IPixelFormatReader + + public unsafe struct Gray4PixelFormatReader : IPixelFormatReader { private int _bit; private byte* _address; @@ -133,8 +157,8 @@ public Rgba8888Pixel ReadNext() }; } } - - public unsafe struct Gray8PixelReader : IPixelFormatReader + + public unsafe struct Gray8PixelFormatReader : IPixelFormatReader { private byte* _address; public void Reset(IntPtr address) @@ -156,8 +180,8 @@ public Rgba8888Pixel ReadNext() }; } } - - public unsafe struct Gray16PixelReader : IPixelFormatReader + + public unsafe struct Gray16PixelFormatReader : IPixelFormatReader { private ushort* _address; public Rgba8888Pixel ReadNext() @@ -177,7 +201,7 @@ public Rgba8888Pixel ReadNext() public void Reset(IntPtr address) => _address = (ushort*)address; } - public unsafe struct Gray32FloatPixelReader : IPixelFormatReader + public unsafe struct Gray32FloatPixelFormatReader : IPixelFormatReader { private byte* _address; public Rgba8888Pixel ReadNext() @@ -199,19 +223,10 @@ public Rgba8888Pixel ReadNext() public void Reset(IntPtr address) => _address = (byte*)address; } - struct Rgba64 - { -#pragma warning disable CS0649 - public ushort R; - public ushort G; - public ushort B; - public ushort A; -#pragma warning restore CS0649 - } public unsafe struct Rgba64PixelFormatReader : IPixelFormatReader { - private Rgba64* _address; + private Rgba64Pixel* _address; public Rgba8888Pixel ReadNext() { var value = *_address; @@ -226,9 +241,9 @@ public Rgba8888Pixel ReadNext() }; } - public void Reset(IntPtr address) => _address = (Rgba64*)address; + public void Reset(IntPtr address) => _address = (Rgba64Pixel*)address; } - + public unsafe struct Rgb24PixelFormatReader : IPixelFormatReader { private byte* _address; @@ -247,7 +262,7 @@ public Rgba8888Pixel ReadNext() public void Reset(IntPtr address) => _address = (byte*)address; } - + public unsafe struct Bgr24PixelFormatReader : IPixelFormatReader { private byte* _address; @@ -267,58 +282,105 @@ public Rgba8888Pixel ReadNext() public void Reset(IntPtr address) => _address = (byte*)address; } - public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst, - PixelFormat format) + public unsafe struct Bgr555PixelFormatReader : IPixelFormatReader { - if (format == PixelFormats.BlackWhite) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray2) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray4) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray8) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray16) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Rgb24) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Bgr24) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray32Float) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Rgba64) - Transcode(dst, src, size, strideSrc, strideDst); - else - throw new NotSupportedException($"Pixel format {format} is not supported"); + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = (ushort*)_address; + + _address += 2; + + return UnPack(*addr); + } + + public void Reset(IntPtr address) => _address = (byte*)address; + + private static Rgba8888Pixel UnPack(ushort value) + { + var r = (byte)Math.Round(((value >> 10) & 0x1F) / 31F * 255); + var g = (byte)Math.Round(((value >> 5) & 0x1F) / 31F * 255); + var b = (byte)Math.Round(((value >> 0) & 0x1F) / 31F * 255); + + return new Rgba8888Pixel(r, g, b, 255); + } } - - public static bool SupportsFormat(PixelFormat format) + + public unsafe struct Bgr565PixelFormatReader : IPixelFormatReader { - return format == PixelFormats.BlackWhite - || format == PixelFormats.Gray2 - || format == PixelFormats.Gray4 - || format == PixelFormats.Gray8 - || format == PixelFormats.Gray16 - || format == PixelFormats.Gray32Float - || format == PixelFormats.Rgba64 - || format == PixelFormats.Bgr24 - || format == PixelFormats.Rgb24; + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = (ushort*)_address; + + _address += 2; + + return UnPack(*addr); + } + + public void Reset(IntPtr address) => _address = (byte*)address; + + private static Rgba8888Pixel UnPack(ushort value) + { + var r = (byte)Math.Round(((value >> 11) & 0x1F) / 31F * 255); + var g = (byte)Math.Round(((value >> 5) & 0x3F) / 63F * 255); + var b = (byte)Math.Round(((value >> 0) & 0x1F) / 31F * 255); + + return new Rgba8888Pixel(r, g, b, 255); + } } - - public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader + + public unsafe struct Rgba8888PixelFormatReader : IPixelFormatReader { - var w = size.Width; - var h = size.Height; - TReader reader = default; - for (var y = 0; y < h; y++) + private Rgba8888Pixel* _address; + public Rgba8888Pixel ReadNext() { - reader.Reset(src + strideSrc * y); - var dstRow = (Rgba8888Pixel*)(dst + strideDst * y); - for (var x = 0; x < w; x++) - { - *dstRow = reader.ReadNext(); - dstRow++; - } + var value = *_address; + + _address++; + + return value; + } + + public void Reset(IntPtr address) => _address = (Rgba8888Pixel*)address; + } + + public unsafe struct Bgra8888PixelFormatReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = _address; + + _address += 4; + + return new Rgba8888Pixel(addr[2], addr[1], addr[0], addr[3]); } + + public void Reset(IntPtr address) => _address = (byte*)address; } -} \ No newline at end of file + + public static bool SupportsFormat(PixelFormat format) + { + switch (format.FormatEnum) + { + case PixelFormatEnum.Rgb565: + case PixelFormatEnum.Rgba8888: + case PixelFormatEnum.Bgra8888: + case PixelFormatEnum.BlackWhite: + case PixelFormatEnum.Gray2: + case PixelFormatEnum.Gray4: + case PixelFormatEnum.Gray8: + case PixelFormatEnum.Gray16: + case PixelFormatEnum.Gray32Float: + case PixelFormatEnum.Rgba64: + case PixelFormatEnum.Rgb24: + case PixelFormatEnum.Bgr24: + case PixelFormatEnum.Bgr555: + case PixelFormatEnum.Bgr565: + return true; + default: + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatTranscoder.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatTranscoder.cs new file mode 100644 index 00000000000..b91c574fe06 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatTranscoder.cs @@ -0,0 +1,154 @@ +using System; +using Avalonia.Platform; +namespace Avalonia.Media.Imaging; + +internal static unsafe class PixelFormatTranscoder +{ + public static void Transcode( + IntPtr source, + PixelSize srcSize, + int sourceStride, + PixelFormat srcFormat, + AlphaFormat srcAlphaFormat, + IntPtr dest, + int destStride, + PixelFormat destFormat, + AlphaFormat destAlphaFormat) + { + var reader = GetReader(srcFormat); + var writer = GetWriter(destFormat); + + var w = srcSize.Width; + var h = srcSize.Height; + + for (var y = 0; y < h; y++) + { + reader.Reset(source + sourceStride * y); + + writer.Reset(dest + destStride * y); + + for (var x = 0; x < w; x++) + { + writer.WriteNext(GetConvertedPixel(reader.ReadNext(), srcAlphaFormat, destAlphaFormat)); + } + } + } + + private static Rgba8888Pixel GetConvertedPixel(Rgba8888Pixel pixel, AlphaFormat sourceAlpha, AlphaFormat destAlpha) + { + if (sourceAlpha != destAlpha) + { + if (sourceAlpha == AlphaFormat.Premul && destAlpha != AlphaFormat.Premul) + { + return ConvertFromPremultiplied(pixel); + } + + if (sourceAlpha != AlphaFormat.Premul && destAlpha == AlphaFormat.Premul) + { + return ConvertToPremultiplied(pixel); + } + } + + return pixel; + } + + private static Rgba8888Pixel ConvertToPremultiplied(Rgba8888Pixel pixel) + { + var factor = pixel.A / 255F; + + return new Rgba8888Pixel + { + R = (byte)(pixel.R * factor), + G = (byte)(pixel.G * factor), + B = (byte)(pixel.B * factor), + A = pixel.A + }; + } + + private static Rgba8888Pixel ConvertFromPremultiplied(Rgba8888Pixel pixel) + { + var factor = 1F / (pixel.A / 255F); + + return new Rgba8888Pixel + { + R = (byte)(pixel.R * factor), + G = (byte)(pixel.G * factor), + B = (byte)(pixel.B * factor), + A = pixel.A + }; + } + + private static IPixelFormatReader GetReader(PixelFormat format) + { + switch (format.FormatEnum) + { + case PixelFormatEnum.Rgb565: + return new PixelFormatReader.Bgr565PixelFormatReader(); + case PixelFormatEnum.Rgba8888: + return new PixelFormatReader.Rgba8888PixelFormatReader(); + case PixelFormatEnum.Bgra8888: + return new PixelFormatReader.Bgra8888PixelFormatReader(); + case PixelFormatEnum.BlackWhite: + return new PixelFormatReader.BlackWhitePixelFormatReader(); + case PixelFormatEnum.Gray2: + return new PixelFormatReader.Gray2PixelFormatReader(); + case PixelFormatEnum.Gray4: + return new PixelFormatReader.Gray4PixelFormatReader(); + case PixelFormatEnum.Gray8: + return new PixelFormatReader.Gray8PixelFormatReader(); + case PixelFormatEnum.Gray16: + return new PixelFormatReader.Gray16PixelFormatReader(); + case PixelFormatEnum.Gray32Float: + return new PixelFormatReader.Gray32FloatPixelFormatReader(); + case PixelFormatEnum.Rgba64: + return new PixelFormatReader.Rgba64PixelFormatReader(); + case PixelFormatEnum.Rgb24: + return new PixelFormatReader.Rgb24PixelFormatReader(); + case PixelFormatEnum.Bgr24: + return new PixelFormatReader.Bgr24PixelFormatReader(); + case PixelFormatEnum.Bgr555: + return new PixelFormatReader.Bgr555PixelFormatReader(); + case PixelFormatEnum.Bgr565: + return new PixelFormatReader.Bgr565PixelFormatReader(); + default: + throw new NotSupportedException($"Pixel format {format} is not supported"); + } + } + + private static IPixelFormatWriter GetWriter(PixelFormat format) + { + switch (format.FormatEnum) + { + case PixelFormatEnum.Rgb565: + return new PixelFormatWriter.Bgr565PixelFormatWriter(); + case PixelFormatEnum.Rgba8888: + return new PixelFormatWriter.Rgba8888PixelFormatWriter(); + case PixelFormatEnum.Bgra8888: + return new PixelFormatWriter.Bgra8888PixelFormatWriter(); + case PixelFormatEnum.BlackWhite: + return new PixelFormatWriter.BlackWhitePixelFormatWriter(); + case PixelFormatEnum.Gray2: + return new PixelFormatWriter.Gray2PixelFormatWriter(); + case PixelFormatEnum.Gray4: + return new PixelFormatWriter.Gray4PixelFormatWriter(); + case PixelFormatEnum.Gray8: + return new PixelFormatWriter.Gray8PixelFormatWriter(); + case PixelFormatEnum.Gray16: + return new PixelFormatWriter.Gray16PixelFormatWriter(); + case PixelFormatEnum.Gray32Float: + return new PixelFormatWriter.Gray32FloatPixelFormatWriter(); + case PixelFormatEnum.Rgba64: + return new PixelFormatWriter.Rgba64PixelFormatWriter(); + case PixelFormatEnum.Rgb24: + return new PixelFormatWriter.Rgb24PixelFormatWriter(); + case PixelFormatEnum.Bgr24: + return new PixelFormatWriter.Bgr24PixelFormatWriter(); + case PixelFormatEnum.Bgr555: + return new PixelFormatWriter.Bgr555PixelFormatWriter(); + case PixelFormatEnum.Bgr565: + return new PixelFormatWriter.Bgr565PixelFormatWriter(); + default: + throw new NotSupportedException($"Pixel format {format} is not supported"); + } + } +} diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatWriter.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatWriter.cs new file mode 100644 index 00000000000..3c0d5b61f21 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatWriter.cs @@ -0,0 +1,324 @@ +using System; +namespace Avalonia.Media.Imaging; + +internal interface IPixelFormatWriter +{ + void WriteNext(Rgba8888Pixel pixel); + void Reset(IntPtr address); +} + +internal static class PixelFormatWriter +{ + public unsafe struct Rgb24PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + addr[0] = pixel.R; + addr[1] = pixel.G; + addr[2] = pixel.B; + + _address += 3; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Rgba64PixelFormatWriter : IPixelFormatWriter + { + private Rgba64Pixel* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = new Rgba64Pixel((ushort)(pixel.R << 8), (ushort)(pixel.G << 8), (ushort)(pixel.B << 8), (ushort)(pixel.A << 8)); + + _address++; + } + + public void Reset(IntPtr address) => _address = (Rgba64Pixel*)address; + } + + public unsafe struct Rgba8888PixelFormatWriter : IPixelFormatWriter + { + private Rgba8888Pixel* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = pixel; + + _address++; + } + + public void Reset(IntPtr address) => _address = (Rgba8888Pixel*)address; + } + + public unsafe struct Bgra8888PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + addr[0] = pixel.B; + addr[1] = pixel.G; + addr[2] = pixel.R; + addr[3] = pixel.A; + + _address += 4; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgr24PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + addr[2] = pixel.R; + addr[1] = pixel.G; + addr[0] = pixel.B; + + _address += 3; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgra32PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + addr[3] = pixel.A; + addr[2] = pixel.R; + addr[1] = pixel.G; + addr[0] = pixel.B; + + _address += 4; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgr565PixelFormatWriter : IPixelFormatWriter + { + private ushort* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = Pack(pixel); + + _address++; + } + + public void Reset(IntPtr address) => _address = (ushort*)address; + + private static ushort Pack(Rgba8888Pixel pixel) + { + return (ushort)((((int)Math.Round(pixel.R / 255F * 31F) & 0x1F) << 11) + | (((int)Math.Round(pixel.G / 255F * 63F) & 0x3F) << 5) + | ((int)Math.Round(pixel.B / 255F * 31F) & 0x1F)); + } + } + + public unsafe struct Bgr555PixelFormatWriter : IPixelFormatWriter + { + private ushort* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = Pack(pixel); + + _address++; + } + + public void Reset(IntPtr address) => _address = (ushort*)address; + + private static ushort Pack(Rgba8888Pixel pixel) + { + return (ushort)( + (((int)Math.Round(pixel.R / 255F * 31F) & 0x1F) << 10) + | (((int)Math.Round(pixel.G / 255F * 31F) & 0x1F) << 5) + | (((int)Math.Round(pixel.B / 255F * 31F) & 0x1F) << 0)); + } + } + + public unsafe struct Gray32FloatPixelFormatWriter : IPixelFormatWriter + { + private float* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = Pack(pixel); + + _address++; + } + + private static float Pack(Rgba8888Pixel pixel) + { + return (float)Math.Pow(pixel.R / 255F, 2.2); + } + + public void Reset(IntPtr address) => _address = (float*)address; + } + + public unsafe struct BlackWhitePixelFormatWriter : IPixelFormatWriter + { + private int _bit; + private byte* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + var grayscale = Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + var value = grayscale > 0x7F ? 1 : 0; + + var shift = 7 - _bit; + var mask = 1 << shift; + + *addr = (byte)((*addr & ~mask) | value << shift); + + _bit++; + + if (_bit == 8) + { + _address++; + + _bit = 0; + } + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Gray2PixelFormatWriter : IPixelFormatWriter + { + private int _bit; + private byte* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + var value = 0; + + var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + if (grayscale > 0 && grayscale <= 0x55) + { + //01 + value = 1; + } + + if (grayscale > 0x55 && grayscale <= 0xAA) + { + //10 + + value = 2; + } + + if (grayscale > 0xAA) + { + //11 + value = 3; + } + + var shift = 6 - _bit; + var mask = 3 << shift; + + *addr = (byte)((*addr & ~mask) | value << shift); + + _bit += 2; + + if (_bit == 8) + { + _address++; + _bit = 0; + } + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Gray4PixelFormatWriter : IPixelFormatWriter + { + private int _bit; + private byte* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + var value = (byte)(grayscale / 255F * 0xF); + + var shift = 4 - _bit; + var mask = 0xF << shift; + + *addr = (byte)((*addr & ~mask) | value << shift); + + _bit += 4; + + if (_bit == 8) + { + _address++; + _bit = 0; + } + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Gray8PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + *addr = grayscale; + + _address++; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Gray16PixelFormatWriter : IPixelFormatWriter + { + private ushort* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + var grayscale = (ushort)Math.Round((0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B) * 0x0101); + + *addr = grayscale; + + _address++; + } + + public void Reset(IntPtr address) => _address = (ushort*)address; + } +} + + diff --git a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs index 868f4439c41..5ec00f9c7e4 100644 --- a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs @@ -74,7 +74,7 @@ public ILockedFramebuffer Lock() Dpi, _pixelFormatMemory.Format, () => { using var inner = ((IWriteableBitmapImpl)PlatformImpl.Item).Lock(); - _pixelFormatMemory.CopyToRgba(inner.Address, inner.RowBytes); + _pixelFormatMemory.CopyToRgba(Platform.AlphaFormat.Unpremul, inner.Address, inner.RowBytes); }); } @@ -137,9 +137,10 @@ private static (IBitmapImpl, BitmapMemory?) CreatePlatformImpl(PixelSize size, i if (!PixelFormatReader.SupportsFormat(finalFormat)) throw new NotSupportedException($"Pixel format {finalFormat} is not supported"); - var impl = ri.CreateWriteableBitmap(size, dpi, PixelFormat.Rgba8888, - finalFormat.HasAlpha ? finalAlphaFormat : AlphaFormat.Opaque); - return (impl, new BitmapMemory(finalFormat, size)); + finalAlphaFormat = finalFormat.HasAlpha ? finalAlphaFormat : Platform.AlphaFormat.Opaque; + + var impl = ri.CreateWriteableBitmap(size, dpi, PixelFormat.Rgba8888, finalAlphaFormat); + return (impl, new BitmapMemory(finalFormat, finalAlphaFormat, size)); } private static IPlatformRenderInterface GetFactory() diff --git a/src/Avalonia.Base/Platform/ILockedFramebuffer.cs b/src/Avalonia.Base/Platform/ILockedFramebuffer.cs index 2f631142d2f..f963b77cd92 100644 --- a/src/Avalonia.Base/Platform/ILockedFramebuffer.cs +++ b/src/Avalonia.Base/Platform/ILockedFramebuffer.cs @@ -28,5 +28,7 @@ public interface ILockedFramebuffer : IDisposable /// Pixel format /// PixelFormat Format { get; } + + //TODO12: Add AlphaFormat } } diff --git a/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs b/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs index acf1801e0a9..d5a0c765ccf 100644 --- a/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs +++ b/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs @@ -1,7 +1,16 @@ +using Avalonia.Metadata; + namespace Avalonia.Platform; public interface IReadableBitmapImpl { PixelFormat? Format { get; } ILockedFramebuffer Lock(); -} \ No newline at end of file +} + +//TODO12: Remove me once we can change IReadableBitmapImpl +[Unstable] +public interface IReadableBitmapWithAlphaImpl : IReadableBitmapImpl +{ + AlphaFormat? AlphaFormat { get; } +} diff --git a/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs b/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs index 3284d34a0ac..685491a326e 100644 --- a/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs +++ b/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform /// Defines the platform-specific interface for a . /// [Unstable] - public interface IWriteableBitmapImpl : IBitmapImpl, IReadableBitmapImpl + public interface IWriteableBitmapImpl : IBitmapImpl, IReadableBitmapWithAlphaImpl { } } diff --git a/src/Avalonia.Base/Platform/PixelFormat.cs b/src/Avalonia.Base/Platform/PixelFormat.cs index 95f49bdb25b..d03b4e04b8d 100644 --- a/src/Avalonia.Base/Platform/PixelFormat.cs +++ b/src/Avalonia.Base/Platform/PixelFormat.cs @@ -15,7 +15,9 @@ internal enum PixelFormatEnum Gray32Float, Rgba64, Rgb24, - Bgr24 + Bgr24, + Bgr555, + Bgr565 } public record struct PixelFormat @@ -34,8 +36,10 @@ public int BitsPerPixel return 4; else if (FormatEnum == PixelFormatEnum.Gray8) return 8; - else if (FormatEnum == PixelFormatEnum.Rgb565 - || FormatEnum == PixelFormatEnum.Gray16) + else if (FormatEnum == PixelFormatEnum.Rgb565 || + FormatEnum == PixelFormatEnum.Bgr555 || + FormatEnum == PixelFormatEnum.Bgr565 || + FormatEnum == PixelFormatEnum.Gray16) return 16; else if (FormatEnum is PixelFormatEnum.Bgr24 or PixelFormatEnum.Rgb24) return 24; @@ -76,5 +80,7 @@ public static class PixelFormats public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float); public static PixelFormat Rgb24 { get; } = new PixelFormat(PixelFormatEnum.Rgb24); public static PixelFormat Bgr24 { get; } = new PixelFormat(PixelFormatEnum.Bgr24); + public static PixelFormat Bgr555 { get; } = new PixelFormat(PixelFormatEnum.Bgr555); + public static PixelFormat Bgr565 { get; } = new PixelFormat(PixelFormatEnum.Bgr565); } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 63940294a97..5d603143de2 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -406,7 +406,10 @@ public void Blit(IDrawingContextImpl context) public Vector Dpi { get; } public PixelSize PixelSize { get; } + public PixelFormat? Format { get; } + public AlphaFormat? AlphaFormat { get; } public int Version { get; set; } + public void Save(string fileName, int? quality = null) { @@ -417,7 +420,6 @@ public void Save(Stream stream, int? quality = null) } - public PixelFormat? Format { get; } public ILockedFramebuffer Lock() { diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 0627407509c..6051985cea2 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -10,7 +10,7 @@ namespace Avalonia.Skia /// /// Immutable Skia bitmap. /// - internal class ImmutableBitmap : IDrawableBitmapImpl, IReadableBitmapImpl + internal class ImmutableBitmap : IDrawableBitmapImpl, IReadableBitmapWithAlphaImpl { private readonly SKImage _image; private readonly SKBitmap? _bitmap; @@ -177,6 +177,9 @@ public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, } public PixelFormat? Format => _bitmap?.ColorType.ToAvalonia(); + + public AlphaFormat? AlphaFormat => _bitmap?.AlphaType.ToAlphaFormat(); + public ILockedFramebuffer Lock() { if (_bitmap is null) diff --git a/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs b/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs index 454e137389f..a6a8fd1def4 100644 --- a/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs @@ -14,7 +14,7 @@ internal class RenderTargetBitmapImpl : WriteableBitmapImpl, public RenderTargetBitmapImpl(PixelSize size, Vector dpi) : base(size, dpi, SKImageInfo.PlatformColorType == SKColorType.Rgba8888 ? PixelFormats.Rgba8888 : PixelFormat.Bgra8888, - AlphaFormat.Premul) + Platform.AlphaFormat.Premul) { _renderTarget = new FramebufferRenderTarget(this); } diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index 581470fcfe3..865bcae99c2 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -148,6 +148,8 @@ public void Save(string fileName, int? quality = null) public PixelFormat? Format => _bitmap.ColorType.ToAvalonia(); + public AlphaFormat? AlphaFormat => _bitmap.AlphaType.ToAlphaFormat(); + /// public ILockedFramebuffer Lock() => new BitmapFramebuffer(this, _bitmap); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 2fa1e5bd7a9..5a8f30a235a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Direct2D1.Media /// /// A WIC implementation of a . /// - internal class WicBitmapImpl : BitmapImpl, IReadableBitmapImpl + internal class WicBitmapImpl : BitmapImpl, IReadableBitmapWithAlphaImpl { private readonly BitmapDecoder _decoder; @@ -87,10 +87,11 @@ public WicBitmapImpl(PixelSize size, Vector dpi, APixelFormat? pixelFormat = nul if (!alphaFormat.HasValue) { - alphaFormat = AlphaFormat.Premul; + alphaFormat = Platform.AlphaFormat.Premul; } PixelFormat = pixelFormat; + AlphaFormat = alphaFormat; WicImpl = new Bitmap( Direct2D1Platform.ImagingFactory, size.Width, @@ -106,6 +107,7 @@ public WicBitmapImpl(APixelFormat format, AlphaFormat alphaFormat, IntPtr data, WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, size.Width, size.Height, format.ToWic(alphaFormat), BitmapCreateCacheOption.CacheOnDemand); WicImpl.SetResolution(dpi.X, dpi.Y); PixelFormat = format; + AlphaFormat = alphaFormat; Dpi = dpi; using (var l = WicImpl.Lock(BitmapLockFlags.Write)) @@ -161,7 +163,9 @@ public WicBitmapImpl(Stream stream, int decodeSize, bool horizontal, Avalonia.Me public override PixelSize PixelSize => WicImpl.Size.ToAvalonia(); - protected APixelFormat? PixelFormat { get; } + public APixelFormat? PixelFormat { get; } + + public AlphaFormat? AlphaFormat { get; } public override void Dispose() { diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index 4cdce8df26a..3cb85a56452 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -3,6 +3,7 @@ net6.0 Library true + true diff --git a/tests/Avalonia.Base.UnitTests/Media/Imaging/PixelFormatWriterTests.cs b/tests/Avalonia.Base.UnitTests/Media/Imaging/PixelFormatWriterTests.cs new file mode 100644 index 00000000000..daf057ff331 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/Imaging/PixelFormatWriterTests.cs @@ -0,0 +1,355 @@ +using System; +using Avalonia.Media.Imaging; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.Imaging +{ + public class PixelFormatWriterTests + { + private static readonly Rgba8888Pixel s_white = new Rgba8888Pixel + { + A = 255, + B = 255, + G = 255, + R = 255 + }; + + private static readonly Rgba8888Pixel s_black = new Rgba8888Pixel + { + A = 255, + B = 0, + G = 0, + R = 0 + }; + + [Fact] + public void Should_Write_Bgr555() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Bgr555), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Bgr555PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Bgr555PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { G = 255 }); + Assert.Equal(new Rgba8888Pixel { G = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { B = 255 }); + Assert.Equal(new Rgba8888Pixel { B = 255, A = 255 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Bgra8888() + { + var sourceMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Bgra8888), + Platform.AlphaFormat.Unpremul, + new PixelSize(3, 1)); + + var sourceWriter = new PixelFormatWriter.Bgra8888PixelFormatWriter(); + var sourceReader = new PixelFormatReader.Bgra8888PixelFormatReader(); + + sourceWriter.Reset(sourceMemory.Address); + sourceReader.Reset(sourceMemory.Address); + + sourceWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 255 }, sourceReader.ReadNext()); + + sourceWriter.WriteNext(new Rgba8888Pixel { G = 255 }); + Assert.Equal(new Rgba8888Pixel { G = 255 }, sourceReader.ReadNext()); + + sourceWriter.WriteNext(new Rgba8888Pixel { B = 255 }); + Assert.Equal(new Rgba8888Pixel { B = 255 }, sourceReader.ReadNext()); + } + + [Fact] + public void Should_Write_Rgba8888() + { + var sourceMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Rgba8888), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Rgba8888PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Rgba8888PixelFormatReader(); + + pixelWriter.Reset(sourceMemory.Address); + pixelReader.Reset(sourceMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Rgb24() + { + var sourceMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Rgb24), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Rgb24PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Rgb24PixelFormatReader(); + + pixelWriter.Reset(sourceMemory.Address); + pixelReader.Reset(sourceMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 255 }, pixelReader.ReadNext()); + } + + + [Fact] + public void Should_Write_Rgba64() + { + var sourceMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Rgba64), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Rgba64PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Rgba64PixelFormatReader(); + + pixelWriter.Reset(sourceMemory.Address); + pixelReader.Reset(sourceMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Bgr565() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Bgr565), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Bgr565PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Bgr565PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { G = 255 }); + Assert.Equal(new Rgba8888Pixel { G = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { B = 255 }); + Assert.Equal(new Rgba8888Pixel { B = 255, A = 255 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Gray32Float() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray32Float), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray32FloatPixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray32FloatPixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 255, G = 255, B = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 125, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel()); + Assert.Equal(new Rgba8888Pixel { A = 255 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_BlackWhite() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.BlackWhite), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.BlackWhitePixelFormatWriter(); + var pixelReader = new PixelFormatReader.BlackWhitePixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(s_white); + Assert.Equal(s_white, pixelReader.ReadNext()); + + pixelWriter.WriteNext(s_black); + Assert.Equal(s_black, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Gray2() + { + var palette = new[] + { + s_black, + new Rgba8888Pixel + { + A = 255, B = 0x55, G = 0x55, R = 0x55 + }, + new Rgba8888Pixel + { + A = 255, B = 0xAA, G = 0xAA, R = 0xAA + }, + s_white + }; + + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray2), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray2PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray2PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(palette[0]); + Assert.Equal(palette[0], pixelReader.ReadNext()); + + pixelWriter.WriteNext(palette[1]); + Assert.Equal(palette[1], pixelReader.ReadNext()); + + pixelWriter.WriteNext(palette[2]); + Assert.Equal(palette[2], pixelReader.ReadNext()); + + pixelWriter.WriteNext(palette[3]); + Assert.Equal(palette[3], pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Gray4() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray4), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray4PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray4PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(GetGray4(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 17 }); + Assert.Equal(GetGray4(new Rgba8888Pixel { R = 17 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel()); + Assert.Equal(new Rgba8888Pixel { A = 255 }, pixelReader.ReadNext()); + } + + private static Rgba8888Pixel GetGray4(Rgba8888Pixel pixel) + { + var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + var value = (byte)(grayscale / 255F * 0xF); + + value = (byte)(value | (value << 4)); + + return new Rgba8888Pixel(value, value, value, 255); + } + + [Fact] + public void Should_Write_Gray8() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray8), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray8PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray8PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(GetGray8(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 120 }); + Assert.Equal(GetGray8(new Rgba8888Pixel { R = 120 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel()); + Assert.Equal(GetGray8(new Rgba8888Pixel { A = 255 }), pixelReader.ReadNext()); + } + + private static Rgba8888Pixel GetGray8(Rgba8888Pixel pixel) + { + var value = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + return new Rgba8888Pixel(value, value, value, 255); + } + + [Fact] + public void Should_Write_Gray16() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray16), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray16PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray16PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(GetGray16(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 120 }); + Assert.Equal(GetGray16(new Rgba8888Pixel { R = 120 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel()); + Assert.Equal(GetGray16(new Rgba8888Pixel { A = 255 }), pixelReader.ReadNext()); + } + + private static Rgba8888Pixel GetGray16(Rgba8888Pixel pixel) + { + var grayscale = (ushort)Math.Round((0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B) * 0x0101); + + var value = (byte)(grayscale >> 8); + + return new Rgba8888Pixel(value, value, value, 255); + } + } +}