Browse Source

Merge pull request #2115 from AvaloniaUI/fixes/1614-tile-brush-dpi-compatibility

Fix tilebrush scaling
Steven Kirk 7 years ago
parent
commit
bf0baa64db
21 changed files with 353 additions and 33 deletions
  1. 2 2
      src/Avalonia.Visuals/Media/PixelSize.cs
  2. 2 2
      src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs
  3. 26 22
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  4. 7 1
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  5. 4 2
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  6. 19 0
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
  7. 13 0
      src/Windows/Avalonia.Direct2D1/Utils/DebugUtils.cs
  8. 276 0
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  9. 4 4
      tests/Avalonia.RenderTests/TestBase.cs
  10. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png
  11. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png
  12. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png
  13. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png
  14. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png
  15. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png
  16. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png
  17. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png
  18. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png
  19. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png
  20. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png
  21. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png

+ 2 - 2
src/Avalonia.Visuals/Media/PixelSize.cs

@@ -159,8 +159,8 @@ namespace Avalonia
         /// <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)));
+            (int)Math.Ceiling(size.Width * (dpi.X / 96)),
+            (int)Math.Ceiling(size.Height * (dpi.Y / 96)));
 
         /// <summary>
         /// Returns the string representation of the size.

+ 2 - 2
src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs

@@ -116,8 +116,8 @@ namespace Avalonia.Rendering.Utilities
                     return true;
                 if (SourceRect.Size.AspectRatio == _imageSize.AspectRatio)
                     return false;
-                if ((int)SourceRect.Width != _imageSize.Width ||
-                    (int)SourceRect.Height != _imageSize.Height)
+                if (SourceRect.Width != _imageSize.Width ||
+                    SourceRect.Height != _imageSize.Height)
                     return true;
                 return false;
             }

+ 26 - 22
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -225,10 +225,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public IRenderTargetBitmapImpl CreateLayer(Size size)
         {
-            var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
-            var pixelSize = size * normalizedDpi;
-
-            return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi);
+            return CreateRenderTarget(size);
         }
 
         /// <inheritdoc />
@@ -387,26 +384,27 @@ namespace Avalonia.Skia
         /// <param name="targetSize">Target size.</param>
         /// <param name="tileBrush">Tile brush to use.</param>
         /// <param name="tileBrushImage">Tile brush image.</param>
-        /// <param name="interpolationMode">The bitmap interpolation mode.</param>
         private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
         {
-            var calc = new TileBrushCalculator(tileBrush,
-                    new Size(tileBrushImage.PixelSize.Width, tileBrushImage.PixelSize.Height), targetSize);
-
-            var intermediate = CreateRenderTarget(
-                (int)calc.IntermediateSize.Width,
-                (int)calc.IntermediateSize.Height, _dpi);
+            var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSize(_dpi), targetSize);
+            var intermediate = CreateRenderTarget(calc.IntermediateSize);
 
             paintWrapper.AddDisposable(intermediate);
 
             using (var context = intermediate.CreateDrawingContext(null))
             {
-                var rect = new Rect(0, 0, tileBrushImage.PixelSize.Width, tileBrushImage.PixelSize.Height);
+                var sourceRect = new Rect(tileBrushImage.PixelSize.ToSize(96));
+                var targetRect = new Rect(tileBrushImage.PixelSize.ToSize(_dpi));
 
                 context.Clear(Colors.Transparent);
                 context.PushClip(calc.IntermediateClip);
                 context.Transform = calc.IntermediateTransform;
-                context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect, tileBrush.BitmapInterpolationMode);
+                context.DrawImage(
+                    RefCountable.CreateUnownedNotClonable(tileBrushImage),
+                    1,
+                    sourceRect,
+                    targetRect,
+                    tileBrush.BitmapInterpolationMode);
                 context.PopClip();
             }
 
@@ -433,7 +431,14 @@ namespace Avalonia.Skia
             var image = intermediate.SnapshotImage();
             paintWrapper.AddDisposable(image);
 
-            using (var shader = image.ToShader(tileX, tileY, tileTransform))
+            var paintTransform = default(SKMatrix);
+
+            SKMatrix.Concat(
+                ref paintTransform,
+                tileTransform,
+                SKMatrix.MakeScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y)));
+
+            using (var shader = image.ToShader(tileX, tileY, paintTransform))
             {
                 paintWrapper.Paint.Shader = shader;
             }
