diff --git a/src/Avalonia.Visuals/Media/PixelSize.cs b/src/Avalonia.Visuals/Media/PixelSize.cs index 3d0ae0b351..6785b51716 100644 --- a/src/Avalonia.Visuals/Media/PixelSize.cs +++ b/src/Avalonia.Visuals/Media/PixelSize.cs @@ -159,8 +159,8 @@ namespace Avalonia /// The dots per inch. /// The device-independent size. 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))); /// /// Returns the string representation of the size. diff --git a/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs b/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs index 52a8fb5ab7..27949fcf55 100644 --- a/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs +++ b/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; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 1bfd7fd90d..076d69ae4a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -225,10 +225,7 @@ namespace Avalonia.Skia /// 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); } /// @@ -387,26 +384,27 @@ namespace Avalonia.Skia /// Target size. /// Tile brush to use. /// Tile brush image. - /// The bitmap interpolation mode. 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 /// /// Create new render target compatible with this drawing context. /// - /// Width. - /// Height. - /// Drawing dpi. + /// The size of the render target in DIPs. /// Pixel format. /// - 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 diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index efe26ba01a..aaca1a3b00 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/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)) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 55877cefcb..19d320b783 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/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); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 72367ac50f..fbe7f840d7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/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(_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); + } + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Utils/DebugUtils.cs b/src/Windows/Avalonia.Direct2D1/Utils/DebugUtils.cs new file mode 100644 index 0000000000..9ce8f15cf4 --- /dev/null +++ b/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); + } + } +} diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index 6f90dfd19e..7c53f4516a 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/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(); + } } } diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 2892615e48..4c8abd85f7 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/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(); 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); diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png new file mode 100644 index 0000000000..7e674b849a Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png new file mode 100644 index 0000000000..f80ac7f90d Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png new file mode 100644 index 0000000000..0f88a0d468 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png new file mode 100644 index 0000000000..1900ade5e2 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png new file mode 100644 index 0000000000..a209dc94cc Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png new file mode 100644 index 0000000000..6ac1bdb088 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png new file mode 100644 index 0000000000..7e674b849a Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_144_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png new file mode 100644 index 0000000000..f80ac7f90d Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_192_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png new file mode 100644 index 0000000000..0f88a0d468 Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Checkerboard_96_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png new file mode 100644 index 0000000000..d530c72519 Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_144_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png new file mode 100644 index 0000000000..a209dc94cc Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_192_Dpi.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png new file mode 100644 index 0000000000..6ac1bdb088 Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Grip_96_Dpi.expected.png differ