Browse Source

Use PixelSize for device-dependent sizes.

Steven Kirk 7 years ago
parent
commit
a0e8dca6e9
43 changed files with 436 additions and 280 deletions
  1. 2 3
      src/Avalonia.Controls/Image.cs
  2. 4 4
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  3. 1 1
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  4. 1 0
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  5. 15 16
      src/Avalonia.Visuals/Media/Imaging/Bitmap.cs
  6. 17 4
      src/Avalonia.Visuals/Media/Imaging/IBitmap.cs
  7. 17 12
      src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs
  8. 9 2
      src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs
  9. 174 0
      src/Avalonia.Visuals/Media/PixelSize.cs
  10. 4 4
      src/Avalonia.Visuals/Platform/IBitmapImpl.cs
  11. 2 7
      src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs
  12. 13 19
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  13. 4 6
      src/Avalonia.Visuals/Platform/LockedFramebuffer.cs
  14. 4 4
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  15. 4 6
      src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs
  16. 8 10
      src/Gtk/Avalonia.Gtk3/X11Framebuffer.cs
  17. 4 5
      src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs
  18. 5 7
      src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs
  19. 3 3
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  20. 1 1
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  21. 13 14
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  22. 12 18
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  23. 8 10
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  24. 12 16
      src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
  25. 8 12
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  26. 2 2
      src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs
  27. 2 6
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  28. 2 2
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  29. 2 2
      src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
  30. 2 2
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
  31. 0 3
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
  32. 21 17
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
  33. 5 7
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs
  34. 3 4
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs
  35. 4 0
      src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
  36. 1 2
      src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs
  37. 2 2
      src/Windows/Avalonia.Win32/FramebufferManager.cs
  38. 13 15
      src/Windows/Avalonia.Win32/WindowFramebuffer.cs
  39. 4 4
      tests/Avalonia.Controls.UnitTests/ImageTests.cs
  40. 9 12
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  41. 5 6
      tests/Avalonia.RenderTests/TestBase.cs
  42. 11 7
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  43. 3 3
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

+ 2 - 3
src/Avalonia.Controls/Image.cs

@@ -57,7 +57,7 @@ namespace Avalonia.Controls
             if (source != null)
             {
                 Rect viewPort = new Rect(Bounds.Size);
-                Size sourceSize = new Size(source.PixelWidth, source.PixelHeight);
+                Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height);
                 Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize);
                 Size scaledSize = sourceSize * scale;
                 Rect destRect = viewPort
@@ -83,8 +83,7 @@ namespace Avalonia.Controls
 
             if (source != null)
             {
-                Size sourceSize = new Size(source.PixelWidth, source.PixelHeight);
-
+                Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height);
                 if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height))
                 {
                     return sourceSize;

+ 4 - 4
src/Avalonia.Controls/Remote/RemoteWidget.cs

@@ -59,9 +59,9 @@ namespace Avalonia.Controls.Remote
             if (_lastFrame != null)
             {
                 var fmt = (PixelFormat) _lastFrame.Format;
-                if (_bitmap == null || _bitmap.PixelWidth != _lastFrame.Width ||
-                    _bitmap.PixelHeight != _lastFrame.Height)
-                    _bitmap = new WriteableBitmap(_lastFrame.Width, _lastFrame.Height, fmt);
+                if (_bitmap == null || _bitmap.PixelSize.Width != _lastFrame.Width ||
+                    _bitmap.PixelSize.Height != _lastFrame.Height)
+                    _bitmap = new WriteableBitmap(new PixelSize(_lastFrame.Width, _lastFrame.Height), new Vector(96, 96), fmt);
                 using (var l = _bitmap.Lock())
                 {
                     var lineLen = (fmt == PixelFormat.Rgb565 ? 2 : 4) * _lastFrame.Width;
@@ -69,7 +69,7 @@ namespace Avalonia.Controls.Remote
                         Marshal.Copy(_lastFrame.Data, y * _lastFrame.Stride,
                             new IntPtr(l.Address.ToInt64() + l.RowBytes * y), lineLen);
                 }
-                context.DrawImage(_bitmap, 1, new Rect(0, 0, _bitmap.PixelWidth, _bitmap.PixelHeight),
+                context.DrawImage(_bitmap, 1, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height),
                     new Rect(Bounds.Size));
             }
             base.Render(context);

+ 1 - 1
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@@ -108,7 +108,7 @@ namespace Avalonia.Controls.Remote.Server
             var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
             try
             {
-                _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), width, height, width * bpp, _dpi, (PixelFormat)fmt,
+                _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt,
                     null);
                 Paint?.Invoke(new Rect(0, 0, width, height));
             }

+ 1 - 0
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
+    <RootNamespace>Avalonia</RootNamespace>
   </PropertyGroup>
   <ItemGroup> 
     <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

+ 15 - 16
src/Avalonia.Visuals/Media/Imaging/Bitmap.cs

@@ -56,30 +56,29 @@ namespace Avalonia.Media.Imaging
         {
             PlatformImpl.Dispose();
         }
-        
+
         /// <summary>
         /// Initializes a new instance of the <see cref="Bitmap"/> class.
         /// </summary>
-        /// <param name="format">Pixel format</param>
-        /// <param name="data">Pointer to source bytes</param>
-        /// <param name="width">Bitmap width</param>
-        /// <param name="height">Bitmap height</param>
-        /// <param name="stride">Bytes per row</param>
-        public Bitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        /// <param name="format">The pixel format.</param>
+        /// <param name="data">The pointer to the source bytes.</param>
+        /// <param name="size">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
+        /// <param name="stride">The number of bytes per row.</param>
+        public Bitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
         {
             PlatformImpl = RefCountable.Create(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()
-                .LoadBitmap(format, data, width, height, stride));
+                .LoadBitmap(format, data, size, dpi, stride));
         }
 
-        /// <summary>
-        /// Gets the width of the bitmap, in pixels.
-        /// </summary>
-        public int PixelWidth => PlatformImpl.Item.PixelWidth;
+        /// <inheritdoc/>
+        public Vector Dpi => PlatformImpl.Item.Dpi;
 
-        /// <summary>
-        /// Gets the height of the bitmap, in pixels.
-        /// </summary>
-        public int PixelHeight => PlatformImpl.Item.PixelHeight;
+        /// <inheritdoc/>
+        public Size Size => PlatformImpl.Item.PixelSize.ToSize(Dpi);
+
+        /// <inheritdoc/>
+        public PixelSize PixelSize => PlatformImpl.Item.PixelSize;
 
         /// <summary>
         /// Gets the platform-specific bitmap implementation.

+ 17 - 4
src/Avalonia.Visuals/Media/Imaging/IBitmap.cs

