diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue32869_Image.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue32869_Image.png new file mode 100644 index 000000000000..12bdbde20a1c Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Issue32869_Image.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue32869.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue32869.cs new file mode 100644 index 000000000000..ba12d60e3091 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue32869.cs @@ -0,0 +1,49 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 32869, "Image control crashes on Android when image width exceeds height", PlatformAffected.Android)] +public class Issue32869 : ContentPage +{ + Image _testImage; + + public Issue32869() + { + Title = "Wide Image Test"; + Padding = new Thickness(24); + _testImage = new Image + { + AutomationId = "TestImage", + }; + Content = _testImage; + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + try + { + await LoadWideImageAsync(); + } + catch (Exception ex) + { + _testImage.Source = null; + await DisplayAlert("Error", $"Failed to load image: {ex.Message}", "OK"); + } + } + + async Task LoadWideImageAsync() + { + // Load the wide image from embedded resources + await using var stream = await FileSystem.OpenAppPackageFileAsync("Issue32869.png"); + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + var imageBytes = ms.ToArray(); + + // Write to local storage + var localPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "test_wide_image.png"); + await using var fileStream = new FileStream(localPath, FileMode.Create); + await fileStream.WriteAsync(imageBytes); + + // Load the image + _testImage.Source = localPath; + } +} diff --git a/src/Controls/tests/TestCases.HostApp/Resources/Raw/Issue32869.png b/src/Controls/tests/TestCases.HostApp/Resources/Raw/Issue32869.png new file mode 100644 index 000000000000..e39bf98c4c09 Binary files /dev/null and b/src/Controls/tests/TestCases.HostApp/Resources/Raw/Issue32869.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32869.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32869.cs new file mode 100644 index 000000000000..734f1ee332cb --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32869.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue32869 : _IssuesUITest +{ + public Issue32869(TestDevice testDevice) : base(testDevice) + { + } + + public override string Issue => "Image control crashes on Android when image width exceeds height"; + + [Test] + [Category(UITestCategories.Image)] + public void Issue32869_Image() + { + App.WaitForElement("TestImage"); + VerifyScreenshot(); + } +} diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue32869_Image.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue32869_Image.png new file mode 100644 index 000000000000..49f7a7b10787 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/Issue32869_Image.png differ diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java index 10f724424e45..d42f4ace5115 100644 --- a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java +++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java @@ -297,6 +297,10 @@ static PorterDuff.Mode getPorterMode(int mode) { } private static void prepare(RequestBuilder builder, MauiTarget target, boolean cachingEnabled, ImageLoaderCallback callback) { + prepare(builder, target, cachingEnabled, callback, false); + } + + private static void prepare(RequestBuilder builder, MauiTarget target, boolean cachingEnabled, ImageLoaderCallback callback, boolean constrainSize) { // A special value to work around https://github.com/dotnet/maui/issues/6783 where targets // are actually re-used if all the variables are the same. // Adding this "error image" that will always load a null image makes each request unique, @@ -310,6 +314,13 @@ private static void prepare(RequestBuilder builder, MauiTarget target, .skipMemoryCache(true); } + // Constrain bitmap size to prevent excessive memory allocation + // See https://github.com/dotnet/maui/issues/32869 + if (constrainSize) { + builder = builder + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); + } + target.load(builder); } @@ -318,20 +329,28 @@ public static String getGlyphHex(String glyph) { } private static void loadInto(RequestBuilder builder, ImageView imageView, boolean cachingEnabled, ImageLoaderCallback callback, Object model) { + loadInto(builder, imageView, cachingEnabled, callback, model, false); + } + + private static void loadInto(RequestBuilder builder, ImageView imageView, boolean cachingEnabled, ImageLoaderCallback callback, Object model, boolean constrainSize) { MauiCustomViewTarget target = new MauiCustomViewTarget(imageView, callback, model); - prepare(builder, target, cachingEnabled, callback); + prepare(builder, target, cachingEnabled, callback, constrainSize); } private static void load(RequestBuilder builder, Context context, boolean cachingEnabled, ImageLoaderCallback callback, Object model) { + load(builder, context, cachingEnabled, callback, model, false); + } + + private static void load(RequestBuilder builder, Context context, boolean cachingEnabled, ImageLoaderCallback callback, Object model, boolean constrainSize) { MauiCustomTarget target = new MauiCustomTarget(context, callback, model); - prepare(builder, target, cachingEnabled, callback); + prepare(builder, target, cachingEnabled, callback, constrainSize); } public static void loadImageFromFile(ImageView imageView, String file, ImageLoaderCallback callback) { RequestBuilder builder = Glide .with(imageView) .load(file); - loadInto(builder, imageView, true, callback, file); + loadInto(builder, imageView, true, callback, file, true); } public static void loadImageFromUri(ImageView imageView, String uri, boolean cachingEnabled, ImageLoaderCallback callback) { @@ -343,14 +362,14 @@ public static void loadImageFromUri(ImageView imageView, String uri, boolean cac RequestBuilder builder = Glide .with(imageView) .load(androidUri); - loadInto(builder, imageView, cachingEnabled, callback, androidUri); + loadInto(builder, imageView, cachingEnabled, callback, androidUri, true); } public static void loadImageFromStream(ImageView imageView, InputStream inputStream, ImageLoaderCallback callback) { RequestBuilder builder = Glide .with(imageView) .load(inputStream); - loadInto(builder, imageView, false, callback, inputStream); + loadInto(builder, imageView, false, callback, inputStream, true); } public static void loadImageFromFont(ImageView imageView, @ColorInt int color, String glyph, Typeface typeface, float textSize, ImageLoaderCallback callback) { @@ -366,7 +385,7 @@ public static void loadImageFromFile(Context context, String file, ImageLoaderCa RequestBuilder builder = Glide .with(context) .load(file); - load(builder, context, true, callback, file); + load(builder, context, true, callback, file, true); } public static void loadImageFromUri(Context context, String uri, boolean cachingEnabled, ImageLoaderCallback callback) { @@ -378,14 +397,14 @@ public static void loadImageFromUri(Context context, String uri, boolean caching RequestBuilder builder = Glide .with(context) .load(androidUri); - load(builder, context, cachingEnabled, callback, androidUri); + load(builder, context, cachingEnabled, callback, androidUri, true); } public static void loadImageFromStream(Context context, InputStream inputStream, ImageLoaderCallback callback) { RequestBuilder builder = Glide .with(context) .load(inputStream); - load(builder, context, false, callback, inputStream); + load(builder, context, false, callback, inputStream, true); } public static void loadImageFromFont(Context context, @ColorInt int color, String glyph, Typeface typeface, float textSize, ImageLoaderCallback callback) {