diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index cb1e421470..f12f07070e 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -59,7 +59,6 @@ - diff --git a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs index 84988e912f..4dc96a074d 100644 --- a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs +++ b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Avalonia.Platform; namespace Avalonia.Controls.Platform.Surfaces { diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 1ba4e730d9..812e9d48ad 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -69,6 +69,7 @@ + @@ -102,7 +103,9 @@ + + diff --git a/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs new file mode 100644 index 0000000000..5c5b516ddd --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Platform; + +namespace Avalonia.Media.Imaging +{ + /// + /// Holds a writable bitmap image. + /// + public class WritableBitmap : Bitmap + { + public WritableBitmap(int width, int height, PixelFormat? format = null) + : base(AvaloniaLocator.Current.GetService().CreateWritableBitmap(width, height, format)) + { + } + + public ILockedFramebuffer Lock() => ((IWritableBitmapImpl) PlatformImpl).Lock(); + } +} diff --git a/src/Avalonia.Controls/Platform/Surfaces/ILockedFramebuffer.cs b/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs similarity index 91% rename from src/Avalonia.Controls/Platform/Surfaces/ILockedFramebuffer.cs rename to src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs index b62060b029..92ec2877ab 100644 --- a/src/Avalonia.Controls/Platform/Surfaces/ILockedFramebuffer.cs +++ b/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs @@ -1,7 +1,6 @@ using System; -using Avalonia.Platform; -namespace Avalonia.Controls.Platform.Surfaces +namespace Avalonia.Platform { public interface ILockedFramebuffer : IDisposable { diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 7dc97b022e..ef58d52b4f 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -56,6 +56,15 @@ namespace Avalonia.Platform /// An . IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height); + /// + /// Creates a writable bitmap implementation. + /// + /// The width of the bitmap. + /// The height of the bitmap. + /// Pixel format (optional). + /// An . + IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null); + /// /// Loads a bitmap implementation from a file.. /// diff --git a/src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs b/src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs new file mode 100644 index 0000000000..b736c11dab --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Platform +{ + /// + /// Defines the platform-specific interface for a . + /// + public interface IWritableBitmapImpl : IBitmapImpl + { + ILockedFramebuffer Lock(); + } +} diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs index b47827cb02..9cf16312e9 100644 --- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs +++ b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs @@ -96,5 +96,10 @@ namespace Avalonia.Cairo { throw new NotSupportedException("No proper control over pixel format with Cairo, use Skia backend instead"); } + + public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt) + { + throw new NotSupportedException("No proper support with Cairo, use Skia backend instead"); + } } } diff --git a/src/Gtk/Avalonia.Gtk/FramebufferManager.cs b/src/Gtk/Avalonia.Gtk/FramebufferManager.cs index 0c9ed44274..5ec49fb91f 100644 --- a/src/Gtk/Avalonia.Gtk/FramebufferManager.cs +++ b/src/Gtk/Avalonia.Gtk/FramebufferManager.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; namespace Avalonia.Gtk { diff --git a/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs b/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs index e52f0efb81..41e174bce4 100644 --- a/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs +++ b/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; namespace Avalonia.Gtk3 { diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs index 7d99156a1d..b564734a47 100644 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/BitmapImpl.cs @@ -9,7 +9,7 @@ using SkiaSharp; namespace Avalonia.Skia { - class BitmapImpl : IRenderTargetBitmapImpl + class BitmapImpl : IRenderTargetBitmapImpl, IWritableBitmapImpl { public SKBitmap Bitmap { get; private set; } @@ -20,11 +20,11 @@ namespace Avalonia.Skia PixelWidth = bm.Width; } - public BitmapImpl(int width, int height) + public BitmapImpl(int width, int height, PixelFormat? fmt = null) { PixelHeight = height; PixelWidth = width; - var colorType = SKImageInfo.PlatformColorType; + var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType; var runtime = AvaloniaLocator.Current?.GetService()?.GetRuntimeInfo(); if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux) colorType = SKColorType.Bgra8888; @@ -38,10 +38,21 @@ namespace Avalonia.Skia public void Save(string fileName) { + #if DESKTOP + if(Bitmap.ColorType != SKColorType.Bgra8888) + { + using (var tmp = new BitmapImpl(Bitmap.Copy(SKColorType.Bgra8888))) + tmp.Save(fileName); + return; + } + IntPtr length; using (var sdb = new System.Drawing.Bitmap(PixelWidth, PixelHeight, Bitmap.RowBytes, - System.Drawing.Imaging.PixelFormat.Format32bppArgb, Bitmap.GetPixels(out length))) + + System.Drawing.Imaging.PixelFormat.Format32bppArgb, + + Bitmap.GetPixels(out length))) sdb.Save(fileName); #else //SkiaSharp doesn't expose image encoders yet @@ -96,5 +107,31 @@ namespace Avalonia.Skia data.SaveTo(stream); } } + + class BitmapFramebuffer : ILockedFramebuffer + { + private SKBitmap _bmp; + + public BitmapFramebuffer(SKBitmap bmp) + { + _bmp = bmp; + _bmp.LockPixels(); + } + + public void Dispose() + { + _bmp.UnlockPixels(); + _bmp = null; + } + + public IntPtr Address => _bmp.GetPixels(); + public int Width => _bmp.Width; + public int Height => _bmp.Height; + public int RowBytes => _bmp.RowBytes; + public Size Dpi { get; } = new Size(96, 96); + public PixelFormat Format => _bmp.ColorType.ToPixelFormat(); + } + + public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap); } } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 1b898f0f48..72f8d08d44 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -84,5 +84,10 @@ namespace Avalonia.Skia throw new Exception("Skia backend currently only supports framebuffer render target"); return new FramebufferRenderTarget(fb); } + + public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null) + { + return new BitmapImpl(width, height, format); + } } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 7540caacfe..8591f9218a 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -57,6 +57,17 @@ namespace Avalonia.Skia throw new ArgumentException("Unknown pixel format: " + fmt); } + public static PixelFormat ToPixelFormat(this SKColorType fmt) + { + if (fmt == SKColorType.Rgb565) + return PixelFormat.Rgb565; + if (fmt == SKColorType.Bgra8888) + return PixelFormat.Bgra8888; + if (fmt == SKColorType.Rgba8888) + return PixelFormat.Rgba8888; + throw new ArgumentException("Unknown pixel format: " + fmt); + } + public static SKShaderTileMode ToSKShaderTileMode(this Media.GradientSpreadMethod m) { switch (m) diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index a055a337eb..ba117d72e9 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -70,6 +70,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 913e76f48e..d5b0f22090 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -10,6 +10,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Rendering; namespace Avalonia @@ -119,6 +120,11 @@ namespace Avalonia.Direct2D1 return new RenderTargetBitmapImpl(s_imagingFactory, s_d2D1Device.Factory, width, height); } + public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null) + { + return new WritableWicBitmapImpl(s_imagingFactory, width, height, format); + } + public IStreamGeometryImpl CreateStreamGeometry() { return new StreamGeometryImpl(); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 4082bf6850..2dd7acd9f9 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -56,29 +56,26 @@ namespace Avalonia.Direct2D1.Media /// The WIC imaging factory to use. /// The width of the bitmap. /// The height of the bitmap. - public WicBitmapImpl(ImagingFactory factory, int width, int height) + /// Pixel format + public WicBitmapImpl(ImagingFactory factory, int width, int height, APixelFormat? pixelFormat = null) { + if (!pixelFormat.HasValue) + pixelFormat = APixelFormat.Rgba8888; + _factory = factory; + PixelFormat = pixelFormat; WicImpl = new Bitmap( factory, width, height, - PixelFormat.Format32bppPBGRA, + pixelFormat.Value.ToWic(), BitmapCreateCacheOption.CacheOnLoad); } public WicBitmapImpl(ImagingFactory factory, Platform.PixelFormat format, IntPtr data, int width, int height, int stride) { - Guid fmt; - if (format == APixelFormat.Rgb565) - fmt = PixelFormat.Format16bppBGR565; - else if (format == APixelFormat.Bgra8888) - fmt = PixelFormat.Format32bppPBGRA; - else if (format == APixelFormat.Rgba8888) - fmt = PixelFormat.Format32bppPRGBA; - else throw new ArgumentException("Unknown pixel format"); - - WicImpl = new Bitmap(factory, width, height, fmt, BitmapCreateCacheOption.CacheOnDemand); + WicImpl = new Bitmap(factory, width, height, format.ToWic(), BitmapCreateCacheOption.CacheOnDemand); + PixelFormat = format; using (var l = WicImpl.Lock(BitmapLockFlags.Write)) { for (var row = 0; row < height; row++) @@ -89,6 +86,8 @@ namespace Avalonia.Direct2D1.Media } } + protected APixelFormat? PixelFormat { get; } + /// /// Gets the width of the bitmap, in pixels. /// @@ -120,7 +119,7 @@ namespace Avalonia.Direct2D1.Media if (_direct2D == null) { FormatConverter converter = new FormatConverter(_factory); - converter.Initialize(WicImpl, PixelFormat.Format32bppPBGRA); + converter.Initialize(WicImpl, SharpDX.WIC.PixelFormat.Format32bppPBGRA); _direct2D = SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs new file mode 100644 index 0000000000..06eb26b407 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Platform; +using SharpDX.WIC; +using PixelFormat = Avalonia.Platform.PixelFormat; + +namespace Avalonia.Direct2D1.Media.Imaging +{ + class WritableWicBitmapImpl : WicBitmapImpl, IWritableBitmapImpl + { + public WritableWicBitmapImpl(ImagingFactory factory, int width, int height, PixelFormat? pixelFormat) + : base(factory, width, height, pixelFormat) + { + } + + class LockedBitmap : ILockedFramebuffer + { + private readonly BitmapLock _lock; + private readonly PixelFormat _format; + + public LockedBitmap(BitmapLock l, PixelFormat format) + { + _lock = l; + _format = format; + } + + + public void Dispose() + { + _lock.Dispose(); + } + + public IntPtr Address => _lock.Data.DataPointer; + public int Width => _lock.Size.Width; + public int Height => _lock.Size.Height; + public int RowBytes => _lock.Stride; + public Size Dpi { get; } = new Size(96, 96); + public PixelFormat Format => _format; + + } + + public ILockedFramebuffer Lock() => new LockedBitmap(WicImpl.Lock(BitmapLockFlags.Write), PixelFormat.Value); + } +} diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 87be89d10c..118b6deb97 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -88,6 +88,17 @@ namespace Avalonia.Direct2D1 return CapStyle.Triangle; } + public static Guid ToWic(this Platform.PixelFormat format) + { + if (format == Platform.PixelFormat.Rgb565) + return SharpDX.WIC.PixelFormat.Format16bppBGR565; + if (format == Platform.PixelFormat.Bgra8888) + return SharpDX.WIC.PixelFormat.Format32bppPBGRA; + if (format == Platform.PixelFormat.Rgba8888) + return SharpDX.WIC.PixelFormat.Format32bppPRGBA; + throw new ArgumentException("Unknown pixel format"); + } + /// /// Converts a pen to a Direct2D stroke style. /// diff --git a/src/Windows/Avalonia.Win32/FramebufferManager.cs b/src/Windows/Avalonia.Win32/FramebufferManager.cs index ecd05f41b4..f0a6430918 100644 --- a/src/Windows/Avalonia.Win32/FramebufferManager.cs +++ b/src/Windows/Avalonia.Win32/FramebufferManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; using Avalonia.Win32.Interop; namespace Avalonia.Win32 diff --git a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs index 4cae5af237..fe4fe5c668 100644 --- a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs +++ b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; using Avalonia.Win32.Interop; using PixelFormat = Avalonia.Platform.PixelFormat; diff --git a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs index b0a57f6f4a..a70130990a 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs @@ -370,6 +370,11 @@ namespace Avalonia.Input.UnitTests throw new NotImplementedException(); } + public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt) + { + throw new NotImplementedException(); + } + class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index e7bd1054d4..1fd5d1eda6 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -102,7 +102,38 @@ namespace Avalonia.Direct2D1.RenderTests.Media rtb.Save(System.IO.Path.Combine(OutputPath, testName + ".out.png")); } CompareImages(testName); - + } + +#if AVALONIA_CAIRO + //wontfix +#else + [Theory] +#endif + [InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgba8888)] + public void WritableBitmapShouldBeUsable(PixelFormat fmt) + { + var writableBitmap = new WritableBitmap(256, 256, fmt); + + var data = new int[256 * 256]; + for (int y = 0; y < 256; y++) + for (int x = 0; x < 256; x++) + data[y * 256 + x] =(int)((uint)(x + (y << 8)) | 0xFF000000u); + + + using (var l = writableBitmap.Lock()) + { + for(var r = 0; r<256; r++) + { + Marshal.Copy(data, r * 256, new IntPtr(l.Address.ToInt64() + r * l.RowBytes), 256); + } + } + + + var name = nameof(WritableBitmapShouldBeUsable) + "_" + fmt; + + writableBitmap.Save(System.IO.Path.Combine(OutputPath, name + ".out.png")); + CompareImages(name); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 7345826829..303736be9c 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -50,6 +50,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt) + { + throw new NotImplementedException(); + } + class MockStreamGeometry : IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png new file mode 100644 index 0000000000..ef77cbe0f2 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png new file mode 100644 index 0000000000..3cabea1742 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png differ diff --git a/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png b/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png new file mode 100644 index 0000000000..ef77cbe0f2 Binary files /dev/null and b/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png differ diff --git a/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png b/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png new file mode 100644 index 0000000000..3cabea1742 Binary files /dev/null and b/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png differ