@@ -14,20 +14,33 @@ namespace Avalonia.Media.Imaging
     public interface IBitmap : IDisposable
     {
         /// <summary>
-        /// Gets the width of the bitmap, in pixels.
+        /// Gets the dots per inch (DPI) of the image.
         /// </summary>
-        int PixelWidth { get; }
+        /// <remarks>
+        /// Note that Skia does not currently support reading the DPI of an image so this value
+        /// will always be 96dpi on Skia.
+        /// </remarks>
+        Vector Dpi { get; }
 
         /// <summary>
-        /// Gets the height of the bitmap, in pixels.
+        /// Gets the size of the bitmap, in device pixels.
         /// </summary>
-        int PixelHeight { get; }
+        PixelSize PixelSize { get; }
 
         /// <summary>
         /// Gets the platform-specific bitmap implementation.
         /// </summary>
         IRef<IBitmapImpl> PlatformImpl { get; }
 
+        /// <summary>
+        /// Gets the size of the image, in device independent pixels.
+        /// </summary>
+        /// <remarks>
+        /// Note that Skia does not currently support reading the DPI of an image so this value
+        /// will equal <see cref="PixelSize"/> on Skia.
+        /// </remarks>
+        Size Size { get; }
+
         /// <summary>
         /// Saves the bitmap to a file.
         /// </summary>

+ 17 - 12
src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs

@@ -17,12 +17,19 @@ namespace Avalonia.Media.Imaging
         /// <summary>
         /// Initializes a new instance of the <see cref="RenderTargetBitmap"/> class.
         /// </summary>
-        /// <param name="pixelWidth">The width of the bitmap in pixels.</param>
-        /// <param name="pixelHeight">The height of the bitmap in pixels.</param>
-        /// <param name="dpiX">The horizontal DPI of the bitmap.</param>
-        /// <param name="dpiY">The vertical DPI of the bitmap.</param>
-        public RenderTargetBitmap(int pixelWidth, int pixelHeight, double dpiX = 96, double dpiY = 96)
-           : this(RefCountable.Create(CreateImpl(pixelWidth, pixelHeight, dpiX, dpiY)))
+        /// <param name="pixelSize">The size of the bitmap.</param>
+        public RenderTargetBitmap(PixelSize pixelSize)
+           : this(pixelSize, new Vector(96, 96))
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RenderTargetBitmap"/> class.
+        /// </summary>
+        /// <param name="pixelSize">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
+        public RenderTargetBitmap(PixelSize pixelSize, Vector dpi)
+           : this(RefCountable.Create(CreateImpl(pixelSize, dpi)))
         {
         }
 
@@ -45,15 +52,13 @@ namespace Avalonia.Media.Imaging
         /// <summary>
         /// Creates a platform-specific implementation for a <see cref="RenderTargetBitmap"/>.
         /// </summary>
-        /// <param name="width">The width of the bitmap.</param>
-        /// <param name="height">The height of the bitmap.</param>
-        /// <param name="dpiX">The horizontal DPI of the bitmap.</param>
-        /// <param name="dpiY">The vertical DPI of the bitmap.</param>
+        /// <param name="size">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
         /// <returns>The platform-specific implementation.</returns>
-        private static IRenderTargetBitmapImpl CreateImpl(int width, int height, double dpiX, double dpiY)
+        private static IRenderTargetBitmapImpl CreateImpl(PixelSize size, Vector dpi)
         {
             IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            return factory.CreateRenderTargetBitmap(width, height, dpiX, dpiY);
+            return factory.CreateRenderTargetBitmap(size, dpi);
         }
 
         /// <inheritdoc/>

+ 9 - 2
src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs

@@ -7,8 +7,15 @@ namespace Avalonia.Media.Imaging
     /// </summary>
     public class WriteableBitmap : Bitmap
     {
-        public WriteableBitmap(int width, int height, PixelFormat? format = null) 
-            : base(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().CreateWriteableBitmap(width, height, format))
+        /// <summary>
+        /// Initializes a new instance of the <see cref="WriteableBitmap"/> class.
+        /// </summary>
+        /// <param name="size">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
+        /// <param name="format">The pixel format (optional).</param>
+        /// <returns>An <see cref="IWriteableBitmapImpl"/>.</returns>
+        public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null) 
+            : base(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().CreateWriteableBitmap(size, dpi, format))
         {
         }
         

+ 174 - 0
src/Avalonia.Visuals/Media/PixelSize.cs

@@ -0,0 +1,174 @@
+// 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.Globalization;
+using Avalonia.Utilities;
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Represents a size in device pixels.
+    /// </summary>
+    public readonly struct PixelSize
+    {
+        /// <summary>
+        /// A size representing zero
+        /// </summary>
+        public static readonly PixelSize Empty = new PixelSize(0, 0);
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PixelSize"/> structure.
+        /// </summary>
+        /// <param name="width">The width.</param>
+        /// <param name="height">The height.</param>
+        public PixelSize(int width, int height)
+        {
+            Width = width;
+            Height = height;
+        }
+
+        /// <summary>
+        /// Gets the aspect ratio of the size.
+        /// </summary>
+        public double AspectRatio => (double)Width / Height;
+
+        /// <summary>
+        /// Gets the width.
+        /// </summary>
+        public int Width { get; }
+
+        /// <summary>
+        /// Gets the height.
+        /// </summary>
+        public int Height { get; }
+
+        /// <summary>
+        /// Checks for equality between two <see cref="PixelSize"/>s.
+        /// </summary>
+        /// <param name="left">The first size.</param>
+        /// <param name="right">The second size.</param>
+        /// <returns>True if the sizes are equal; otherwise false.</returns>
+        public static bool operator ==(PixelSize left, PixelSize right)
+        {
+            return left.Width == right.Width && left.Height == right.Height;
+        }
+
+        /// <summary>
+        /// Checks for inequality between two <see cref="Size"/>s.
+        /// </summary>
+        /// <param name="left">The first size.</param>
+        /// <param name="right">The second size.</param>
+        /// <returns>True if the sizes are unequal; otherwise false.</returns>
+        public static bool operator !=(PixelSize left, PixelSize right)
+        {
+            return !(left == right);
+        }
+
+        /// <summary>
+        /// Parses a <see cref="PixelSize"/> string.
+        /// </summary>
+        /// <param name="s">The string.</param>
+        /// <returns>The <see cref="PixelSize"/>.</returns>
+        public static PixelSize Parse(string s)
+        {
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size"))
+            {
+                return new PixelSize(
+                    tokenizer.ReadInt32(),
+                    tokenizer.ReadInt32());
+            }
+        }
+
+        /// <summary>
+        /// Checks for equality between a size and an object.
+        /// </summary>
+        /// <param name="obj">The object.</param>
+        /// <returns>
+        /// True if <paramref name="obj"/> is a size that equals the current size.
+        /// </returns>
+        public override bool Equals(object obj)
+        {
+            if (obj is PixelSize other)
+            {
+                return this == other;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Returns a hash code for a <see cref="PixelSize"/>.
+        /// </summary>
+        /// <returns>The hash code.</returns>
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                int hash = 17;
+                hash = (hash * 23) + Width.GetHashCode();
+                hash = (hash * 23) + Height.GetHashCode();
+                return hash;
+            }
+        }
+
+        /// <summary>
+        /// Returns a new <see cref="PixelSize"/> with the same height and the specified width.
+        /// </summary>
+        /// <param name="width">The width.</param>
+        /// <returns>The new <see cref="PixelSize"/>.</returns>
+        public PixelSize WithWidth(int width) => new PixelSize(width, Height);
+
+        /// <summary>
+        /// Returns a new <see cref="PixelSize"/> with the same width and the specified height.
+        /// </summary>
+        /// <param name="height">The height.</param>
+        /// <returns>The new <see cref="PixelSize"/>.</returns>
+        public PixelSize WithHeight(int height) => new PixelSize(Width, height);
+
+        /// <summary>
+        /// Converts the <see cref="PixelSize"/> to a device-independent <see cref="Size"/> using the
+        /// specified dots per inch (DPI).
+        /// </summary>
+        /// <param name="dpi">The dots per inch.</param>
+        /// <returns>The device-independent size.</returns>
+        public Size ToSize(double dpi) => new Size(Width / (dpi / 96), Height / (dpi / 96));
+
+        /// <summary>
+        /// Converts the <see cref="PixelSize"/> to a device-independent <see cref="Size"/> using the
+        /// specified dots per inch (DPI).
+        /// </summary>
+        /// <param name="dpi">The dots per inch.</param>
+        /// <returns>The device-independent size.</returns>
+        public Size ToSize(Vector dpi) => new Size(Width / (dpi.X / 96), Height / (dpi.Y / 96));
+
+        /// <summary>
+        /// Converts a <see cref="Size"/> to device pixels using the specified dots per inch (DPI).
+        /// </summary>
+        /// <param name="size">The size.</param>
+        /// <param name="dpi">The dots per inch.</param>
+        /// <returns>The device-independent size.</returns>
+        public static PixelSize FromSize(Size size, double dpi) => new PixelSize(
+            (int)(size.Width * (dpi / 96)),
+            (int)(size.Height * (dpi / 96)));
+
+        /// <summary>
+        /// Converts a <see cref="Size"/> to device pixels using the specified dots per inch (DPI).
+        /// </summary>
+        /// <param name="size">The size.</param>
+        /// <param name="dpi">The dots per inch.</param>
+        /// <returns>The device-independent size.</returns>
+        public static PixelSize FromSize(Size size, Vector dpi) => new PixelSize(
+            (int)(size.Width * (dpi.X / 96)),
+            (int)(size.Height * (dpi.Y / 96)));
+
+        /// <summary>
+        /// Returns the string representation of the size.
+        /// </summary>
+        /// <returns>The string representation of the size.</returns>
+        public override string ToString()
+        {
+            return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", Width, Height);
+        }
+    }
+}

