diff --git a/src/Essentials/samples/Samples/View/MediaPickerPage.xaml b/src/Essentials/samples/Samples/View/MediaPickerPage.xaml
index e28098ec2f75..721144920f6e 100644
--- a/src/Essentials/samples/Samples/View/MediaPickerPage.xaml
+++ b/src/Essentials/samples/Samples/View/MediaPickerPage.xaml
@@ -8,37 +8,219 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
diff --git a/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs b/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs
index 9b5983aabf12..7d167ee6065b 100644
--- a/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs
+++ b/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs
@@ -1,21 +1,36 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.IO;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Maui.Controls;
+using Microsoft.Maui.Graphics;
+using Microsoft.Maui.Graphics.Platform;
using Microsoft.Maui.Media;
using Microsoft.Maui.Storage;
namespace Samples.ViewModel
{
+ public class PhotoInfo
+ {
+ public ImageSource Source { get; set; }
+ public string Dimensions { get; set; }
+ public string FileSize { get; set; }
+ }
+
public class MediaPickerViewModel : BaseViewModel
{
ImageSource photoSource;
bool showPhoto;
int pickerSelectionLimit = 1;
- private ObservableCollection photoList = [];
+ int pickerCompressionQuality = 100;
+ int pickerMaximumWidth = 0;
+ int pickerMaximumHeight = 0;
+ long imageByteLength = 0;
+ string imageDimensions = "";
+ private ObservableCollection photoList = [];
private bool showMultiplePhotos;
public MediaPickerViewModel()
@@ -41,6 +56,36 @@ public int PickerSelectionLimit
set => SetProperty(ref pickerSelectionLimit, value);
}
+ public int PickerCompressionQuality
+ {
+ get => pickerCompressionQuality;
+ set => SetProperty(ref pickerCompressionQuality, value);
+ }
+
+ public int PickerMaximumWidth
+ {
+ get => pickerMaximumWidth;
+ set => SetProperty(ref pickerMaximumWidth, value);
+ }
+
+ public int PickerMaximumHeight
+ {
+ get => pickerMaximumHeight;
+ set => SetProperty(ref pickerMaximumHeight, value);
+ }
+
+ public long ImageByteLength
+ {
+ get => imageByteLength;
+ set => SetProperty(ref imageByteLength, value);
+ }
+
+ public string ImageDimensions
+ {
+ get => imageDimensions;
+ set => SetProperty(ref imageDimensions, value);
+ }
+
public bool ShowPhoto
{
get => showPhoto;
@@ -53,7 +98,7 @@ public bool ShowMultiplePhotos
set => SetProperty(ref showMultiplePhotos, value);
}
- public ObservableCollection PhotoList
+ public ObservableCollection PhotoList
{
get => photoList;
set => SetProperty(ref photoList, value);
@@ -73,6 +118,9 @@ async void DoPickPhoto()
{
Title = "Pick a photo",
SelectionLimit = PickerSelectionLimit,
+ CompressionQuality = PickerCompressionQuality,
+ MaximumWidth = PickerMaximumWidth > 0 ? PickerMaximumWidth : null,
+ MaximumHeight = PickerMaximumHeight > 0 ? PickerMaximumHeight : null,
});
await LoadPhotoAsync(photo);
@@ -89,7 +137,13 @@ async void DoCapturePhoto()
{
try
{
- var photo = await MediaPicker.CapturePhotoAsync();
+ var photo = await MediaPicker.CapturePhotoAsync(new MediaPickerOptions
+ {
+ Title = "Capture a photo",
+ CompressionQuality = PickerCompressionQuality,
+ MaximumWidth = PickerMaximumWidth > 0 ? PickerMaximumWidth : null,
+ MaximumHeight = PickerMaximumHeight > 0 ? PickerMaximumHeight : null,
+ });
await LoadPhotoAsync(photo);
@@ -113,6 +167,8 @@ async void DoPickVideo()
ShowPhoto = false;
ShowMultiplePhotos = false;
+ ImageByteLength = 0;
+ ImageDimensions = "";
await DisplayAlertAsync($"{videos.Count} videos successfully picked.");
@@ -132,6 +188,8 @@ async void DoCaptureVideo()
ShowPhoto = false;
ShowMultiplePhotos = false;
+ ImageByteLength = 0;
+ ImageDimensions = "";
await DisplayAlertAsync($"Video successfully captured at {video.FullPath}.");
@@ -148,11 +206,26 @@ async Task LoadPhotoAsync(FileResult photo)
if (photo is null)
{
PhotoSource = null;
+ ImageDimensions = "";
return;
}
var stream = await photo.OpenReadAsync();
+
+ // Get image dimensions
+ try
+ {
+ var imageInfo = GetImageDimensions(stream);
+ ImageDimensions = $"{imageInfo.Width} × {imageInfo.Height} • {stream.Length:N0} bytes";
+ stream.Position = 0; // Reset stream position
+ }
+ catch
+ {
+ ImageDimensions = $"Unknown dimensions • {stream.Length:N0} bytes";
+ }
+
PhotoSource = ImageSource.FromStream(() => stream);
+ ImageByteLength = stream.Length;
ShowMultiplePhotos = false;
ShowPhoto = true;
@@ -161,6 +234,8 @@ async Task LoadPhotoAsync(FileResult photo)
async Task LoadPhotoAsync(List photo)
{
PhotoList.Clear();
+ ImageByteLength = 0;
+ ImageDimensions = "";
// canceled
if (photo is null || photo.Count == 0)
@@ -172,17 +247,52 @@ async Task LoadPhotoAsync(List photo)
foreach (var item in photo)
{
var stream = await item.OpenReadAsync();
- PhotoList.Add(ImageSource.FromStream(() => stream));
+
+ // Get image dimensions
+ var dimensions = GetImageDimensions(stream);
+ stream.Position = 0; // Reset stream position for ImageSource
+
+ var photoInfo = new PhotoInfo
+ {
+ Source = ImageSource.FromStream(() => stream),
+ Dimensions = $"{dimensions.Width} × {dimensions.Height}",
+ FileSize = $"{stream.Length:N0} bytes"
+ };
+
+ PhotoList.Add(photoInfo);
+ ImageByteLength += stream.Length;
}
+ // Show count for multiple photos
+ ImageDimensions = $"{photo.Count} photos selected";
+
ShowPhoto = false;
ShowMultiplePhotos = true;
}
+ (int Width, int Height) GetImageDimensions(Stream imageStream)
+ {
+ try
+ {
+ // Reset position to beginning of stream
+ imageStream.Position = 0;
+
+ // Use MAUI.Graphics to load the image and get dimensions
+ using var image = PlatformImage.FromStream(imageStream);
+ return ((int)image.Width, (int)image.Height);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to extract image dimensions: {ex.Message}");
+ return (0, 0);
+ }
+ }
+
public override void OnDisappearing()
{
PhotoList?.Clear();
PhotoSource = null;
+ ImageDimensions = "";
base.OnDisappearing();
}
diff --git a/src/Essentials/src/MediaPicker/ImageProcessor.shared.cs b/src/Essentials/src/MediaPicker/ImageProcessor.shared.cs
new file mode 100644
index 000000000000..246998413873
--- /dev/null
+++ b/src/Essentials/src/MediaPicker/ImageProcessor.shared.cs
@@ -0,0 +1,232 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+#if IOS || ANDROID || WINDOWS
+using Microsoft.Maui.Graphics;
+using Microsoft.Maui.Graphics.Platform;
+#endif
+
+namespace Microsoft.Maui.Essentials;
+
+///
+/// Unified image processing helper using MAUI Graphics for cross-platform consistency.
+///
+internal static class ImageProcessor
+{
+ ///
+ /// Processes an image by applying resizing and compression using MAUI Graphics.
+ ///
+ /// The input image stream.
+ /// Maximum width constraint (null for no constraint).
+ /// Maximum height constraint (null for no constraint).
+ /// Compression quality percentage (0-100).
+ /// Original filename to determine format preservation logic.
+ /// A new stream containing the processed image.
+ public static async Task ProcessImageAsync(Stream inputStream,
+ int? maxWidth, int? maxHeight, int qualityPercent, string originalFileName = null)
+ {
+#if !(IOS || ANDROID || WINDOWS)
+ // For platforms without MAUI Graphics support, return null to indicate no processing available
+ await Task.CompletedTask; // Avoid async warning
+ return null;
+#else
+ if (inputStream is null)
+ {
+ throw new ArgumentNullException(nameof(inputStream));
+ }
+
+ // Ensure we can read from the beginning of the stream
+ if (inputStream.CanSeek)
+ {
+ inputStream.Position = 0;
+ }
+
+ IImage image = null;
+ try
+ {
+ // Load the image using MAUI Graphics
+ var imageLoadingService = new PlatformImageLoadingService();
+ image = imageLoadingService.FromStream(inputStream);
+
+ if (image is null)
+ {
+ throw new InvalidOperationException("Failed to load image from stream");
+ }
+
+ // Apply resizing if needed
+ if (maxWidth.HasValue || maxHeight.HasValue)
+ {
+ image = ApplyResizing(image, maxWidth, maxHeight);
+ }
+
+ // Determine output format and quality
+ var format = ShouldUsePngFormat(originalFileName, qualityPercent)
+ ? ImageFormat.Png : ImageFormat.Jpeg;
+ var quality = Math.Max(0f, Math.Min(1f, qualityPercent / 100.0f));
+
+ // Save to new stream
+ var outputStream = new MemoryStream();
+ await image.SaveAsync(outputStream, format, quality);
+ outputStream.Position = 0;
+
+ return outputStream;
+ }
+ finally
+ {
+ image?.Dispose();
+ }
+#endif
+ }
+
+#if IOS || ANDROID || WINDOWS
+ ///
+ /// Applies resizing constraints to an image while preserving aspect ratio.
+ ///
+ private static IImage ApplyResizing(IImage image, int? maxWidth, int? maxHeight)
+ {
+ var currentWidth = image.Width;
+ var currentHeight = image.Height;
+
+ // Calculate new dimensions while preserving aspect ratio
+ var newDimensions = CalculateResizedDimensions(currentWidth, currentHeight, maxWidth, maxHeight);
+
+ // Only resize if dimensions actually changed
+ if (Math.Abs(newDimensions.Width - currentWidth) > 0.1f ||
+ Math.Abs(newDimensions.Height - currentHeight) > 0.1f)
+ {
+ return image.Downsize(newDimensions.Width, newDimensions.Height, disposeOriginal: true);
+ }
+
+ return image;
+ }
+
+ ///
+ /// Calculates new image dimensions while preserving aspect ratio.
+ ///
+ private static (float Width, float Height) CalculateResizedDimensions(
+ float originalWidth, float originalHeight, int? maxWidth, int? maxHeight)
+ {
+ if (!maxWidth.HasValue && !maxHeight.HasValue)
+ {
+ return (originalWidth, originalHeight);
+ }
+
+ var targetWidth = maxWidth ?? float.MaxValue;
+ var targetHeight = maxHeight ?? float.MaxValue;
+
+ var scaleX = targetWidth / originalWidth;
+ var scaleY = targetHeight / originalHeight;
+ var scale = Math.Min(scaleX, scaleY);
+
+ // Only scale down, never scale up
+ if (scale >= 1.0f)
+ return (originalWidth, originalHeight);
+
+ return (originalWidth * scale, originalHeight * scale);
+ }
+
+ ///
+ /// Determines whether to use PNG format based on the original filename and quality settings.
+ ///
+ private static bool ShouldUsePngFormat(string originalFileName, int qualityPercent)
+ {
+ var originalWasPng = !string.IsNullOrEmpty(originalFileName) &&
+ Path.GetExtension(originalFileName).Equals(".png", StringComparison.OrdinalIgnoreCase);
+
+ // High quality (>=95): Prefer PNG for lossless quality
+ // High quality (>=90) + original was PNG: preserve PNG format
+ // Otherwise: Use JPEG for better compression
+ return qualityPercent >= 95 || (qualityPercent >= 90 && originalWasPng);
+ }
+#endif
+
+ ///
+ /// Determines if image processing is needed based on the provided options.
+ ///
+ public static bool IsProcessingNeeded(int? maxWidth, int? maxHeight, int qualityPercent)
+ {
+#if !(IOS || ANDROID || WINDOWS)
+ // On platforms without MAUI Graphics support, always return false - no processing available
+ return false;
+#else
+ return (maxWidth.HasValue || maxHeight.HasValue) || qualityPercent < 100;
+#endif
+ }
+
+ ///
+ /// Determines the output file extension based on processed image data and quality settings.
+ ///
+ /// The processed image stream to analyze
+ /// Compression quality percentage
+ /// Original filename for format hints (optional)
+ /// File extension including the dot (e.g., ".jpg", ".png")
+ public static string DetermineOutputExtension(Stream imageData, int qualityPercent, string originalFileName = null)
+ {
+#if !(IOS || ANDROID)
+ // On platforms without MAUI Graphics support, fall back to simple logic
+ bool originalWasPng = !string.IsNullOrEmpty(originalFileName) &&
+ originalFileName.EndsWith(".png", StringComparison.OrdinalIgnoreCase);
+ return (qualityPercent >= 95 || originalWasPng) ? ".png" : ".jpg";
+#else
+ // Try to detect format from the actual processed image data
+ var detectedFormat = DetectImageFormat(imageData);
+ if (!string.IsNullOrEmpty(detectedFormat))
+ {
+ return detectedFormat;
+ }
+
+ // Fallback: Use quality-based decision with original format consideration
+ var originalWasPng = !string.IsNullOrEmpty(originalFileName) &&
+ Path.GetExtension(originalFileName).Equals(".png", StringComparison.OrdinalIgnoreCase);
+
+ // High quality (>=90) and original was PNG: keep PNG
+ // Very high quality (>=95): prefer PNG for maximum quality
+ // Otherwise: use JPEG for better compression
+ return (qualityPercent >= 95 || (qualityPercent >= 90 && originalWasPng)) ? ".png" : ".jpg";
+#endif
+ }
+
+#if IOS || ANDROID || WINDOWS
+ ///
+ /// Detects image format from stream using magic numbers.
+ ///
+ private static string DetectImageFormat(Stream imageData)
+ {
+ if (imageData?.Length < 4)
+ {
+ return null;
+ }
+
+ var originalPosition = imageData.Position;
+ try
+ {
+ imageData.Position = 0;
+ var bytes = new byte[8];
+ var bytesRead = imageData.Read(bytes, 0, Math.Min(8, (int)imageData.Length));
+
+ if (bytesRead < 3)
+ {
+ return null;
+ }
+
+ // PNG: 89 50 4E 47
+ if (bytesRead >= 4 && bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47)
+ {
+ return ".png";
+ }
+
+ // JPEG: FF D8 FF
+ if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF)
+ {
+ return ".jpg";
+ }
+
+ return null;
+ }
+ finally
+ {
+ imageData.Position = originalPosition;
+ }
+ }
+#endif
+}
diff --git a/src/Essentials/src/MediaPicker/MediaPicker.android.cs b/src/Essentials/src/MediaPicker/MediaPicker.android.cs
index 787defcc8b33..0de96a96ea57 100644
--- a/src/Essentials/src/MediaPicker/MediaPicker.android.cs
+++ b/src/Essentials/src/MediaPicker/MediaPicker.android.cs
@@ -1,13 +1,17 @@
using System;
using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Content.PM;
+using Android.Graphics;
using Android.Provider;
using AndroidX.Activity.Result;
using AndroidX.Activity.Result.Contract;
using Microsoft.Maui.ApplicationModel;
+using Microsoft.Maui.Essentials;
using Microsoft.Maui.Storage;
using static AndroidX.Activity.Result.Contract.ActivityResultContracts;
using AndroidUri = Android.Net.Uri;
@@ -81,6 +85,11 @@ public async Task CaptureAsync(MediaPickerOptions options, bool phot
if (photo)
{
captureResult = await CapturePhotoAsync(captureIntent);
+ // Apply compression/resizing if needed for photos
+ if (captureResult is not null && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ captureResult = await CompressImageIfNeeded(captureResult, options);
+ }
}
else
{
@@ -122,7 +131,17 @@ void OnResult(Intent intent)
await IntermediateActivity.StartAsync(pickerIntent, PlatformUtils.requestCodeMediaPicker, onResult: OnResult);
- return path is not null ? new FileResult(path) : null;
+ if (path is not null)
+ {
+ // Apply compression/resizing if needed for photos
+ if (photo && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ path = await CompressImageIfNeeded(path, options);
+ }
+ return new FileResult(path);
+ }
+
+ return null;
}
catch (OperationCanceledException)
{
@@ -144,6 +163,13 @@ async Task PickUsingPhotoPicker(MediaPickerOptions options, bool pho
}
var path = FileSystemUtils.EnsurePhysicalPath(androidUri);
+
+ // Apply compression/resizing if needed for photos
+ if (photo && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ path = await CompressImageIfNeeded(path, options);
+ }
+
return new FileResult(path);
}
@@ -185,6 +211,13 @@ async Task> PickMultipleUsingPhotoPicker(MediaPickerOptions opt
if (!uri?.Equals(AndroidUri.Empty) ?? false)
{
var path = FileSystemUtils.EnsurePhysicalPath(uri);
+
+ // Apply compression/resizing if needed for photos
+ if (photo && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ path = await CompressImageIfNeeded(path, options);
+ }
+
resultList.Add(new FileResult(path));
}
}
@@ -216,6 +249,58 @@ void OnCreate(Intent intent)
return tmpFile.AbsolutePath;
}
+ static async Task CompressImageIfNeeded(string imagePath, MediaPickerOptions options)
+ {
+ if (!ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100) || string.IsNullOrEmpty(imagePath))
+ return imagePath;
+
+ try
+ {
+ var originalFile = new Java.IO.File(imagePath);
+ if (!originalFile.Exists())
+ {
+ return imagePath;
+ }
+
+ // Use ImageProcessor for unified image processing
+ using var inputStream = File.OpenRead(imagePath);
+ var inputFileName = System.IO.Path.GetFileName(imagePath);
+ using var processedStream = await ImageProcessor.ProcessImageAsync(
+ inputStream,
+ options?.MaximumWidth,
+ options?.MaximumHeight,
+ options?.CompressionQuality ?? 100,
+ inputFileName);
+
+ if (processedStream != null)
+ {
+ // Determine output extension based on processed data and original filename
+ var outputExtension = ImageProcessor.DetermineOutputExtension(processedStream, options?.CompressionQuality ?? 100, inputFileName);
+ var processedFileName = System.IO.Path.GetFileNameWithoutExtension(imagePath) + "_processed" + outputExtension;
+ var processedPath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(imagePath), processedFileName);
+
+ // Write processed image to file
+ using var outputStream = File.Create(processedPath);
+ processedStream.Position = 0;
+ await processedStream.CopyToAsync(outputStream);
+
+ // Delete original file
+ try { originalFile.Delete(); } catch { }
+ return processedPath;
+ }
+
+ // If ImageProcessor returns null (e.g., on .NET Standard), ImageProcessor.IsProcessingNeeded would have returned false,
+ // so we shouldn't reach this point. Return original path as fallback.
+ return imagePath;
+ }
+ catch
+ {
+ // If processing fails, return original path
+ }
+
+ return imagePath;
+ }
+
async Task CaptureVideoAsync(Intent captureIntent)
{
string path = null;
@@ -291,6 +376,21 @@ void OnResult(Intent resultIntent)
await IntermediateActivity.StartAsync(pickerIntent, PlatformUtils.requestCodeMediaPicker, onResult: OnResult);
+ // Apply compression/resizing if needed for photos
+ if (photo && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ var tempResultList = resultList.Select(fr => fr.FullPath).ToList();
+ resultList.Clear();
+
+ var compressionTasks = tempResultList.Select(async path =>
+ {
+ return await CompressImageIfNeeded(path, options);
+ });
+
+ var compressedPaths = await Task.WhenAll(compressionTasks);
+ resultList.AddRange(compressedPaths.Select(path => new FileResult(path)));
+ }
+
return resultList;
}
catch (OperationCanceledException)
diff --git a/src/Essentials/src/MediaPicker/MediaPicker.ios.cs b/src/Essentials/src/MediaPicker/MediaPicker.ios.cs
index 2150c8c1879a..5b2992a5d274 100644
--- a/src/Essentials/src/MediaPicker/MediaPicker.ios.cs
+++ b/src/Essentials/src/MediaPicker/MediaPicker.ios.cs
@@ -6,6 +6,8 @@
using Foundation;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Devices;
+using Microsoft.Maui.Essentials;
+using Microsoft.Maui.Graphics.Platform;
using Microsoft.Maui.Storage;
using MobileCoreServices;
using Photos;
@@ -137,7 +139,7 @@ public async Task PhotoAsync(MediaPickerOptions options, bool photo,
{
CompletedHandler = async info =>
{
- GetFileResult(info, tcs);
+ GetFileResult(info, tcs, options);
await vc.DismissViewControllerAsync(true);
}
};
@@ -213,8 +215,11 @@ async Task> PhotosAsync(MediaPickerOptions options, bool photo,
{
Delegate = new Media.PhotoPickerDelegate
{
- CompletedHandler = res =>
- tcs.TrySetResult(PickerResultsToMediaFiles(res))
+ CompletedHandler = async res =>
+ {
+ var result = await PickerResultsToMediaFiles(res, options);
+ tcs.TrySetResult(result);
+ }
}
};
@@ -258,18 +263,32 @@ static FileResult PickerResultsToMediaFile(PHPickerResult[] results)
: new PHPickerFileResult(file.ItemProvider);
}
- static List PickerResultsToMediaFiles(PHPickerResult[] results)
+ static async Task> PickerResultsToMediaFiles(PHPickerResult[] results, MediaPickerOptions options = null)
{
- return results?
+ var fileResults = results?
.Select(file => (FileResult)new PHPickerFileResult(file.ItemProvider))
.ToList() ?? [];
+
+ // Apply resizing and compression if specified and dealing with images
+ if (ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ var compressedResults = new List();
+ foreach (var result in fileResults)
+ {
+ var compressedResult = await CompressedUIImageFileResult.CreateCompressedFromFileResult(result, options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100);
+ compressedResults.Add(compressedResult);
+ }
+ return compressedResults;
+ }
+
+ return fileResults;
}
- static void GetFileResult(NSDictionary info, TaskCompletionSource tcs)
+ static void GetFileResult(NSDictionary info, TaskCompletionSource tcs, MediaPickerOptions options = null)
{
try
{
- tcs.TrySetResult(DictionaryToMediaFile(info));
+ tcs.TrySetResult(DictionaryToMediaFile(info, options));
}
catch (Exception ex)
{
@@ -277,7 +296,7 @@ static void GetFileResult(NSDictionary info, TaskCompletionSource tc
}
}
- static FileResult DictionaryToMediaFile(NSDictionary info)
+ static FileResult DictionaryToMediaFile(NSDictionary info, MediaPickerOptions options = null)
{
// This method should only be called for iOS < 14
if (!OperatingSystem.IsIOSVersionAtLeast(14))
@@ -329,7 +348,7 @@ static FileResult DictionaryToMediaFile(NSDictionary info)
if (img is not null)
{
- return new UIImageFileResult(img);
+ return new CompressedUIImageFileResult(img, null, options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100);
}
}
@@ -415,4 +434,188 @@ internal override async Task PlatformOpenReadAsync()
protected internal static string GetTag(string identifier, string tagClass)
=> UTType.CopyAllTags(identifier, tagClass)?.FirstOrDefault();
}
+
+ class CompressedUIImageFileResult : FileResult
+ {
+ readonly UIImage uiImage;
+ readonly int? maximumWidth;
+ readonly int? maximumHeight;
+ readonly int compressionQuality;
+ readonly string originalFileName;
+ NSData data;
+
+ // Static factory method to create compressed result from existing FileResult
+ internal static async Task CreateCompressedFromFileResult(FileResult originalResult, int? maximumWidth, int? maximumHeight, int compressionQuality = 100)
+ {
+ if (originalResult is null || !ImageProcessor.IsProcessingNeeded(maximumWidth, maximumHeight, compressionQuality))
+ return originalResult;
+
+ try
+ {
+ using var originalStream = await originalResult.OpenReadAsync();
+ using var processedStream = await ImageProcessor.ProcessImageAsync(
+ originalStream, maximumWidth, maximumHeight, compressionQuality, originalResult.FileName);
+
+ // If ImageProcessor returns null (e.g., on .NET Standard), return original file
+ if (processedStream is null)
+ {
+ return originalResult;
+ }
+
+ // Read processed stream into memory
+ var memoryStream = new MemoryStream();
+ await processedStream.CopyToAsync(memoryStream);
+ memoryStream.Position = 0;
+
+ return new ProcessedImageFileResult(memoryStream, originalResult.FileName);
+ }
+ catch
+ {
+ // If compression fails, return original
+ }
+
+ return originalResult;
+ }
+
+ internal CompressedUIImageFileResult(UIImage image, string originalFileName = null, int? maximumWidth = null, int? maximumHeight = null, int compressionQuality = 100)
+ : base()
+ {
+ uiImage = image;
+ this.originalFileName = originalFileName;
+ this.maximumWidth = maximumWidth;
+ this.maximumHeight = maximumHeight;
+ this.compressionQuality = Math.Max(0, Math.Min(100, compressionQuality));
+
+ // Determine output format: preserve PNG when appropriate, otherwise use JPEG
+ var extension = ShouldUsePngFormat() ? FileExtensions.Png : FileExtensions.Jpg;
+ FullPath = Guid.NewGuid().ToString() + extension;
+ FileName = FullPath;
+ }
+
+ bool ShouldUsePngFormat()
+ {
+ // Use PNG if:
+ // 1. High quality (>=90) and no resizing needed (preserves original format)
+ // 2. Original file was PNG
+ // 3. Image might have transparency (PNG supports alpha channel)
+
+ bool highQualityNoResize = compressionQuality >= 90 && !maximumWidth.HasValue && !maximumHeight.HasValue;
+ bool originalWasPng = !string.IsNullOrEmpty(originalFileName) &&
+ (originalFileName.EndsWith(".png", StringComparison.OrdinalIgnoreCase) ||
+ originalFileName.EndsWith(".PNG", StringComparison.OrdinalIgnoreCase));
+
+ // For very high quality or when original was PNG, preserve PNG format
+ return (compressionQuality >= 95 && !maximumWidth.HasValue && !maximumHeight.HasValue) || originalWasPng;
+ }
+
+ internal override Task PlatformOpenReadAsync()
+ {
+ if (data == null)
+ {
+ var normalizedImage = uiImage.NormalizeOrientation();
+
+ // First, apply resizing if needed
+ var workingImage = normalizedImage;
+ var originalSize = normalizedImage.Size;
+ var newSize = CalculateResizedDimensions(originalSize.Width, originalSize.Height, maximumWidth, maximumHeight);
+
+ if (newSize.Width != originalSize.Width || newSize.Height != originalSize.Height)
+ {
+ // Resize the image
+ UIGraphics.BeginImageContextWithOptions(newSize, false, normalizedImage.CurrentScale);
+ normalizedImage.Draw(new CoreGraphics.CGRect(CoreGraphics.CGPoint.Empty, newSize));
+ workingImage = UIGraphics.GetImageFromCurrentImageContext();
+ UIGraphics.EndImageContext();
+ }
+
+ // Then determine output format and apply compression
+ bool usePng = ShouldUsePngFormat();
+
+ if (usePng)
+ {
+ // Use PNG format - lossless compression, supports transparency
+ data = workingImage.AsPNG();
+ }
+ else
+ {
+ // Use JPEG with quality-based compression
+ if (compressionQuality < 90)
+ {
+ // Use JPEG compression with quality setting for aggressive compression
+ var qualityFloat = compressionQuality / 100.0f;
+ data = workingImage.AsJPEG(qualityFloat);
+ }
+ else if (compressionQuality < 100)
+ {
+ // Use JPEG with high quality
+ data = workingImage.AsJPEG(0.9f);
+ }
+ else
+ {
+ // Use JPEG with maximum quality
+ data = workingImage.AsJPEG(0.95f);
+ }
+ }
+ }
+
+ return Task.FromResult(data.AsStream());
+ }
+
+ static CoreGraphics.CGSize CalculateResizedDimensions(nfloat originalWidth, nfloat originalHeight, int? maxWidth, int? maxHeight)
+ {
+ if (!maxWidth.HasValue && !maxHeight.HasValue)
+ return new CoreGraphics.CGSize(originalWidth, originalHeight);
+
+ nfloat scaleWidth = maxWidth.HasValue ? (nfloat)maxWidth.Value / originalWidth : nfloat.MaxValue;
+ nfloat scaleHeight = maxHeight.HasValue ? (nfloat)maxHeight.Value / originalHeight : nfloat.MaxValue;
+
+ // Use the smaller scale to ensure both constraints are respected
+ nfloat scale = (nfloat)Math.Min(Math.Min((double)scaleWidth, (double)scaleHeight), 1.0); // Don't scale up
+
+ return new CoreGraphics.CGSize(originalWidth * scale, originalHeight * scale);
+ }
+ }
+
+ ///
+ /// FileResult implementation for processed images using MAUI Graphics
+ ///
+ internal class ProcessedImageFileResult : FileResult, IDisposable
+ {
+ readonly MemoryStream imageData;
+ readonly string originalFileName;
+
+ internal ProcessedImageFileResult(MemoryStream imageData, string originalFileName = null)
+ : base()
+ {
+ this.imageData = imageData;
+ this.originalFileName = originalFileName;
+
+ // Determine output format extension using ImageProcessor's improved logic
+ var extension = ImageProcessor.DetermineOutputExtension(imageData, 75, originalFileName);
+ FullPath = Guid.NewGuid().ToString() + extension;
+ FileName = FullPath;
+ }
+
+ internal override Task PlatformOpenReadAsync()
+ {
+ // Reset position and return a copy of the stream
+ imageData.Position = 0;
+ var copyStream = new MemoryStream(imageData.ToArray());
+ return Task.FromResult(copyStream);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ imageData?.Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
}
diff --git a/src/Essentials/src/MediaPicker/MediaPicker.shared.cs b/src/Essentials/src/MediaPicker/MediaPicker.shared.cs
index e1ea079496bd..a8b172618efc 100644
--- a/src/Essentials/src/MediaPicker/MediaPicker.shared.cs
+++ b/src/Essentials/src/MediaPicker/MediaPicker.shared.cs
@@ -144,6 +144,49 @@ internal static void SetDefault(IMediaPicker? implementation) =>
///
public class MediaPickerOptions
{
+ private int compressionQuality = 100;
+
+ ///
+ /// Gets or sets the compression quality for picked media.
+ /// The value should be between 0 and 100, where 0 is the lowest quality (most compression) and 100 is the highest quality (least compression).
+ ///
+ ///
+ /// Please note that performance might be affected by the compression quality, especially on lower-end devices.
+ /// For JPEG images, this controls the lossy compression quality directly.
+ /// For PNG images, values below 90 will convert to JPEG format for better compression. Values 90-99 will scale down the PNG image. Value 100 preserves original PNG format and quality.
+ ///
+ public int CompressionQuality
+ {
+ get => compressionQuality;
+ set => compressionQuality = Math.Max(0, Math.Min(100, value));
+ }
+
+ ///
+ /// Gets or sets the maximum width for image resizing.
+ /// When set, images will be resized to fit within this width while preserving aspect ratio.
+ /// A value of 0 or null means no width constraint.
+ ///
+ ///
+ /// This property only applies to images. It has no effect on video files.
+ /// The image will be resized to fit within the specified maximum dimensions while maintaining aspect ratio.
+ /// If both MaximumWidth and MaximumHeight are specified, the image will be scaled to fit within both constraints.
+ /// This resizing is applied before any compression quality settings.
+ ///
+ public int? MaximumWidth { get; set; }
+
+ ///
+ /// Gets or sets the maximum height for image resizing.
+ /// When set, images will be resized to fit within this height while preserving aspect ratio.
+ /// A value of 0 or null means no height constraint.
+ ///
+ ///
+ /// This property only applies to images. It has no effect on video files.
+ /// The image will be resized to fit within the specified maximum dimensions while maintaining aspect ratio.
+ /// If both MaximumWidth and MaximumHeight are specified, the image will be scaled to fit within both constraints.
+ /// This resizing is applied before any compression quality settings.
+ ///
+ public int? MaximumHeight { get; set; }
+
///
/// Gets or sets the title that is displayed when picking media.
///
diff --git a/src/Essentials/src/MediaPicker/MediaPicker.windows.cs b/src/Essentials/src/MediaPicker/MediaPicker.windows.cs
index d53bcd4e5965..539069cb4e27 100644
--- a/src/Essentials/src/MediaPicker/MediaPicker.windows.cs
+++ b/src/Essentials/src/MediaPicker/MediaPicker.windows.cs
@@ -6,11 +6,14 @@
#nullable enable
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel;
+using Microsoft.Maui.Essentials;
using Microsoft.Maui.Storage;
using Windows.Foundation.Collections;
+using Windows.Graphics.Imaging;
using Windows.Media.Capture;
using Windows.Storage;
using Windows.Storage.Pickers;
@@ -59,10 +62,35 @@ public Task> PickVideosAsync(MediaPickerOptions? options = null
// cancelled
if (result is null)
+ {
return null;
+ }
+
+ var fileResult = new FileResult(result);
+
+ // Apply compression/resizing if specified for photos
+ if (photo && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ using var originalStream = await result.OpenStreamForReadAsync();
+ var processedStream = await ImageProcessor.ProcessImageAsync(
+ originalStream,
+ options?.MaximumWidth,
+ options?.MaximumHeight,
+ options?.CompressionQuality ?? 100,
+ result.Name);
+
+ if (processedStream != null)
+ {
+ // Convert to MemoryStream for ProcessedImageFileResult
+ var memoryStream = new MemoryStream();
+ await processedStream.CopyToAsync(memoryStream);
+ processedStream.Dispose();
+
+ return new ProcessedImageFileResult(memoryStream, result.Name);
+ }
+ }
- // picked
- return new FileResult(result);
+ return fileResult;
}
public async Task> PickMultipleAsync(MediaPickerOptions? options, bool photo)
@@ -97,9 +125,44 @@ public async Task> PickMultipleAsync(MediaPickerOptions? option
{
return [];
}
+
+ var fileResults = result.Select(file => new FileResult(file)).ToList();
+
+ // Apply compression/resizing if specified for photos
+ if (photo && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ var compressedResults = new List();
+ for (int i = 0; i < result.Count; i++)
+ {
+ var originalFile = result[i];
+ var fileResult = fileResults[i];
+
+ using var originalStream = await originalFile.OpenStreamForReadAsync();
+ var processedStream = await ImageProcessor.ProcessImageAsync(
+ originalStream,
+ options?.MaximumWidth,
+ options?.MaximumHeight,
+ options?.CompressionQuality ?? 100,
+ originalFile.Name);
+
+ if (processedStream != null)
+ {
+ // Convert to MemoryStream for ProcessedImageFileResult
+ var memoryStream = new MemoryStream();
+ await processedStream.CopyToAsync(memoryStream);
+ processedStream.Dispose();
+
+ compressedResults.Add(new ProcessedImageFileResult(memoryStream, originalFile.Name));
+ }
+ else
+ {
+ compressedResults.Add(fileResult);
+ }
+ }
+ return compressedResults;
+ }
- // picked
- return [.. result.Select(file => new FileResult(file))];
+ return fileResults;
}
public Task CapturePhotoAsync(MediaPickerOptions? options = null)
@@ -113,14 +176,44 @@ public async Task> PickMultipleAsync(MediaPickerOptions? option
var captureUi = new WinUICameraCaptureUI();
if (photo)
+ {
captureUi.PhotoSettings.Format = CameraCaptureUIPhotoFormat.Jpeg;
+ }
else
+ {
captureUi.VideoSettings.Format = CameraCaptureUIVideoFormat.Mp4;
+ }
var file = await captureUi.CaptureFileAsync(photo ? CameraCaptureUIMode.Photo : CameraCaptureUIMode.Video);
if (file is not null)
- return new FileResult(file);
+ {
+ var fileResult = new FileResult(file);
+
+ // Apply compression/resizing if specified for photos
+ if (photo && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100))
+ {
+ using var originalStream = await file.OpenStreamForReadAsync();
+ var processedStream = await ImageProcessor.ProcessImageAsync(
+ originalStream,
+ options?.MaximumWidth,
+ options?.MaximumHeight,
+ options?.CompressionQuality ?? 100,
+ file.Name);
+
+ if (processedStream != null)
+ {
+ // Convert to MemoryStream for ProcessedImageFileResult
+ var memoryStream = new MemoryStream();
+ await processedStream.CopyToAsync(memoryStream);
+ processedStream.Dispose();
+
+ return new ProcessedImageFileResult(memoryStream, file.Name);
+ }
+ }
+
+ return fileResult;
+ }
return null;
}
@@ -208,4 +301,44 @@ public string GetFormatExtension() =>
};
}
}
+ ///
+ /// FileResult implementation for processed images using MAUI Graphics
+ ///
+ internal class ProcessedImageFileResult : FileResult, IDisposable
+ {
+ readonly MemoryStream imageData;
+
+ internal ProcessedImageFileResult(MemoryStream imageData, string? originalFileName = null)
+ : base()
+ {
+ this.imageData = imageData;
+
+ // Determine output format extension using ImageProcessor's improved logic
+ var extension = ImageProcessor.DetermineOutputExtension(imageData, 75, originalFileName);
+ FullPath = Guid.NewGuid().ToString() + extension;
+ FileName = FullPath;
+ }
+
+ internal override Task PlatformOpenReadAsync()
+ {
+ // Reset position and return a copy of the stream
+ imageData.Position = 0;
+ var copyStream = new MemoryStream(imageData.ToArray());
+ return Task.FromResult(copyStream);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ imageData?.Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
}
diff --git a/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
index ff4832f8fa0c..3669b9ff5825 100644
--- a/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -2,6 +2,12 @@
Microsoft.Maui.Devices.Sensors.IGeolocation.IsEnabled.get -> bool
Microsoft.Maui.Media.IMediaPicker.PickPhotosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
Microsoft.Maui.Media.IMediaPicker.PickVideosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.get -> int
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.set -> void
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.get -> int
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.set -> void
static Microsoft.Maui.Devices.Sensors.Geolocation.IsEnabled.get -> bool
diff --git a/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
index ff4832f8fa0c..3669b9ff5825 100644
--- a/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -2,6 +2,12 @@
Microsoft.Maui.Devices.Sensors.IGeolocation.IsEnabled.get -> bool
Microsoft.Maui.Media.IMediaPicker.PickPhotosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
Microsoft.Maui.Media.IMediaPicker.PickVideosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.get -> int
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.set -> void
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.get -> int
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.set -> void
static Microsoft.Maui.Devices.Sensors.Geolocation.IsEnabled.get -> bool
diff --git a/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
index ff4832f8fa0c..3669b9ff5825 100644
--- a/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -2,6 +2,12 @@
Microsoft.Maui.Devices.Sensors.IGeolocation.IsEnabled.get -> bool
Microsoft.Maui.Media.IMediaPicker.PickPhotosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
Microsoft.Maui.Media.IMediaPicker.PickVideosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.get -> int
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.set -> void
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.get -> int
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.set -> void
static Microsoft.Maui.Devices.Sensors.Geolocation.IsEnabled.get -> bool
diff --git a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
index ff4832f8fa0c..3669b9ff5825 100644
--- a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
@@ -2,6 +2,12 @@
Microsoft.Maui.Devices.Sensors.IGeolocation.IsEnabled.get -> bool
Microsoft.Maui.Media.IMediaPicker.PickPhotosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
Microsoft.Maui.Media.IMediaPicker.PickVideosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.get -> int
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.set -> void
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.get -> int
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.set -> void
static Microsoft.Maui.Devices.Sensors.Geolocation.IsEnabled.get -> bool
diff --git a/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt
index ff4832f8fa0c..3669b9ff5825 100644
--- a/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt
@@ -2,6 +2,12 @@
Microsoft.Maui.Devices.Sensors.IGeolocation.IsEnabled.get -> bool
Microsoft.Maui.Media.IMediaPicker.PickPhotosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
Microsoft.Maui.Media.IMediaPicker.PickVideosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.get -> int
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.set -> void
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.get -> int
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.set -> void
static Microsoft.Maui.Devices.Sensors.Geolocation.IsEnabled.get -> bool
diff --git a/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
index ff4832f8fa0c..3669b9ff5825 100644
--- a/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
@@ -2,6 +2,12 @@
Microsoft.Maui.Devices.Sensors.IGeolocation.IsEnabled.get -> bool
Microsoft.Maui.Media.IMediaPicker.PickPhotosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
Microsoft.Maui.Media.IMediaPicker.PickVideosAsync(Microsoft.Maui.Media.MediaPickerOptions? options = null) -> System.Threading.Tasks.Task!>!
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.get -> int
+Microsoft.Maui.Media.MediaPickerOptions.CompressionQuality.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumHeight.set -> void
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.get -> int?
+Microsoft.Maui.Media.MediaPickerOptions.MaximumWidth.set -> void
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.get -> int
Microsoft.Maui.Media.MediaPickerOptions.SelectionLimit.set -> void
static Microsoft.Maui.Devices.Sensors.Geolocation.IsEnabled.get -> bool
diff --git a/src/Graphics/src/Graphics/Platforms/Windows/PlatformImage.cs b/src/Graphics/src/Graphics/Platforms/Windows/PlatformImage.cs
index 6dd3b4b326d1..dd5a72214e4d 100644
--- a/src/Graphics/src/Graphics/Platforms/Windows/PlatformImage.cs
+++ b/src/Graphics/src/Graphics/Platforms/Windows/PlatformImage.cs
@@ -5,7 +5,9 @@
using System.Threading.Tasks;
using Microsoft.Graphics.Canvas;
using Microsoft.IO;
+using Windows.Foundation;
using Windows.Storage.Streams;
+using WinRect = Windows.Foundation.Rect;
#if MAUI_GRAPHICS_WIN2D
namespace Microsoft.Maui.Graphics.Win2D
@@ -78,7 +80,35 @@ public IImage Downsize(float maxWidthOrHeight, bool disposeOriginal = false)
public IImage Downsize(float maxWidth, float maxHeight, bool disposeOriginal = false)
{
- throw new NotImplementedException();
+ if (Width <= maxWidth && Height <= maxHeight)
+ return this;
+
+ // Calculate scale factor to fit within bounds while preserving aspect ratio
+ var scaleX = maxWidth / Width;
+ var scaleY = maxHeight / Height;
+ var scale = Math.Min(scaleX, scaleY);
+
+ var newWidth = (int)(Width * scale);
+ var newHeight = (int)(Height * scale);
+
+ // Create a new render target with the scaled dimensions
+ var newRenderTarget = new CanvasRenderTarget(_creator, newWidth, newHeight, 96);
+ using (var session = newRenderTarget.CreateDrawingSession())
+ {
+ // Draw the original bitmap scaled to the new size
+ session.DrawImage(_bitmap, new WinRect(0, 0, newWidth, newHeight));
+ }
+
+ if (disposeOriginal)
+ {
+ Dispose();
+ }
+
+#if MAUI_GRAPHICS_WIN2D
+ return new W2DImage(_creator, newRenderTarget);
+#else
+ return new PlatformImage(_creator, newRenderTarget);
+#endif
}
public IImage Resize(float width, float height, ResizeMode resizeMode = ResizeMode.Fit,