@@ -457,7 +462,7 @@ namespace Avalonia.Skia
 
             if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
             {
-                var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
+                var intermediate = CreateRenderTarget(intermediateSize);
 
                 using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer))
                 {
@@ -609,18 +614,17 @@ namespace Avalonia.Skia
         /// <summary>
         /// Create new render target compatible with this drawing context.
         /// </summary>
-        /// <param name="width">Width.</param>
-        /// <param name="height">Height.</param>
-        /// <param name="dpi">Drawing dpi.</param>
+        /// <param name="size">The size of the render target in DIPs.</param>
         /// <param name="format">Pixel format.</param>
         /// <returns></returns>
-        private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null)
+        private SurfaceRenderTarget CreateRenderTarget(Size size, PixelFormat? format = null)
         {
+            var pixelSize = PixelSize.FromSize(size, _dpi);
             var createInfo = new SurfaceRenderTarget.CreateInfo
             {
-                Width = width,
-                Height = height,
-                Dpi = dpi,
+                Width = pixelSize.Width,
+                Height = pixelSize.Height,
+                Dpi = _dpi,
                 Format = format,
                 DisableTextLcdRendering = !_canTextUseLcdRendering,
                 GrContext = _grContext

+ 7 - 1
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -434,10 +434,16 @@ namespace Avalonia.Direct2D1.Media
 
                     if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
                     {
+                        // We need to ensure the size we're requesting is an integer pixel size, otherwise
+                        // D2D alters the DPI of the render target, which messes stuff up. PixelSize.FromSize
+                        // will do the rounding for us.
+                        var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height);
+                        var pixelSize = PixelSize.FromSize(intermediateSize, dpi);
+
                         using (var intermediate = new BitmapRenderTarget(
                             _deviceContext,
                             CompatibleRenderTargetOptions.None,
-                            intermediateSize.ToSharpDX()))
+                            pixelSize.ToSize(dpi).ToSharpDX()))
                         {
                             using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer))
                             {

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

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

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

@@ -1,8 +1,10 @@
 // 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.IO;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Utilities;
 using SharpDX;
 using SharpDX.Direct2D1;
 using D2DBitmap = SharpDX.Direct2D1.Bitmap;
@@ -49,5 +51,22 @@ namespace Avalonia.Direct2D1.Media.Imaging
         {
             return new OptionalDispose<D2DBitmap>(_renderTarget.Bitmap, false);
         }
+
+        public override void Save(Stream stream)
+        {
+            using (var wic = new WicRenderTargetBitmapImpl(PixelSize, Dpi))
+            {
+                using (var dc = wic.CreateDrawingContext(null))
+                {
+                    dc.DrawImage(
+                        RefCountable.CreateUnownedNotClonable(this),
+                        1,
+                        new Rect(PixelSize.ToSize(Dpi.X)),
+                        new Rect(PixelSize.ToSize(Dpi.X)));
+                }
+
+                wic.Save(stream);
+            }
+        }
     }
 }

+ 13 - 0
src/Windows/Avalonia.Direct2D1/Utils/DebugUtils.cs

@@ -0,0 +1,13 @@
+using Avalonia.Direct2D1.Media.Imaging;
+
+namespace Avalonia.Direct2D1.Utils
+{
+    internal static class DebugUtils
+    {
+        public static void Save(SharpDX.Direct2D1.BitmapRenderTarget bitmap, string filename)
+        {
+            var rtb = new D2DRenderTargetBitmapImpl(bitmap);
+            rtb.Save(filename);
+        }
+    }
+}

+ 276 - 0
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@@ -449,5 +449,281 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             await RenderToFile(target);
             CompareImages();
         }
