diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png
new file mode 100644
index 000000000000..3cfafd6540d3
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue30403.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue30403.cs
new file mode 100644
index 000000000000..d11943ede035
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue30403.cs
@@ -0,0 +1,26 @@
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 30403, "Image under WinUI does not respect VerticalOptions and HorizontalOptions with AspectFit", PlatformAffected.UWP)]
+public class Issue30403 : TestContentPage
+{
+ protected override void Init()
+ {
+ Title = "Issue 30403";
+
+ Content = new Grid
+ {
+ BackgroundColor = Colors.LightGray,
+ Children =
+ {
+ new Image
+ {
+ AutomationId = "TestImage",
+ Source = "dotnet_bot.png",
+ Aspect = Aspect.AspectFit,
+ VerticalOptions = LayoutOptions.Center,
+ HorizontalOptions = LayoutOptions.Center,
+ }
+ }
+ };
+ }
+}
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png
new file mode 100644
index 000000000000..22aa5071ed34
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png differ
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30403.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30403.cs
new file mode 100644
index 000000000000..ea0d4fb68db1
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30403.cs
@@ -0,0 +1,25 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue30403 : _IssuesUITest
+{
+ public Issue30403(TestDevice testDevice) : base(testDevice)
+ {
+ }
+
+ public override string Issue => "Image under WinUI does not respect VerticalOptions and HorizontalOptions with AspectFit";
+
+ [Test]
+ [Category(UITestCategories.Image)]
+ public void ImageRespectsVerticalAndHorizontalOptionsWithAspectFit()
+ {
+ App.WaitForElement("TestImage");
+
+ // Verify that the image is positioned correctly according to VerticalOptions.Center and HorizontalOptions.Center
+ // with AspectFit aspect ratio. The image should appear in the center of the Grid.
+ VerifyScreenshot();
+ }
+}
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png
new file mode 100644
index 000000000000..3c1f7e279e48
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyGif.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyGif.png
index 3164c2f8d217..0671ac6b814c 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyGif.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyGif.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyJpg.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyJpg.png
index 5a8e2b372ff9..ab34d56fbb05 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyJpg.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyJpg.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyPng.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyPng.png
index e2a27d10c753..2c9fc3947288 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyPng.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifyPng.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifySvg.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifySvg.png
index 0be5c5278b05..65b6fa0da96b 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifySvg.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/LoadAndVerifySvg.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png
index 202eb5acc2a8..ec97d75334f0 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/RadioButtonContentNotRendering.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/RadioButtonContentNotRendering.png
index 69726e16d53f..438d1e344a01 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/RadioButtonContentNotRendering.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/RadioButtonContentNotRendering.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowsDontRespectControlShape.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowsDontRespectControlShape.png
index 6fd5df1edce0..a2fe0a78c813 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowsDontRespectControlShape.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShadowsDontRespectControlShape.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png
index de6c51458ca7..5c884a59c092 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenMultipleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png
index 30dbf3e2d594..4901f26a8da1 100644
Binary files a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/VerifyModelItemsObservableCollectionWhenSingleModePreSelection.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png
new file mode 100644
index 000000000000..756c5f8f7db6
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageRespectsVerticalAndHorizontalOptionsWithAspectFit.png differ
diff --git a/src/Core/src/Handlers/Image/ImageHandler.Windows.cs b/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
index ee3a6ddbd533..9cbcc2253d8f 100644
--- a/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
+++ b/src/Core/src/Handlers/Image/ImageHandler.Windows.cs
@@ -1,7 +1,9 @@
-using System.Threading.Tasks;
+using System;
+using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Imaging;
using WImage = Microsoft.UI.Xaml.Controls.Image;
namespace Microsoft.Maui.Handlers
@@ -55,6 +57,33 @@ protected override void RemoveContainer()
UpdateValue(nameof(IView.Width));
}
+ ///
+ public override Graphics.Size GetDesiredSize(double widthConstraint, double heightConstraint)
+ {
+ // Compute a possible size without mutating platform properties during measure.
+ var possibleSize = base.GetDesiredSize(widthConstraint, heightConstraint);
+
+ // For AspectFit we want each non-Fill axis (independently) to not exceed intrinsic bitmap size
+ // so that alignment (Center, Start, End) has space to operate. A Fill axis should remain
+ // unconstrained here and rely on layout constraints.
+ if (VirtualView.Aspect == Aspect.AspectFit)
+ {
+ var imageSize = GetImageSize();
+ double w = possibleSize.Width;
+ double h = possibleSize.Height;
+
+ if (VirtualView.HorizontalLayoutAlignment != Primitives.LayoutAlignment.Fill && imageSize.Width > 0)
+ w = Math.Min(w, imageSize.Width);
+
+ if (VirtualView.VerticalLayoutAlignment != Primitives.LayoutAlignment.Fill && imageSize.Height > 0)
+ h = Math.Min(h, imageSize.Height);
+
+ return new Graphics.Size(w, h);
+ }
+
+ return possibleSize;
+ }
+
///
/// Maps the abstract property to the platform-specific implementations.
///
@@ -112,6 +141,9 @@ public static void MapAspect(IImageHandler handler, IImage image)
{
handler.UpdateValue(nameof(IViewHandler.ContainerView));
handler.PlatformView?.UpdateAspect(image);
+ // Aspect changes may affect whether we cap to intrinsic size
+ if (handler is ImageHandler ih)
+ ih.UpdatePlatformMaxConstraints();
}
///
@@ -135,15 +167,75 @@ public static void MapSource(IImageHandler handler, IImage image) =>
///
/// The associated handler.
/// The associated instance.
- public static Task MapSourceAsync(IImageHandler handler, IImage image) =>
- handler.SourceLoader.UpdateImageSourceAsync();
+ public static Task MapSourceAsync(IImageHandler handler, IImage image)
+ {
+ // Reset platform caps so we don't keep stale values between sources
+ if (handler is ImageHandler ih && ih.PlatformView is not null)
+ {
+ ih.PlatformView.MaxWidth = double.PositiveInfinity;
+ ih.PlatformView.MaxHeight = double.PositiveInfinity;
+ }
+
+ return handler.SourceLoader.UpdateImageSourceAsync();
+ }
void OnImageOpened(object sender, RoutedEventArgs e)
{
// Because this resolves from a task we should validate that the
// handler hasn't been disconnected
if (this.IsConnected())
+ {
UpdateValue(nameof(IImage.IsAnimationPlaying));
+ // Apply platform constraints when the decoded size is available
+ UpdatePlatformMaxConstraints();
+ }
+ }
+
+ ///
+ /// Updates platform MaxWidth/MaxHeight based on current aspect/alignment and decoded image size.
+ /// Avoids doing this during GetDesiredSize to prevent side effects across layout passes.
+ ///
+ private void UpdatePlatformMaxConstraints()
+ {
+ if (PlatformView is null || VirtualView is null)
+ return;
+
+ if (VirtualView.Aspect == Aspect.AspectFit)
+ {
+ var sz = GetImageSize();
+
+ // Width: cap to intrinsic only if horizontal alignment isn't Fill
+ if (VirtualView.HorizontalLayoutAlignment != Primitives.LayoutAlignment.Fill && sz.Width > 0)
+ PlatformView.MaxWidth = Math.Min(sz.Width, VirtualView.MaximumWidth);
+ else
+ PlatformView.MaxWidth = VirtualView.MaximumWidth;
+
+ // Height: cap to intrinsic only if vertical alignment isn't Fill
+ if (VirtualView.VerticalLayoutAlignment != Primitives.LayoutAlignment.Fill && sz.Height > 0)
+ PlatformView.MaxHeight = Math.Min(sz.Height, VirtualView.MaximumHeight);
+ else
+ PlatformView.MaxHeight = VirtualView.MaximumHeight;
+
+ return;
+ }
+
+ // Non AspectFit: mirror the view's declared maximums
+ PlatformView.MaxWidth = VirtualView.MaximumWidth;
+ PlatformView.MaxHeight = VirtualView.MaximumHeight;
+ }
+
+ private Graphics.Size GetImageSize()
+ {
+ if (PlatformView.Source is BitmapSource bitmap)
+ {
+ // BitmapSource may not have PixelWidth/PixelHeight set until image is loaded
+ if (bitmap.PixelWidth > 0 && bitmap.PixelHeight > 0)
+ {
+ return new Graphics.Size(bitmap.PixelWidth, bitmap.PixelHeight);
+ }
+ // If not available, return zero
+ }
+ return Graphics.Size.Zero;
}
partial class ImageImageSourcePartSetter
diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
index ad4534aa97fd..a060bf128ef1 100644
--- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
@@ -147,6 +147,7 @@ override Microsoft.Maui.Converters.ThicknessTypeConverter.CanConvertFrom(System.
override Microsoft.Maui.Converters.ThicknessTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool
override Microsoft.Maui.Converters.ThicknessTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object?
override Microsoft.Maui.Converters.ThicknessTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object?
+override Microsoft.Maui.Handlers.ImageHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size
override Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.Equals(object? obj) -> bool
override Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.GetHashCode() -> int
override Microsoft.Maui.Handlers.ContextFlyoutItemHandlerUpdate.ToString() -> string!