+ 4 - 4
src/Avalonia.Visuals/Platform/IBitmapImpl.cs

@@ -12,14 +12,14 @@ namespace Avalonia.Platform
     public interface IBitmapImpl : IDisposable
     {
         /// <summary>
-        /// Gets the width of the bitmap, in pixels.
+        /// Gets the dots per inch (DPI) of the image.
         /// </summary>
-        int PixelWidth { get; }
+        Vector Dpi { get; }
 
         /// <summary>
-        /// Gets the height of the bitmap, in pixels.
+        /// Gets the size of the bitmap, in device pixels.
         /// </summary>
-        int PixelHeight { get; }
+        PixelSize PixelSize { get; }
 
         /// <summary>
         /// Saves the bitmap to a file.

+ 2 - 7
src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs

@@ -10,14 +10,9 @@ namespace Avalonia.Platform
         IntPtr Address { get; }
 
         /// <summary>
-        /// Framebuffer width
+        /// Gets the framebuffer size in device pixels.
         /// </summary>
-        int Width { get; }
-        
-        /// <summary>
-        /// Framebuffer height
-        /// </summary>
-        int Height { get; }
+        PixelSize Size{ get; }
         
         /// <summary>
         /// Number of bytes per row

+ 13 - 19
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -49,25 +49,19 @@ namespace Avalonia.Platform
         /// <summary>
         /// Creates a render target bitmap implementation.
         /// </summary>
-        /// <param name="width">The width of the bitmap.</param>
-        /// <param name="height">The height of the bitmap.</param>
-        /// <param name="dpiX">The horizontal DPI of the bitmap.</param>
-        /// <param name="dpiY">The vertical DPI of the bitmap.</param>
+        /// <param name="size">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
         /// <returns>An <see cref="IRenderTargetBitmapImpl"/>.</returns>
-        IRenderTargetBitmapImpl CreateRenderTargetBitmap(
-            int width,
-            int height,
-            double dpiX,
-            double dpiY);
+        IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi);
 
         /// <summary>
         /// Creates a writeable bitmap implementation.
         /// </summary>
-        /// <param name="width">The width of the bitmap.</param>
-        /// <param name="height">The height of the bitmap.</param>
+        /// <param name="size">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
         /// <param name="format">Pixel format (optional).</param>
         /// <returns>An <see cref="IWriteableBitmapImpl"/>.</returns>
-        IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null);
+        IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null);
 
         /// <summary>
         /// Loads a bitmap implementation from a file..
@@ -84,14 +78,14 @@ namespace Avalonia.Platform
         IBitmapImpl LoadBitmap(Stream stream);
 
         /// <summary>
-        /// Loads a bitmap implementation from a pixels in memory..
+        /// Loads a bitmap implementation from a pixels in memory.
         /// </summary>
-        /// <param name="format">Pixel format</param>
-        /// <param name="data">Pointer to source bytes</param>
-        /// <param name="width">Bitmap width</param>
-        /// <param name="height">Bitmap height</param>
-        /// <param name="stride">Bytes per row</param>
+        /// <param name="format">The pixel format.</param>
+        /// <param name="data">The pointer to source bytes.</param>
+        /// <param name="size">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
+        /// <param name="stride">The number of bytes per row.</param>
         /// <returns>An <see cref="IBitmapImpl"/>.</returns>
-        IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride);
+        IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride);
     }
 }

+ 4 - 6
src/Avalonia.Visuals/Platform/LockedFramebuffer.cs

@@ -6,21 +6,19 @@ namespace Avalonia.Platform
     {
         private readonly Action _onDispose;
 
-        public LockedFramebuffer(IntPtr address, int width, int height, int rowBytes, Vector dpi, PixelFormat format,
+        public LockedFramebuffer(IntPtr address, PixelSize size, int rowBytes, Vector dpi, PixelFormat format,
             Action onDispose)
         {
             _onDispose = onDispose;
             Address = address;
-            Width = width;
-            Height = height;
+            Size = Size;
             RowBytes = rowBytes;
             Dpi = dpi;
             Format = format;
         }
 
         public IntPtr Address { get; }
-        public int Width { get; }
-        public int Height { get; }
+        public PixelSize Size { get; }
         public int RowBytes { get; }
         public Vector Dpi { get; }
         public PixelFormat Format { get; }
@@ -30,4 +28,4 @@ namespace Avalonia.Platform
             _onDispose?.Invoke();
         }
     }
-}
+}