+
+        [Fact]
+        public async Task VisualBrush_Grip_96_Dpi()
+        {
+            var target = new Border
+            {
+                Width = 100,
+                Height = 10,
+                Background = new VisualBrush
+                {
+                    SourceRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute),
+                    DestinationRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute),
+                    TileMode = TileMode.Tile,
+                    Stretch = Stretch.UniformToFill,
+                    Visual = new Canvas
+                    {
+                        Width = 4,
+                        Height = 5,
+                        Background = Brushes.WhiteSmoke,
+                        Children =
+                        {
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.LeftProperty] = 2,
+                            },
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.TopProperty] = 2,
+                            },
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.LeftProperty] = 2,
+                                [Canvas.TopProperty] = 4,
+                            }
+                        }
+                    }
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task VisualBrush_Grip_144_Dpi()
+        {
+            var target = new Border
+            {
+                Width = 100,
+                Height = 7.5,
+                Background = new VisualBrush
+                {
+                    SourceRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute),
+                    DestinationRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute),
+                    TileMode = TileMode.Tile,
+                    Stretch = Stretch.UniformToFill,
+                    Visual = new Canvas
+                    {
+                        Width = 4,
+                        Height = 5,
+                        Background = Brushes.WhiteSmoke,
+                        Children =
+                        {
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.LeftProperty] = 2,
+                            },
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.TopProperty] = 2,
+                            },
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.LeftProperty] = 2,
+                                [Canvas.TopProperty] = 4,
+                            }
+                        }
+                    }
+                }
+            };
+
+            await RenderToFile(target, dpi: 144);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task VisualBrush_Grip_192_Dpi()
+        {
+            var target = new Border
+            {
+                Width = 100,
+                Height = 10,
+                Background = new VisualBrush
+                {
+                    SourceRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute),
+                    DestinationRect = new RelativeRect(0, 0, 4, 5, RelativeUnit.Absolute),
+                    TileMode = TileMode.Tile,
+                    Stretch = Stretch.UniformToFill,
+                    Visual = new Canvas
+                    {
+                        Width = 4,
+                        Height = 5,
+                        Background = Brushes.WhiteSmoke,
+                        Children =
+                        {
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.LeftProperty] = 2,
+                            },
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.TopProperty] = 2,
+                            },
+                            new Rectangle
+                            {
+                                Width = 1,
+                                Height = 1,
+                                Fill = Brushes.Red,
+                                [Canvas.LeftProperty] = 2,
+                                [Canvas.TopProperty] = 4,
+                            }
+                        }
+                    }
+                }
+            };
+
+            await RenderToFile(target, dpi: 192);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task VisualBrush_Checkerboard_96_Dpi()
+        {
+            var target = new Border
+            {
+                Width = 200,
+                Height = 200,
+                Background = new VisualBrush
+                {
+                    DestinationRect = new RelativeRect(0, 0, 16, 16, RelativeUnit.Absolute),
+                    TileMode = TileMode.Tile,
+                    Visual = new Canvas
+                    {
+                        Width = 16,
+                        Height= 16,
+                        Background = Brushes.Red,
+                        Children =
+                        {
+                            new Rectangle
+                            {
+                                Width = 8,
+                                Height = 8,
+                                Fill = Brushes.Green,
+                            },
+                            new Rectangle
+                            {
+                                Width = 8,
+                                Height = 8,
+                                Fill = Brushes.Green,
+                                [Canvas.LeftProperty] = 8,
+                                [Canvas.TopProperty] = 8,
+                            },
+                        }
+                    }
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task VisualBrush_Checkerboard_144_Dpi()
+        {
+            var target = new Border
+            {
+                Width = 200,
+                Height = 200,
+                Background = new VisualBrush
+                {
+                    DestinationRect = new RelativeRect(0, 0, 16, 16, RelativeUnit.Absolute),
+                    TileMode = TileMode.Tile,
+                    Visual = new Canvas
+                    {
+                        Width = 16,
+                        Height = 16,
+                        Background = Brushes.Red,
+                        Children =
+                        {
+                            new Rectangle
+                            {
+                                Width = 8,
+                                Height = 8,
+                                Fill = Brushes.Green,
+                            },
+                            new Rectangle
+                            {
+                                Width = 8,
+                                Height = 8,
+                                Fill = Brushes.Green,
+                                [Canvas.LeftProperty] = 8,
+                                [Canvas.TopProperty] = 8,
+                            },
+                        }
+                    }
+                }
+            };
+
+            await RenderToFile(target, dpi: 144);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task VisualBrush_Checkerboard_192_Dpi()
+        {
+            var target = new Border
+            {
+                Width = 200,
+                Height = 200,
+                Background = new VisualBrush
+                {
+                    DestinationRect = new RelativeRect(0, 0, 16, 16, RelativeUnit.Absolute),
+                    TileMode = TileMode.Tile,
+                    Visual = new Canvas
+                    {
+                        Width = 16,
+                        Height = 16,
+                        Background = Brushes.Red,
+                        Children =
+                        {
+                            new Rectangle
+                            {
+                                Width = 8,
+                                Height = 8,
+                                Fill = Brushes.Green,
+                            },
+                            new Rectangle
+                            {
+                                Width = 8,
+                                Height = 8,
+                                Fill = Brushes.Green,
+                                [Canvas.LeftProperty] = 8,
+                                [Canvas.TopProperty] = 8,
+                            },
+                        }
+                    }
+                }
+            };
+
+            await RenderToFile(target, dpi: 192);
+            CompareImages();
+        }
     }
 }

+ 4 - 4
tests/Avalonia.RenderTests/TestBase.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Direct2D1.RenderTests
             get;
         }
 
-        protected async Task RenderToFile(Control target, [CallerMemberName] string testName = "")
+        protected async Task RenderToFile(Control target, [CallerMemberName] string testName = "", double dpi = 96)
         {
             if (!Directory.Exists(OutputPath))
             {
@@ -75,9 +75,9 @@ namespace Avalonia.Direct2D1.RenderTests
             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);
+            var dpiVector = new Vector(dpi, dpi);
 
-            using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpi))
+            using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
             {
                 target.Measure(size);
                 target.Arrange(new Rect(size));
@@ -85,7 +85,7 @@ namespace Avalonia.Direct2D1.RenderTests
                 bitmap.Save(immediatePath);
             }
 
-            using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpi))
+            using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector))
             using (var renderer = new DeferredRenderer(target, rtb))
             {
                 target.Measure(size);

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png


BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png


BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png


BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png


BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png


BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png


BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png


BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png


BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png


BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png


BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png


BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png