diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 6119103e6d..cb1e421470 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -60,7 +60,6 @@ - diff --git a/src/Avalonia.Controls/Platform/Surfaces/ILockedFramebuffer.cs b/src/Avalonia.Controls/Platform/Surfaces/ILockedFramebuffer.cs index d6402d170d..b62060b029 100644 --- a/src/Avalonia.Controls/Platform/Surfaces/ILockedFramebuffer.cs +++ b/src/Avalonia.Controls/Platform/Surfaces/ILockedFramebuffer.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Platform; namespace Avalonia.Controls.Platform.Surfaces { diff --git a/src/Avalonia.Controls/Platform/Surfaces/PixelFormat.cs b/src/Avalonia.Controls/Platform/Surfaces/PixelFormat.cs deleted file mode 100644 index c9f8eabe97..0000000000 --- a/src/Avalonia.Controls/Platform/Surfaces/PixelFormat.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Controls.Platform.Surfaces -{ - public enum PixelFormat - { - Rgb565, - Rgba8888, - Bgra8888 - } -} diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index a0ed19d2b4..1ba4e730d9 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -103,6 +103,7 @@ + diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 1800110e68..c9f7e0f7ac 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -41,6 +41,20 @@ namespace Avalonia.Media.Imaging PlatformImpl = impl; } + /// + /// Initializes a new instance of the class. + /// + /// Pixel format + /// Pointer to source bytes + /// Bitmap width + /// Bitmap height + /// Bytes per row + public Bitmap(PixelFormat format, IntPtr data, int width, int height, int stride) + { + PlatformImpl = AvaloniaLocator.Current.GetService() + .LoadBitmap(format, data, width, height, stride); + } + /// /// Gets the width of the bitmap, in pixels. /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index cdec0a07a1..7dc97b022e 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; using System.IO; using Avalonia.Media; @@ -68,5 +69,16 @@ namespace Avalonia.Platform /// The stream to read the bitmap from. /// An . IBitmapImpl LoadBitmap(Stream stream); + + /// + /// Loads a bitmap implementation from a pixels in memory.. + /// + /// Pixel format + /// Pointer to source bytes + /// Bitmap width + /// Bitmap height + /// Bytes per row + /// An . + IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride); } } diff --git a/src/Avalonia.Visuals/Platform/PixelFormat.cs b/src/Avalonia.Visuals/Platform/PixelFormat.cs new file mode 100644 index 0000000000..526303ebb1 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/PixelFormat.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Platform +{ + public enum PixelFormat + { + Rgb565, + Rgba8888, + Bgra8888 + } +} diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs index e6c493320f..b47827cb02 100644 --- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs +++ b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs @@ -91,5 +91,10 @@ namespace Avalonia.Cairo Gtk.Application.Init(); return new Gtk.Invisible().CreatePangoContext(); } + + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) + { + throw new NotSupportedException("No proper control over pixel format with Cairo, use Skia backend instead"); + } } } diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index b273d6209f..b2b5a0653f 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -22,19 +22,6 @@ namespace Avalonia.Skia //Nothing to do here, since we don't own framebuffer } - - SKColorType TranslatePixelFormat(PixelFormat fmt) - { - if(fmt == PixelFormat.Rgb565) - return SKColorType.Rgb565; - if(fmt == PixelFormat.Bgra8888) - return SKColorType.Bgra8888; - if (fmt == PixelFormat.Rgba8888) - return SKColorType.Rgba8888; - throw new ArgumentException("Unknown pixel format: " + fmt); - } - - class PixelFormatShim : IDisposable { private readonly SKImageInfo _nfo; @@ -73,8 +60,8 @@ namespace Avalonia.Skia { var fb = _surface.Lock(); PixelFormatShim shim = null; - SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, TranslatePixelFormat(fb.Format), - SKAlphaType.Opaque); + SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(), + SKAlphaType.Premul); var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ?? (shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes)) .CreateSurface(); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f0735bb0df..1b898f0f48 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -52,6 +52,16 @@ namespace Avalonia.Skia } } + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) + { + using (var tmp = new SKBitmap()) + { + tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul) + , data, stride); + return new BitmapImpl(tmp.Copy()); + } + } + public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) { return new Renderer(root, renderLoop); diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 0d0ab380c5..7540caacfe 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,4 +1,6 @@ +using System; using Avalonia.Media; +using Avalonia.Platform; using SkiaSharp; @@ -44,6 +46,17 @@ namespace Avalonia.Skia return new SKColor(c.R, c.G, c.B, c.A); } + public static SKColorType ToSkColorType(this PixelFormat fmt) + { + if (fmt == PixelFormat.Rgb565) + return SKColorType.Rgb565; + if (fmt == PixelFormat.Bgra8888) + return SKColorType.Bgra8888; + if (fmt == PixelFormat.Rgba8888) + return SKColorType.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/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 34595fecc8..913e76f48e 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -133,5 +133,10 @@ namespace Avalonia.Direct2D1 { return new WicBitmapImpl(s_imagingFactory, stream); } + + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) + { + return new WicBitmapImpl(s_imagingFactory, format, data, width, height, stride); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index f17c516edd..4082bf6850 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -4,6 +4,9 @@ using System; using System.IO; using Avalonia.Platform; +using Avalonia.Win32.Interop; +using PixelFormat = SharpDX.WIC.PixelFormat; +using APixelFormat = Avalonia.Platform.PixelFormat; using SharpDX.WIC; namespace Avalonia.Direct2D1.Media @@ -64,6 +67,28 @@ namespace Avalonia.Direct2D1.Media 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); + using (var l = WicImpl.Lock(BitmapLockFlags.Write)) + { + for (var row = 0; row < height; row++) + { + UnmanagedMethods.CopyMemory(new IntPtr(l.Data.DataPointer.ToInt64() + row * l.Stride), + new IntPtr(data.ToInt64() + row * stride), (uint) l.Data.Pitch); + } + } + } + /// /// Gets the width of the bitmap, in pixels. /// diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index 8362305b9f..119715b5fc 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -9,6 +9,7 @@ using Avalonia.Win32.Interop; using SharpDX; using SharpDX.Direct2D1; using SharpDX.DXGI; +using PixelFormat = SharpDX.Direct2D1.PixelFormat; using AlphaMode = SharpDX.Direct2D1.AlphaMode; using Device = SharpDX.Direct2D1.Device; using Factory = SharpDX.Direct2D1.Factory; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 65a9f96b71..fe04d2c011 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -909,6 +909,9 @@ namespace Avalonia.Win32.Interop uint dwMaximumSizeLow, string lpName); + [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] + public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); + public enum MONITOR { MONITOR_DEFAULTTONULL = 0x00000000, diff --git a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs index b00348d97d..4cae5af237 100644 --- a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs +++ b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs @@ -2,7 +2,7 @@ using System.Runtime.InteropServices; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Win32.Interop; -using PixelFormat = Avalonia.Controls.Platform.Surfaces.PixelFormat; +using PixelFormat = Avalonia.Platform.PixelFormat; namespace Avalonia.Win32 { diff --git a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs index e00d504124..b0a57f6f4a 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs @@ -365,6 +365,11 @@ namespace Avalonia.Input.UnitTests throw new NotImplementedException(); } + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) + { + throw new NotImplementedException(); + } + class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); diff --git a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems index ad3b182bdf..e26ac81dda 100644 --- a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems +++ b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems @@ -10,6 +10,7 @@ + diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs new file mode 100644 index 0000000000..e7bd1054d4 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Controls.Shapes; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Xunit; + +#if AVALONIA_CAIRO +namespace Avalonia.Cairo.RenderTests.Media +#elif AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class BitmapTests : TestBase + { + public BitmapTests() + : base(@"Media\Bitmap") + { + Directory.CreateDirectory(OutputPath); + } + + class Framebuffer : ILockedFramebuffer, IFramebufferPlatformSurface + { + public Framebuffer(PixelFormat fmt, int width, int height) + { + Format = fmt; + var bpp = fmt == PixelFormat.Rgb565 ? 2 : 4; + Width = width; + Height = height; + RowBytes = bpp * width; + Address = Marshal.AllocHGlobal(Height * RowBytes); + } + + public IntPtr Address { get; } + + public Size Dpi { get; } = new Size(96, 96); + + public PixelFormat Format { get; } + + public int Height { get; } + + public int RowBytes { get; } + + public int Width { get; } + + public void Dispose() + { + //no-op + } + + public ILockedFramebuffer Lock() + { + return this; + } + + public void Deallocate() => Marshal.FreeHGlobal(Address); + } + + +#if AVALONIA_SKIA + [Theory] +#else + [Theory(Skip = "Framebuffer not supported")] +#endif + [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgb565)] + public void FramebufferRenderResultsShouldBeUsableAsBitmap(PixelFormat fmt) + { + var testName = nameof(FramebufferRenderResultsShouldBeUsableAsBitmap) + "_" + fmt; + var fb = new Framebuffer(fmt, 80, 80); + var r = Avalonia.AvaloniaLocator.Current.GetService(); + using (var target = r.CreateRenderTarget(new object[] { fb })) + using (var ctx = target.CreateDrawingContext()) + { + ctx.PushOpacity(0.8); + ctx.FillRectangle(Brushes.Chartreuse, new Rect(0, 0, 20, 100)); + ctx.FillRectangle(Brushes.Crimson, new Rect(20, 0, 20, 100)); + ctx.FillRectangle(Brushes.Gold, new Rect(40, 0, 20, 100)); + } + + var bmp = new Bitmap(fmt, fb.Address, fb.Width, fb.Height, fb.RowBytes); + fb.Deallocate(); + using (var rtb = new RenderTargetBitmap(100, 100)) + { + using (var ctx = rtb.CreateDrawingContext()) + { + ctx.FillRectangle(Brushes.Blue, new Rect(0, 0, 100, 100)); + ctx.FillRectangle(Brushes.Pink, new Rect(0, 20, 100, 10)); + + var rc = new Rect(0, 0, 60, 60); + ctx.DrawImage(bmp, 1, rc, rc); + } + rtb.Save(System.IO.Path.Combine(OutputPath, testName + ".out.png")); + } + CompareImages(testName); + + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index ac31b3852b..7345826829 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -45,6 +45,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) + { + throw new NotImplementedException(); + } + class MockStreamGeometry : IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); diff --git a/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Bgra8888.expected.png b/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Bgra8888.expected.png new file mode 100644 index 0000000000..19686464c5 Binary files /dev/null and b/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Bgra8888.expected.png differ diff --git a/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgb565.expected.png b/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgb565.expected.png new file mode 100644 index 0000000000..f3d20008a1 Binary files /dev/null and b/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgb565.expected.png differ diff --git a/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgba8888.expected.png b/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgba8888.expected.png new file mode 100644 index 0000000000..19686464c5 Binary files /dev/null and b/tests/TestFiles/Skia/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgba8888.expected.png differ