+ 4 - 4
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -344,7 +344,7 @@ namespace Avalonia.Rendering
             foreach (var layer in scene.Layers)
             {
                 var bitmap = Layers[layer.LayerRoot].Bitmap;
-                var sourceRect = new Rect(0, 0, bitmap.Item.PixelWidth, bitmap.Item.PixelHeight);
+                var sourceRect = new Rect(0, 0, bitmap.Item.PixelSize.Width, bitmap.Item.PixelSize.Height);
 
                 if (layer.GeometryClip != null)
                 {
@@ -368,7 +368,7 @@ namespace Avalonia.Rendering
 
             if (_overlay != null)
             {
-                var sourceRect = new Rect(0, 0, _overlay.Item.PixelWidth, _overlay.Item.PixelHeight);
+                var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height);
                 context.DrawImage(_overlay, 0.5, sourceRect, clientRect);
             }
 
@@ -453,8 +453,8 @@ namespace Avalonia.Rendering
             var pixelSize = size * scaling;
 
             if (_overlay == null ||
-                _overlay.Item.PixelWidth != pixelSize.Width ||
-                _overlay.Item.PixelHeight != pixelSize.Height)
+                _overlay.Item.PixelSize.Width != pixelSize.Width ||
+                _overlay.Item.PixelSize.Height != pixelSize.Height)
             {
                 _overlay?.Dispose();
                 _overlay = RefCountable.Create(parentContext.CreateLayer(size));

+ 4 - 6
src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs

@@ -22,8 +22,7 @@ namespace Avalonia.Gtk3
             height *= _factor;
             _surface = new ManagedCairoSurface(width, height);
             
-            Width = width;
-            Height = height;
+            Size = new PixelSize(width, height);
             Address = _surface.Buffer;
             RowBytes = _surface.Stride;
             Native.CairoSurfaceFlush(_surface.Surface);
@@ -115,18 +114,17 @@ namespace Avalonia.Gtk3
                     if (_impl.CurrentCairoContext != IntPtr.Zero)
                         Draw(_impl.CurrentCairoContext, _surface.Surface, _factor);
                     else
-                        DrawToWidget(_widget, _surface.Surface, Width, Height, _factor);
+                        DrawToWidget(_widget, _surface.Surface, Size.Width, Size.Height, _factor);
                     _surface.Dispose();
                 }
                 else
-                    _impl.SetNextRenderOperation(new RenderOp(_widget, _surface, _factor, Width, Height));
+                    _impl.SetNextRenderOperation(new RenderOp(_widget, _surface, _factor, Size.Width, Size.Height));
                 _surface = null;
             }
         }
 
         public IntPtr Address { get; }
-        public int Width  { get; }
-        public int Height { get; }
+        public PixelSize Size { get; }
         public int RowBytes { get; }
 
         

+ 8 - 10
src/Gtk/Avalonia.Gtk3/X11Framebuffer.cs

@@ -13,12 +13,11 @@ namespace Avalonia.Gtk3
         {
             _display = display;
             _xid = xid;
-            Width = width*factor;
-            Height = height*factor;
-            RowBytes = Width * 4;
+            Size = new PixelSize(width * factor, height * factor);
+            RowBytes = Size.Width * 4;
             Dpi = new Vector(96, 96) * factor;
             Format = PixelFormat.Bgra8888;
-            _blob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(RowBytes * Height);
+            _blob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(RowBytes * Size.Height);
             Address = _blob.Address;
         }
         
@@ -26,8 +25,8 @@ namespace Avalonia.Gtk3
         {
             var image = new X11.XImage();
             int bitsPerPixel = 32;
-            image.width = Width;
-            image.height = Height;
+            image.width = Size.Width;
+            image.height = Size.Height;
             image.format = 2; //ZPixmap;
             image.data = Address;
             image.byte_order = 0;// LSBFirst;
@@ -35,12 +34,12 @@ namespace Avalonia.Gtk3
             image.bitmap_bit_order = 0;// LSBFirst;
             image.bitmap_pad = bitsPerPixel;
             image.depth = 24;
-            image.bytes_per_line = RowBytes - Width * 4;
+            image.bytes_per_line = RowBytes - Size.Width * 4;
             image.bits_per_pixel = bitsPerPixel;
             X11.XLockDisplay(_display);
             X11.XInitImage(ref image);
             var gc = X11.XCreateGC(_display, _xid, 0, IntPtr.Zero);
-            X11.XPutImage(_display, _xid, gc, ref image, 0, 0, 0, 0, (uint) Width, (uint) Height);
+            X11.XPutImage(_display, _xid, gc, ref image, 0, 0, 0, 0, (uint)Size.Width, (uint)Size.Height);
             X11.XFreeGC(_display, gc);
             X11.XSync(_display, true);
             X11.XUnlockDisplay(_display);
@@ -48,8 +47,7 @@ namespace Avalonia.Gtk3
         }
 
         public IntPtr Address { get; }
-        public int Width { get; }
-        public int Height { get; }
+        public PixelSize Size { get; }
         public int RowBytes { get; }
         public Vector Dpi { get; }
         public PixelFormat Format { get; }

+ 4 - 5
src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs

@@ -19,7 +19,7 @@ namespace Avalonia.LinuxFramebuffer
             _address = address;
             Dpi = dpi;
             //Use double buffering to avoid flicker
-            Address = Marshal.AllocHGlobal(RowBytes * Height);
+            Address = Marshal.AllocHGlobal(RowBytes * Size.Height);
         }
 
 
@@ -31,17 +31,16 @@ namespace Avalonia.LinuxFramebuffer
         public void Dispose()
         {
             VSync();
-            NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Height));
+            NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Size.Height));
 
             Marshal.FreeHGlobal(Address);
             Address = IntPtr.Zero;
         }
 
         public IntPtr Address { get; private set; }
-        public int Width => (int)_varInfo.xres;
-        public int Height => (int) _varInfo.yres;
+        public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres);
         public int RowBytes => (int) _fixedInfo.line_length;
         public Vector Dpi { get; }
         public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888;
     }
-}
+}

+ 5 - 7
src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs

@@ -24,12 +24,11 @@ namespace Avalonia.MonoMac
             _isDeferred = !Dispatcher.UIThread.CheckAccess();
             _logicalSize = _view.LogicalSize;
             var pixelSize = _view.PixelSize;
-            Width = (int)pixelSize.Width;
-            Height = (int)pixelSize.Height;
-            RowBytes = Width * 4;
+            Size = new PixelSize((int)pixelSize.Width, (int)pixelSize.Height);
+            RowBytes = Size.Width * 4;
             Dpi = new Vector(96 * pixelSize.Width / _logicalSize.Width, 96 * pixelSize.Height / _logicalSize.Height);
             Format = PixelFormat.Rgba8888;
-            var size = Height * RowBytes;
+            var size = Size.Height * RowBytes;
             _blob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(size);
             memset(Address, 0, new IntPtr(size));
         }
