From f1703dd784becf63d55606b51b852e854dae8ea3 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:40:21 +0100 Subject: [PATCH] Add `DrawingImage.Viewbox` (#18913) * Added DrawingImage.Frame: a rectangular region of `Drawing`, in device independent pixels, to display when rendering the image * Renamed Frame to Viewbox Added test --- src/Avalonia.Base/Media/DrawingImage.cs | 39 +++++++++++++++--- .../Media/ImageDrawingTests.cs | 27 +++++++++++- .../ImageDrawing_Viewbox.expected.png | Bin 0 -> 8985 bytes 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Viewbox.expected.png diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index c83e8eb6ee..7949eaa351 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -22,6 +22,12 @@ namespace Avalonia.Media public static readonly StyledProperty DrawingProperty = AvaloniaProperty.Register(nameof(Drawing)); + /// + /// Defines the property. + /// + public static readonly StyledProperty ViewboxProperty = + AvaloniaProperty.Register(nameof(Viewbox)); + /// public event EventHandler? Invalidated; @@ -35,8 +41,25 @@ namespace Avalonia.Media set => SetValue(DrawingProperty, value); } + /// + /// Gets or sets a rectangular region of , in device independent pixels, to display + /// when rendering this image. + /// + /// + /// This value can be used to display only part of , or to surround it with empty + /// space. If null, will provide its own viewbox. + /// + /// + public Rect? Viewbox + { + get => GetValue(ViewboxProperty); + set => SetValue(ViewboxProperty, value); + } + /// - public Size Size => Drawing?.GetBounds().Size ?? default; + public Size Size => GetBounds().Size; + + private Rect GetBounds() => Viewbox ?? Drawing?.GetBounds() ?? default; /// void IImage.Draw( @@ -44,14 +67,18 @@ namespace Avalonia.Media Rect sourceRect, Rect destRect) { - var drawing = Drawing; + if (Drawing is not { } drawing || sourceRect.Size == default || destRect.Size == default) + { + return; + } + + var bounds = GetBounds(); - if (drawing == null) + if (bounds.Size == default) { return; } - var bounds = drawing.GetBounds(); var scale = Matrix.CreateScale( destRect.Width / sourceRect.Width, destRect.Height / sourceRect.Height); @@ -62,7 +89,7 @@ namespace Avalonia.Media using (context.PushClip(destRect)) using (context.PushTransform(translate * scale)) { - Drawing?.Draw(context); + drawing.Draw(context); } } @@ -71,7 +98,7 @@ namespace Avalonia.Media { base.OnPropertyChanged(change); - if (change.Property == DrawingProperty) + if (change.Property == DrawingProperty || change.Property == ViewboxProperty) { RaiseInvalidated(EventArgs.Empty); } diff --git a/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs b/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs index 4294e3874d..f1e9a12289 100644 --- a/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs +++ b/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs @@ -45,6 +45,31 @@ namespace Avalonia.Skia.RenderTests CompareImages(); } + [Fact] + public async Task ImageDrawing_Viewbox() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Image + { + Source = new DrawingImage + { + Viewbox = new Rect(48, 37, 100, 125), + Drawing = new ImageDrawing + { + ImageSource = new Bitmap(BitmapPath), + Rect = new Rect(0, 0, 200, 200), + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + [Fact] public async Task ImageDrawing_BottomRight() { @@ -85,7 +110,7 @@ namespace Avalonia.Skia.RenderTests { var target = new Border { - Width = 400, + Width = 400, Height = 400, Child = new DrawingBrushTransformTest() }; diff --git a/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Viewbox.expected.png b/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Viewbox.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..c2abd87bd2d10d029f8a836483ce4df8f9055b92 GIT binary patch literal 8985 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