diff --git a/src/BlazorWebView/src/Maui/BlazorWebView.cs b/src/BlazorWebView/src/Maui/BlazorWebView.cs index 87d9f88cbbec..15922e86b364 100644 --- a/src/BlazorWebView/src/Maui/BlazorWebView.cs +++ b/src/BlazorWebView/src/Maui/BlazorWebView.cs @@ -2,8 +2,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.FileProviders; -using Microsoft.Maui.Controls; using Microsoft.Maui; +using Microsoft.Maui.Controls; namespace Microsoft.AspNetCore.Components.WebView.Maui { diff --git a/src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs b/src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs index 0c4a4160fdf6..d137f05d9695 100644 --- a/src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs +++ b/src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs @@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Components.WebView.WebView2; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; +using Microsoft.Maui.Platform; using Microsoft.Web.WebView2.Core; using Windows.ApplicationModel; using Windows.Storage.Streams; -using Microsoft.Maui.Platform; using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; namespace Microsoft.AspNetCore.Components.WebView.Maui diff --git a/src/BlazorWebView/src/Maui/iOS/BlazorWebViewHandler.iOS.cs b/src/BlazorWebView/src/Maui/iOS/BlazorWebViewHandler.iOS.cs index aff3b291c3cb..c6cfdea35039 100644 --- a/src/BlazorWebView/src/Maui/iOS/BlazorWebViewHandler.iOS.cs +++ b/src/BlazorWebView/src/Maui/iOS/BlazorWebViewHandler.iOS.cs @@ -267,7 +267,7 @@ public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask } var logger = _webViewHandler.Logger; - + logger.LogDebug("Intercepting request for {Url}.", url); // 1. First check if the app wants to modify or override the request. diff --git a/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.Components.cs b/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.Components.cs index d5dac9319441..24df331f3eef 100644 --- a/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.Components.cs +++ b/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.Components.cs @@ -2,9 +2,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebView.Maui; using Microsoft.Extensions.DependencyInjection; -using Xunit; using Microsoft.Maui.MauiBlazorWebView.DeviceTests.Components; using WebViewAppShared; +using Xunit; namespace Microsoft.Maui.MauiBlazorWebView.DeviceTests.Elements; diff --git a/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.RequestInterception.cs b/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.RequestInterception.cs index 0961e1ed2678..fccb1b4335fa 100644 --- a/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.RequestInterception.cs +++ b/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.RequestInterception.cs @@ -307,7 +307,7 @@ public Task RequestsCanBeInterceptedAndCancelledForDifferentHosts(string uriBase // 1. Create the response e.SetResponse(403, "Forbidden"); - + // 2. Let the app know we are handling it entirely e.Handled = true; } diff --git a/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.cs b/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.cs index fbf51c28c5d6..62b02014d689 100644 --- a/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.cs +++ b/src/BlazorWebView/tests/MauiDeviceTests/Elements/BlazorWebViewTests.cs @@ -13,7 +13,7 @@ public BlazorWebViewTests(ITestOutputHelper output) { Output = output; } - + public ITestOutputHelper Output { get; } sealed class BlazorWebViewWithCustomFiles : BlazorWebView diff --git a/src/BlazorWebView/tests/MauiDeviceTests/WebViewHelpers.Shared.cs b/src/BlazorWebView/tests/MauiDeviceTests/WebViewHelpers.Shared.cs index 3b9715f6cf9e..75816843f3fb 100644 --- a/src/BlazorWebView/tests/MauiDeviceTests/WebViewHelpers.Shared.cs +++ b/src/BlazorWebView/tests/MauiDeviceTests/WebViewHelpers.Shared.cs @@ -83,7 +83,7 @@ await ExecuteScriptAsync(webView, } throw new Exception($"Failed to deserialize result from controlDiv: {result}"); - + static bool TryDeserialize(string? result, out TInner? value) { if (result is null or "null" or "undefined") diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs index c1266651b653..e2c4354866ad 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs @@ -730,7 +730,7 @@ await Assert.ThrowsAsync(() => hybridWebView.InvokeJavaScriptAsync( function, HybridWebViewTestContext.Default.ResponseObject)); - + Assert.True(intercepted, "Request was not intercepted"); }); diff --git a/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs b/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs index 7d167ee6065b..96a4816a17e7 100644 --- a/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs +++ b/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs @@ -211,7 +211,7 @@ async Task LoadPhotoAsync(FileResult photo) } var stream = await photo.OpenReadAsync(); - + // Get image dimensions try { @@ -223,7 +223,7 @@ async Task LoadPhotoAsync(FileResult photo) { ImageDimensions = $"Unknown dimensions • {stream.Length:N0} bytes"; } - + PhotoSource = ImageSource.FromStream(() => stream); ImageByteLength = stream.Length; @@ -247,18 +247,18 @@ async Task LoadPhotoAsync(List photo) foreach (var item in photo) { var stream = await item.OpenReadAsync(); - + // 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; } diff --git a/src/Essentials/src/MediaPicker/ImageProcessor.shared.cs b/src/Essentials/src/MediaPicker/ImageProcessor.shared.cs index 246998413873..3437f1bdc22d 100644 --- a/src/Essentials/src/MediaPicker/ImageProcessor.shared.cs +++ b/src/Essentials/src/MediaPicker/ImageProcessor.shared.cs @@ -13,22 +13,22 @@ namespace Microsoft.Maui.Essentials; /// 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) - { + /// + /// 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; + // 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) { @@ -76,7 +76,7 @@ public static async Task ProcessImageAsync(Stream inputStream, image?.Dispose(); } #endif - } + } #if IOS || ANDROID || WINDOWS /// @@ -140,33 +140,33 @@ private static bool ShouldUsePngFormat(string originalFileName, int qualityPerce } #endif - /// - /// Determines if image processing is needed based on the provided options. - /// - public static bool IsProcessingNeeded(int? maxWidth, int? maxHeight, int qualityPercent) - { + /// + /// 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; + // 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) - { + } + + /// + /// 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"; + // 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); @@ -184,7 +184,7 @@ public static string DetermineOutputExtension(Stream imageData, int qualityPerce // Otherwise: use JPEG for better compression return (qualityPercent >= 95 || (qualityPercent >= 90 && originalWasPng)) ? ".png" : ".jpg"; #endif - } + } #if IOS || ANDROID || WINDOWS /// diff --git a/src/Essentials/src/MediaPicker/MediaPicker.android.cs b/src/Essentials/src/MediaPicker/MediaPicker.android.cs index 5e52610ec404..ab0786202ec2 100644 --- a/src/Essentials/src/MediaPicker/MediaPicker.android.cs +++ b/src/Essentials/src/MediaPicker/MediaPicker.android.cs @@ -140,7 +140,7 @@ void OnResult(Intent intent) } return new FileResult(path); } - + return null; } catch (OperationCanceledException) @@ -163,13 +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); } @@ -211,13 +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)); } } @@ -285,10 +285,12 @@ static async Task CompressImageIfNeeded(string imagePath, MediaPickerOpt await processedStream.CopyToAsync(outputStream); // Delete original file - try { originalFile.Delete(); } catch { } + 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; @@ -297,7 +299,7 @@ static async Task CompressImageIfNeeded(string imagePath, MediaPickerOpt { // If processing fails, return original path } - + return imagePath; } @@ -381,12 +383,12 @@ void OnResult(Intent resultIntent) { 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))); } diff --git a/src/Essentials/src/MediaPicker/MediaPicker.ios.cs b/src/Essentials/src/MediaPicker/MediaPicker.ios.cs index a5549fe252df..bb7195d3e9e5 100644 --- a/src/Essentials/src/MediaPicker/MediaPicker.ios.cs +++ b/src/Essentials/src/MediaPicker/MediaPicker.ios.cs @@ -455,18 +455,18 @@ internal static async Task CreateCompressedFromFileResult(FileResult 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 @@ -498,12 +498,12 @@ bool ShouldUsePngFormat() // 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) && + 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; } @@ -513,12 +513,12 @@ 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 @@ -527,10 +527,10 @@ internal override Task PlatformOpenReadAsync() workingImage = UIGraphics.GetImageFromCurrentImageContext(); UIGraphics.EndImageContext(); } - + // Then determine output format and apply compression bool usePng = ShouldUsePngFormat(); - + if (usePng) { // Use PNG format - lossless compression, supports transparency @@ -560,7 +560,7 @@ internal override Task PlatformOpenReadAsync() return Task.FromResult(data.AsStream()); } - + static CoreGraphics.CGSize CalculateResizedDimensions(nfloat originalWidth, nfloat originalHeight, int? maxWidth, int? maxHeight) { if (!maxWidth.HasValue && !maxHeight.HasValue) @@ -568,10 +568,10 @@ static CoreGraphics.CGSize CalculateResizedDimensions(nfloat originalWidth, nflo 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); } } diff --git a/src/Essentials/src/MediaPicker/MediaPicker.shared.cs b/src/Essentials/src/MediaPicker/MediaPicker.shared.cs index a8b172618efc..52d5e61d27dd 100644 --- a/src/Essentials/src/MediaPicker/MediaPicker.shared.cs +++ b/src/Essentials/src/MediaPicker/MediaPicker.shared.cs @@ -155,8 +155,8 @@ public class MediaPickerOptions /// 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 - { + public int CompressionQuality + { get => compressionQuality; set => compressionQuality = Math.Max(0, Math.Min(100, value)); } diff --git a/src/Essentials/src/MediaPicker/MediaPicker.windows.cs b/src/Essentials/src/MediaPicker/MediaPicker.windows.cs index 539069cb4e27..59597ef08cd6 100644 --- a/src/Essentials/src/MediaPicker/MediaPicker.windows.cs +++ b/src/Essentials/src/MediaPicker/MediaPicker.windows.cs @@ -67,7 +67,7 @@ public Task> PickVideosAsync(MediaPickerOptions? options = null } var fileResult = new FileResult(result); - + // Apply compression/resizing if specified for photos if (photo && ImageProcessor.IsProcessingNeeded(options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100)) { @@ -85,7 +85,7 @@ public Task> PickVideosAsync(MediaPickerOptions? options = null var memoryStream = new MemoryStream(); await processedStream.CopyToAsync(memoryStream); processedStream.Dispose(); - + return new ProcessedImageFileResult(memoryStream, result.Name); } } @@ -125,9 +125,9 @@ 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)) { @@ -136,7 +136,7 @@ public async Task> PickMultipleAsync(MediaPickerOptions? option { var originalFile = result[i]; var fileResult = fileResults[i]; - + using var originalStream = await originalFile.OpenStreamForReadAsync(); var processedStream = await ImageProcessor.ProcessImageAsync( originalStream, @@ -151,7 +151,7 @@ public async Task> PickMultipleAsync(MediaPickerOptions? option var memoryStream = new MemoryStream(); await processedStream.CopyToAsync(memoryStream); processedStream.Dispose(); - + compressedResults.Add(new ProcessedImageFileResult(memoryStream, originalFile.Name)); } else @@ -207,7 +207,7 @@ public async Task> PickMultipleAsync(MediaPickerOptions? option var memoryStream = new MemoryStream(); await processedStream.CopyToAsync(memoryStream); processedStream.Dispose(); - + return new ProcessedImageFileResult(memoryStream, file.Name); } }