@@ -43,7 +42,7 @@ namespace Avalonia.MonoMac
             try
             {
                 using (var colorSpace = CGColorSpace.CreateDeviceRGB())
-                using (var bContext = new CGBitmapContext(Address, Width, Height, 8, Width * 4,
+                using (var bContext = new CGBitmapContext(Address, Size.Width, Size.Height, 8, Size.Width * 4,
                     colorSpace, (CGImageAlphaInfo)nfo))
                     image = bContext.ToImage();
                 lock (_view.SyncRoot)
@@ -79,8 +78,7 @@ namespace Avalonia.MonoMac
         }
 
         public IntPtr Address => _blob.Address;
-        public int Width { get; }
-        public int Height { get; }
+        public PixelSize Size { get; }
         public int RowBytes { get; }
         public Vector Dpi { get; }
         public PixelFormat Format { get; }

+ 3 - 3
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -135,7 +135,7 @@ namespace Avalonia.Skia
         public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
         {
             PushOpacityMask(opacityMask, opacityMaskRect);
-            DrawImage(source, 1, new Rect(0, 0, source.Item.PixelWidth, source.Item.PixelHeight), destRect, BitmapInterpolationMode.Default);
+            DrawImage(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default);
             PopOpacityMask();
         }
 
@@ -384,7 +384,7 @@ namespace Avalonia.Skia
         private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
         {
             var calc = new TileBrushCalculator(tileBrush,
-                    new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
+                    new Size(tileBrushImage.PixelSize.Width, tileBrushImage.PixelSize.Height), targetSize);
 
             var intermediate = CreateRenderTarget(
                 (int)calc.IntermediateSize.Width,
@@ -394,7 +394,7 @@ namespace Avalonia.Skia
 
             using (var context = intermediate.CreateDrawingContext(null))
             {
-                var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
+                var rect = new Rect(0, 0, tileBrushImage.PixelSize.Width, tileBrushImage.PixelSize.Height);
 
                 context.Clear(Colors.Transparent);
                 context.PushClip(calc.IntermediateClip);

+ 1 - 1
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@@ -41,7 +41,7 @@ namespace Avalonia.Skia
         public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
             var framebuffer = _platformSurface.Lock();
-            var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height,
+            var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height,
                 framebuffer.Format.ToSkColorType(), SKAlphaType.Premul);
 
             CreateSurface(framebufferImageInfo, framebuffer);

+ 13 - 14
src/Skia/Avalonia.Skia/ImmutableBitmap.cs

@@ -31,22 +31,24 @@ namespace Avalonia.Skia
                     throw new ArgumentException("Unable to load bitmap from provided data");
                 }
 
-                PixelWidth = _image.Width;
-                PixelHeight = _image.Height;
+                PixelSize = new PixelSize(_image.Width, _image.Height);
+
+                // TODO: Skia doesn't have an API for DPI.
+                Dpi = new Vector(96, 96);
             }
         }
 
         /// <summary>
         /// Create immutable bitmap from given pixel data copy.
         /// </summary>
-        /// <param name="width">Width of data pixels.</param>
-        /// <param name="height">Height of data pixels.</param>
+        /// <param name="size">Size of the bitmap.</param>
+        /// <param name="dpi">DPI of the bitmap.</param>
         /// <param name="stride">Stride of data pixels.</param>
         /// <param name="format">Format of data pixels.</param>
         /// <param name="data">Data pixels.</param>
-        public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data)
+        public ImmutableBitmap(PixelSize size, Vector dpi, int stride, PixelFormat format, IntPtr data)
         {
-            var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul);
+            var imageInfo = new SKImageInfo(size.Width, size.Height, format.ToSkColorType(), SKAlphaType.Premul);
 
             _image = SKImage.FromPixelCopy(imageInfo, data, stride);
 
@@ -55,15 +57,12 @@ namespace Avalonia.Skia
                 throw new ArgumentException("Unable to create bitmap from provided data");
             }
 
-            PixelWidth = width;
-            PixelHeight = height;
+            PixelSize = size;
+            Dpi = dpi;
         }
 
-        /// <inheritdoc />
-        public int PixelWidth { get; }
-
-        /// <inheritdoc />
-        public int PixelHeight { get; }
+        public Vector Dpi { get; }
+        public PixelSize PixelSize { get; }
 
         /// <inheritdoc />
         public void Dispose()
@@ -89,4 +88,4 @@ namespace Avalonia.Skia
             context.Canvas.DrawImage(_image, sourceRect, destRect, paint);
         }
     }
-}
+}

+ 12 - 18
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -49,34 +49,28 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
         {
-            return new ImmutableBitmap(width, height, stride, format, data);
+            return new ImmutableBitmap(size, dpi, stride, format, data);
         }
 
         /// <inheritdoc />
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
-            int width,
-            int height,
-            double dpiX,
-            double dpiY)
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
         {
-            if (width < 1)
+            if (size.Width < 1)
             {
-                throw new ArgumentException("Width can't be less than 1", nameof(width));
+                throw new ArgumentException("Width can't be less than 1", nameof(size));
             }
 
-            if (height < 1)
+            if (size.Height < 1)
             {
-                throw new ArgumentException("Height can't be less than 1", nameof(height));
+                throw new ArgumentException("Height can't be less than 1", nameof(size));
             }
 
-            var dpi = new Vector(dpiX, dpiY);
-
             var createInfo = new SurfaceRenderTarget.CreateInfo
             {
-                Width = width,
-                Height = height,
+                Width = size.Width,
+                Height = size.Height,
                 Dpi = dpi,
                 DisableTextLcdRendering = false
             };
@@ -100,9 +94,9 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null)
+        public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null)
         {
-            return new WriteableBitmapImpl(width, height, format);
+            return new WriteableBitmapImpl(size, dpi, format);
         }
     }
-}
+}

+ 8 - 10
src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs

@@ -15,7 +15,6 @@ namespace Avalonia.Skia
     /// </summary>
     public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl
     {
-        private readonly Vector _dpi;
         private readonly SKSurface _surface;
         private readonly SKCanvas _canvas;
         private readonly bool _disableLcdRendering;
@@ -26,12 +25,11 @@ namespace Avalonia.Skia
         /// <param name="createInfo">Create info.</param>
         public SurfaceRenderTarget(CreateInfo createInfo)
         {
-            PixelWidth = createInfo.Width;
-            PixelHeight = createInfo.Height;
-            _dpi = createInfo.Dpi;
-            _disableLcdRendering = createInfo.DisableTextLcdRendering;
+            PixelSize = new PixelSize(createInfo.Width, createInfo.Height);
+            Dpi = createInfo.Dpi;
 
-            _surface = CreateSurface(PixelWidth, PixelHeight, createInfo.Format);
+            _disableLcdRendering = createInfo.DisableTextLcdRendering;
+            _surface = CreateSurface(PixelSize.Width, PixelSize.Height, createInfo.Format);
 
             _canvas = _surface?.Canvas;
 
@@ -71,7 +69,7 @@ namespace Avalonia.Skia
             var createInfo = new DrawingContextImpl.CreateInfo
             {
                 Canvas = _canvas,
-                Dpi = _dpi,
+                Dpi = Dpi,
                 VisualBrushRenderer = visualBrushRenderer,
                 DisableTextLcdRendering = _disableLcdRendering
             };
@@ -80,10 +78,10 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public int PixelWidth { get; }
+        public Vector Dpi { get; }
 
         /// <inheritdoc />
-        public int PixelHeight { get; }
+        public PixelSize PixelSize { get; }
 
         /// <inheritdoc />
         public void Save(string fileName)
@@ -166,4 +164,4 @@ namespace Avalonia.Skia
             public bool DisableTextLcdRendering;
         }
     }
-}
+}

+ 12 - 16
src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs

@@ -20,13 +20,13 @@ namespace Avalonia.Skia
         /// <summary>
         /// Create new writeable bitmap.
         /// </summary>
-        /// <param name="width">Width.</param>
-        /// <param name="height">Height.</param>
-        /// <param name="format">Format.</param>
-        public WriteableBitmapImpl(int width, int height, PixelFormat? format = null)
+        /// <param name="size">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
+        /// <param name="format">The pixel format.</param>
+        public WriteableBitmapImpl(PixelSize size, Vector dpi, PixelFormat? format = null)
         {
-            PixelHeight = height;
-            PixelWidth = width;
+            PixelSize = size;
+            Dpi = dpi;
 
             var colorType = PixelFormatHelper.ResolveColorType(format);
             
@@ -36,24 +36,23 @@ namespace Avalonia.Skia
             {
                 _bitmap = new SKBitmap();
 
-                var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
+                var nfo = new SKImageInfo(size.Width, size.Height, colorType, SKAlphaType.Premul);
                 var blob = runtimePlatform.AllocBlob(nfo.BytesSize);
 
                 _bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, s_releaseDelegate, blob);
             }
             else
             {
-                _bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul);
+                _bitmap = new SKBitmap(size.Width, size.Height, colorType, SKAlphaType.Premul);
             }
 
             _bitmap.Erase(SKColor.Empty);
         }
 
-        /// <inheritdoc />
-        public int PixelWidth { get; }
+        public Vector Dpi { get; }
 
         /// <inheritdoc />
-        public int PixelHeight { get; }
+        public PixelSize PixelSize { get; }
 
         /// <inheritdoc />
         public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
@@ -133,10 +132,7 @@ namespace Avalonia.Skia
             public IntPtr Address => _bitmap.GetPixels();
 
             /// <inheritdoc />
-            public int Width => _bitmap.Width;
-
-            /// <inheritdoc />
-            public int Height => _bitmap.Height;
+            public PixelSize Size => new PixelSize(_bitmap.Width, _bitmap.Height);
 
             /// <inheritdoc />
             public int RowBytes => _bitmap.RowBytes;
@@ -148,4 +144,4 @@ namespace Avalonia.Skia
             public PixelFormat Format => _bitmap.ColorType.ToPixelFormat();
         }
     }
-}
+}

+ 8 - 12
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -110,9 +110,9 @@ namespace Avalonia.Direct2D1
             SharpDX.Configuration.EnableReleaseOnFinalizer = true;
         }
 
-        public IBitmapImpl CreateBitmap(int width, int height)
+        public IBitmapImpl CreateBitmap(PixelSize size, Vector dpi)
         {
-            return new WicBitmapImpl(width, height);
+            return new WicBitmapImpl(size, dpi);
         }
 
         public IFormattedTextImpl CreateFormattedText(
@@ -159,18 +159,14 @@ namespace Avalonia.Direct2D1
             throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from any of provided surfaces");
         }
 
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
-            int width,
-            int height,
-            double dpiX,
-            double dpiY)
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
         {
-            return new WicRenderTargetBitmapImpl(width, height, dpiX, dpiY);
+            return new WicRenderTargetBitmapImpl(size, dpi);
         }
 
-        public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null)
+        public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null)
         {
-            return new WriteableWicBitmapImpl(width, height, format);
+            return new WriteableWicBitmapImpl(size, dpi, format);
         }
 
         public IStreamGeometryImpl CreateStreamGeometry()
@@ -188,9 +184,9 @@ namespace Avalonia.Direct2D1
             return new WicBitmapImpl(stream);
         }
 
-        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
         {
-            return new WicBitmapImpl(format, data, width, height, stride);
+            return new WicBitmapImpl(format, data, size, dpi, stride);
         }
     }
 }

+ 2 - 2
src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs

@@ -40,7 +40,7 @@ namespace Avalonia.Direct2D1
             private readonly ILockedFramebuffer _target;
 
             public FramebufferShim(ILockedFramebuffer target) : 
-                base(target.Width, target.Height, target.Dpi.X, target.Dpi.Y, target.Format)
+                base(target.Size, target.Dpi, target.Format)
             {
                 _target = target;
             }
@@ -51,7 +51,7 @@ namespace Avalonia.Direct2D1
                 {
                     using (var l = WicImpl.Lock(BitmapLockFlags.Read))
                     {
-                        for (var y = 0; y < _target.Height; y++)
+                        for (var y = 0; y < _target.Size.Height; y++)
                         {
                             UnmanagedMethods.CopyMemory(
                                 (_target.Address + _target.RowBytes * y),

+ 2 - 6
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -330,12 +330,8 @@ namespace Avalonia.Direct2D1.Media
             {
                 var platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
                 var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height);
-                var pixelSize = size * (dpi / 96);
-                return platform.CreateRenderTargetBitmap(
-                    (int)pixelSize.Width,
-                    (int)pixelSize.Height,
-                    dpi.X,
-                    dpi.Y);
+                var pixelSize = PixelSize.FromSize(size, dpi);
+                return platform.CreateRenderTargetBitmap(pixelSize, dpi);
             }
         }
 

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Direct2D1.Media
             BitmapImpl bitmap,
             Size targetSize)
         {
-            var calc = new TileBrushCalculator(brush, new Size(bitmap.PixelWidth, bitmap.PixelHeight), targetSize);
+            var calc = new TileBrushCalculator(brush, bitmap.PixelSize.ToSize(96), targetSize);
 
             if (!calc.NeedsIntermediate)
             {
@@ -99,7 +99,7 @@ namespace Avalonia.Direct2D1.Media
 
             using (var context = new RenderTarget(result).CreateDrawingContext(null))
             {
-                var rect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
+                var rect = new Rect(0, 0, bitmap.PixelSize.Width, bitmap.PixelSize.Height);
 
                 context.Clear(Colors.Transparent);
                 context.PushClip(calc.IntermediateClip);

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs

@@ -7,8 +7,8 @@ namespace Avalonia.Direct2D1.Media
 {
     public abstract class BitmapImpl : IBitmapImpl, IDisposable
     {
-        public abstract int PixelWidth { get; }
-        public abstract int PixelHeight { get; }
+        public abstract Vector Dpi { get; }
+        public abstract PixelSize PixelSize { get; }
 
         public abstract OptionalDispose<D2DBitmap> GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target);
 

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs

@@ -30,8 +30,8 @@ namespace Avalonia.Direct2D1.Media
             _direct2DBitmap = d2DBitmap ?? throw new ArgumentNullException(nameof(d2DBitmap));
         }
 
-        public override int PixelWidth => _direct2DBitmap.PixelSize.Width;
-        public override int PixelHeight => _direct2DBitmap.PixelSize.Height;
+        public override Vector Dpi => _direct2DBitmap.DotsPerInch.ToAvaloniaVector();
+        public override PixelSize PixelSize => _direct2DBitmap.PixelSize.ToAvalonia();
 
         public override void Dispose()
         {

+ 0 - 3
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs

@@ -19,9 +19,6 @@ namespace Avalonia.Direct2D1.Media.Imaging
             _renderTarget = renderTarget;
         }
 
-        public override int PixelWidth => _renderTarget.PixelSize.Width;
-        public override int PixelHeight => _renderTarget.PixelSize.Height;
-
         public static D2DRenderTargetBitmapImpl CreateCompatible(
             SharpDX.Direct2D1.RenderTarget renderTarget,
             Size size)

+ 21 - 17
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs

@@ -42,10 +42,10 @@ namespace Avalonia.Direct2D1.Media
         /// <summary>
         /// Initializes a new instance of the <see cref="WicBitmapImpl"/> class.
         /// </summary>
-        /// <param name="width">The width of the bitmap.</param>
-        /// <param name="height">The height of the bitmap.</param>
+        /// <param name="size">The size of the bitmap in device pixels.</param>
+        /// <param name="dpi">The DPI of the bitmap.</param>
         /// <param name="pixelFormat">Pixel format</param>
-        public WicBitmapImpl(int width, int height, APixelFormat? pixelFormat = null)
+        public WicBitmapImpl(PixelSize size, Vector dpi, APixelFormat? pixelFormat = null)
         {
             if (!pixelFormat.HasValue)
             {
@@ -55,19 +55,22 @@ namespace Avalonia.Direct2D1.Media
             PixelFormat = pixelFormat;
             WicImpl = new Bitmap(
                 Direct2D1Platform.ImagingFactory,
-                width,
-                height,
+                size.Width,
+                size.Height,
                 pixelFormat.Value.ToWic(),
                 BitmapCreateCacheOption.CacheOnLoad);
+            WicImpl.SetResolution(dpi.X, dpi.Y);
         }
 
-        public WicBitmapImpl(APixelFormat format, IntPtr data, int width, int height, int stride)
+        public WicBitmapImpl(APixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
         {
-            WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, width, height, format.ToWic(), BitmapCreateCacheOption.CacheOnDemand);
+            WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, size.Width, size.Height, format.ToWic(), BitmapCreateCacheOption.CacheOnDemand);
+            WicImpl.SetResolution(dpi.X, dpi.Y);
+
             PixelFormat = format;
             using (var l = WicImpl.Lock(BitmapLockFlags.Write))
             {
-                for (var row = 0; row < height; row++)
+                for (var row = 0; row < size.Height; row++)
                 {
                     UnmanagedMethods.CopyMemory(
                         (l.Data.DataPointer + row * l.Stride),
@@ -77,17 +80,18 @@ namespace Avalonia.Direct2D1.Media
             }
         }
 
-        protected APixelFormat? PixelFormat { get; }
+        public override Vector Dpi
+        {
+            get
+            {
+                WicImpl.GetResolution(out double x, out double y);
+                return new Vector(x, y);
+            }
+        }
 
-        /// <summary>
-        /// Gets the width of the bitmap, in pixels.
-        /// </summary>
-        public override int PixelWidth => WicImpl.Size.Width;
+        public override PixelSize PixelSize => WicImpl.Size.ToAvalonia();
 
-        /// <summary>
-        /// Gets the height of the bitmap, in pixels.
-        /// </summary>
-        public override int PixelHeight => WicImpl.Size.Height;
+        protected APixelFormat? PixelFormat { get; }
 
         public override void Dispose()
         {

+ 5 - 7
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs

@@ -13,17 +13,15 @@ namespace Avalonia.Direct2D1.Media
         private readonly WicRenderTarget _renderTarget;
 
         public WicRenderTargetBitmapImpl(
-            int width,
-            int height,
-            double dpiX,
-            double dpiY,
+            PixelSize size,
+            Vector dpi,
             Platform.PixelFormat? pixelFormat = null)
-            : base(width, height, pixelFormat)
+            : base(size, dpi, pixelFormat)
         {
             var props = new RenderTargetProperties
             {
-                DpiX = (float)dpiX,
-                DpiY = (float)dpiY,
+                DpiX = (float)dpi.X,
+                DpiY = (float)dpi.Y,
             };
 
             _renderTarget = new WicRenderTarget(

+ 3 - 4
src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs

@@ -10,8 +10,8 @@ namespace Avalonia.Direct2D1.Media.Imaging
 {
     class WriteableWicBitmapImpl : WicBitmapImpl, IWriteableBitmapImpl
     {
-        public WriteableWicBitmapImpl(int width, int height, PixelFormat? pixelFormat) 
-            : base(width, height, pixelFormat)
+        public WriteableWicBitmapImpl(PixelSize size, Vector dpi, PixelFormat? pixelFormat) 
+            : base(size, dpi, pixelFormat)
         {
         }
 
@@ -33,8 +33,7 @@ namespace Avalonia.Direct2D1.Media.Imaging
             }
 
             public IntPtr Address => _lock.Data.DataPointer;
-            public int Width => _lock.Size.Width;
-            public int Height => _lock.Size.Height;
+            public PixelSize Size => _lock.Size.ToAvalonia();
             public int RowBytes => _lock.Stride;
             public Vector Dpi { get; } = new Vector(96, 96);
             public PixelFormat Format => _format;

+ 4 - 0
src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs

@@ -41,6 +41,10 @@ namespace Avalonia.Direct2D1
             return new Rect(new Point(r.Left, r.Top), new Point(r.Right, r.Bottom));
         }
 
+        public static PixelSize ToAvalonia(this Size2 p) => new PixelSize(p.Width, p.Height);
+
+        public static Vector ToAvaloniaVector(this Size2F p) => new Vector(p.Width, p.Height);
+
         public static RawRectangleF ToSharpDX(this Rect r)
         {
             return new RawRectangleF((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom);

+ 1 - 2
src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs

@@ -58,8 +58,7 @@ namespace Avalonia.Win32.Interop.Wpf
             }
 
             public IntPtr Address => _bitmap.BackBuffer;
-            public int Width => _bitmap.PixelWidth;
-            public int Height => _bitmap.PixelHeight;
+            public PixelSize Size => new PixelSize(_bitmap.PixelWidth, _bitmap.PixelHeight);
             public int RowBytes => _bitmap.BackBufferStride;
             public Vector Dpi { get; }
             public PixelFormat Format => PixelFormat.Bgra8888;

+ 2 - 2
src/Windows/Avalonia.Win32/FramebufferManager.cs

@@ -21,11 +21,11 @@ namespace Avalonia.Win32
             UnmanagedMethods.GetClientRect(_hwnd, out rc);
             var width = rc.right - rc.left;
             var height = rc.bottom - rc.top;
-            if ((_fb == null || _fb.Width != width || _fb.Height != height) && width > 0 && height > 0)
+            if ((_fb == null || _fb.Size.Width != width || _fb.Size.Height != height) && width > 0 && height > 0)
             {
                 _fb?.Deallocate();
                 _fb = null;
-                _fb = new WindowFramebuffer(_hwnd, width, height);
+                _fb = new WindowFramebuffer(_hwnd, new PixelSize(width, height));
             }
             return _fb;
         }

+ 13 - 15
src/Windows/Avalonia.Win32/WindowFramebuffer.cs

@@ -11,21 +11,21 @@ namespace Avalonia.Win32
         private IUnmanagedBlob _bitmapBlob;
         private UnmanagedMethods.BITMAPINFOHEADER _bmpInfo;
 
-        public WindowFramebuffer(IntPtr handle, int width, int height)
+        public WindowFramebuffer(IntPtr handle, PixelSize size)
         {
             
-            if (width <= 0)
-                throw new ArgumentException("width is less than zero");
-            if (height <= 0)
-                throw new ArgumentException("height is less than zero");
+            if (size.Width <= 0)
+                throw new ArgumentException("Width is less than zero");
+            if (size.Height <= 0)
+                throw new ArgumentException("Height is less than zero");
             _handle = handle;
             _bmpInfo.Init();
             _bmpInfo.biPlanes = 1;
             _bmpInfo.biBitCount = 32;
             _bmpInfo.Init();
-            _bmpInfo.biWidth = width;
-            _bmpInfo.biHeight = -height;
-            _bitmapBlob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(width * height * 4);
+            _bmpInfo.biWidth = size.Width;
+            _bmpInfo.biHeight = -size.Height;
+            _bitmapBlob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(size.Width * size.Height * 4);
         }
 
         ~WindowFramebuffer()
@@ -34,7 +34,7 @@ namespace Avalonia.Win32
         }
 
         public IntPtr Address => _bitmapBlob.Address;
-        public int RowBytes => Width * 4;
+        public int RowBytes => Size.Width * 4;
         public PixelFormat Format => PixelFormat.Bgra8888;
 
         public Vector Dpi
@@ -61,19 +61,17 @@ namespace Avalonia.Win32
             }
         }
 
-        public int Width => _bmpInfo.biWidth;
-
-        public int Height => -_bmpInfo.biHeight;
+        public PixelSize Size => new PixelSize(_bmpInfo.biWidth, _bmpInfo.biHeight);
 
         public void DrawToDevice(IntPtr hDC, int destX = 0, int destY = 0, int srcX = 0, int srcY = 0, int width = -1,
             int height = -1)
         {
             if (width == -1)
-                width = Width;
+                width = Size.Width;
             if (height == -1)
-                height = Height;
+                height = Size.Height;
             UnmanagedMethods.SetDIBitsToDevice(hDC, destX, destY, (uint) width, (uint) height, srcX, srcY,
-                0, (uint)Height, _bitmapBlob.Address, ref _bmpInfo, 0);
+                0, (uint)Size.Height, _bitmapBlob.Address, ref _bmpInfo, 0);
         }
 
         public bool DrawToWindow(IntPtr hWnd, int destX = 0, int destY = 0, int srcX = 0, int srcY = 0, int width = -1,

+ 4 - 4
tests/Avalonia.Controls.UnitTests/ImageTests.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Measure_Should_Return_Correct_Size_For_No_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelWidth == 50 && x.PixelHeight == 100);
+            var bitmap = Mock.Of<IBitmap>(x => x.Size == new Size(50, 100));
             var target = new Image();
             target.Stretch = Stretch.None;
             target.Source = bitmap;
@@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Measure_Should_Return_Correct_Size_For_Fill_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelWidth == 50 && x.PixelHeight == 100);
+            var bitmap = Mock.Of<IBitmap>(x => x.Size == new Size(50, 100));
             var target = new Image();
             target.Stretch = Stretch.Fill;
             target.Source = bitmap;
@@ -39,7 +39,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Measure_Should_Return_Correct_Size_For_Uniform_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelWidth == 50 && x.PixelHeight == 100);
+            var bitmap = Mock.Of<IBitmap>(x => x.Size == new Size(50, 100));
             var target = new Image();
             target.Stretch = Stretch.Uniform;
             target.Source = bitmap;
@@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Measure_Should_Return_Correct_Size_For_UniformToFill_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelWidth == 50 && x.PixelHeight == 100);
+            var bitmap = Mock.Of<IBitmap>(x => x.Size == new Size(50, 100));
             var target = new Image();
             target.Stretch = Stretch.UniformToFill;
             target.Source = bitmap;

+ 9 - 12
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@@ -29,14 +29,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media
 
         class Framebuffer : ILockedFramebuffer, IFramebufferPlatformSurface
         {
-            public Framebuffer(PixelFormat fmt, int width, int height)
+            public Framebuffer(PixelFormat fmt, PixelSize size)
             {
                 Format = fmt;
                 var bpp = fmt == PixelFormat.Rgb565 ? 2 : 4;
-                Width = width;
-                Height = height;
-                RowBytes = bpp * width;
-                Address = Marshal.AllocHGlobal(Height * RowBytes);
+                Size = size;
+                RowBytes = bpp * size.Width;
+                Address = Marshal.AllocHGlobal(size.Height * RowBytes);
             }
 
             public IntPtr Address { get; }
@@ -45,12 +44,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media
 
             public PixelFormat Format { get; }
 
-            public int Height { get; }
+            public PixelSize Size { get; }
 
             public int RowBytes { get; }
 
-            public int Width { get; }
-
             public void Dispose()
             {
                 //no-op
@@ -74,7 +71,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         public void FramebufferRenderResultsShouldBeUsableAsBitmap(PixelFormat fmt)
         {
             var testName = nameof(FramebufferRenderResultsShouldBeUsableAsBitmap) + "_" + fmt;
-            var fb = new Framebuffer(fmt, 80, 80);
+            var fb = new Framebuffer(fmt, new PixelSize(80, 80));
             var r = Avalonia.AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
             using (var target = r.CreateRenderTarget(new object[] { fb }))
             using (var ctx = target.CreateDrawingContext(null))
@@ -87,9 +84,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                 ctx.PopOpacity();
             }
 
-            var bmp = new Bitmap(fmt, fb.Address, fb.Width, fb.Height, fb.RowBytes);
+            var bmp = new Bitmap(fmt, fb.Address, fb.Size, new Vector(96, 96), fb.RowBytes);
             fb.Deallocate();
-            using (var rtb = new RenderTargetBitmap(100, 100))
+            using (var rtb = new RenderTargetBitmap(new PixelSize(100, 100), new Vector(96, 96)))
             {
                 using (var ctx = rtb.CreateDrawingContext(null))
                 {
@@ -108,7 +105,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         [InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgba8888)]
         public void WriteableBitmapShouldBeUsable(PixelFormat fmt)
         {
-            var writeableBitmap = new WriteableBitmap(256, 256, fmt);
+            var writeableBitmap = new WriteableBitmap(new PixelSize(256, 256), new Vector(96, 96), fmt);
 
             var data = new int[256 * 256];
             for (int y = 0; y < 256; y++)

+ 5 - 6
tests/Avalonia.RenderTests/TestBase.cs

@@ -73,22 +73,21 @@ namespace Avalonia.Direct2D1.RenderTests
             var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png");
             var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png");
             var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+            var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
+            var size = new Size(target.Width, target.Height);
+            var dpi = new Vector(96, 96);
 
-            using (RenderTargetBitmap bitmap = new RenderTargetBitmap(
-                (int)target.Width,
-                (int)target.Height))
+            using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpi))
             {
-                Size size = new Size(target.Width, target.Height);
                 target.Measure(size);
                 target.Arrange(new Rect(size));
                 bitmap.Render(target);
                 bitmap.Save(immediatePath);
             }
 
-            using (var rtb = factory.CreateRenderTargetBitmap((int)target.Width, (int)target.Height, 96, 96))
+            using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpi))
             using (var renderer = new DeferredRenderer(target, rtb))
             {
-                Size size = new Size(target.Width, target.Height);
                 target.Measure(size);
                 target.Arrange(new Rect(size));
                 renderer.UnitTestUpdateScene();

+ 11 - 7
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -25,11 +25,7 @@ namespace Avalonia.UnitTests
             return Mock.Of<IRenderTarget>();
         }
 
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
-            int width,
-            int height,
-            double dpiX,
-            double dpiY)
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
         {
             return Mock.Of<IRenderTargetBitmapImpl>();
         }
@@ -39,7 +35,10 @@ namespace Avalonia.UnitTests
             return new MockStreamGeometryImpl();
         }
 
-        public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = default(PixelFormat?))
+        public IWriteableBitmapImpl CreateWriteableBitmap(
+            PixelSize size,
+            Vector dpi,
+            PixelFormat? format = default(PixelFormat?))
         {
             throw new NotImplementedException();
         }
@@ -54,7 +53,12 @@ namespace Avalonia.UnitTests
             return Mock.Of<IBitmapImpl>();
         }
 
-        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        public IBitmapImpl LoadBitmap(
+            PixelFormat format,
+            IntPtr data,
+            PixelSize size,
+            Vector dpi,
+            int stride)
         {
             throw new NotImplementedException();
         }

+ 3 - 3
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height, double dpiX, double dpiY)
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
         {
             throw new NotImplementedException();
         }
@@ -44,12 +44,12 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
-        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
         {
             throw new NotImplementedException();
         }
 
-        public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? fmt)
+        public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt)
         {
             throw new NotImplementedException();
         }