Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 136 additions & 17 deletions src/Essentials/src/MediaPicker/MediaPicker.ios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,17 +283,17 @@ static async Task<List<FileResult>> PickerResultsToMediaFiles(PHPickerResult[] r
{
using var originalStream = await result.OpenReadAsync();
using var rotatedStream = await ImageProcessor.RotateImageAsync(originalStream, result.FileName);

// Create a temp file for the rotated image
var tempFileName = $"{Guid.NewGuid()}{Path.GetExtension(result.FileName)}";
var tempFilePath = Path.Combine(Path.GetTempPath(), tempFileName);

using (var fileStream = File.Create(tempFilePath))
{
rotatedStream.Position = 0;
await rotatedStream.CopyToAsync(fileStream);
}

var rotatedResult = new FileResult(tempFilePath)
{
FileName = result.FileName,
Expand All @@ -316,7 +316,7 @@ static async Task<List<FileResult>> PickerResultsToMediaFiles(PHPickerResult[] r
var compressedResults = new List<FileResult>();
foreach (var result in fileResults)
{
var compressedResult = await CompressedUIImageFileResult.CreateCompressedFromFileResult(result, options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100, options?.RotateImage ?? false, options?.PreserveMetaData ?? true);
var compressedResult = new PHPickerProcessedFileResult(result, options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100, options?.RotateImage ?? false, options?.PreserveMetaData ?? true);
compressedResults.Add(compressedResult);
}
return compressedResults;
Expand Down Expand Up @@ -365,7 +365,7 @@ static FileResult DictionaryToMediaFile(NSDictionary info, MediaPickerOptions op
if (!assetUrl.Scheme.Equals("assets-library", StringComparison.OrdinalIgnoreCase))
{
var docResult = new UIDocumentFileResult(assetUrl);

// Apply rotation if needed and this is a photo
if (ImageProcessor.IsRotationNeeded(options) && IsImageFile(docResult.FileName))
{
Expand All @@ -380,7 +380,7 @@ static FileResult DictionaryToMediaFile(NSDictionary info, MediaPickerOptions op
// If rotation fails, continue with the original file
}
}

return docResult;
}

Expand Down Expand Up @@ -411,7 +411,7 @@ static FileResult DictionaryToMediaFile(NSDictionary info, MediaPickerOptions op
{
img = img.NormalizeOrientation();
}

return new CompressedUIImageFileResult(img, null, options?.MaximumWidth, options?.MaximumHeight, options?.CompressionQuality ?? 100);
}
}
Expand All @@ -423,7 +423,7 @@ static FileResult DictionaryToMediaFile(NSDictionary info, MediaPickerOptions op

string originalFilename = PHAssetResource.GetAssetResources(phAsset).FirstOrDefault()?.OriginalFilename;
var assetResult = new PHAssetFileResult(assetUrl, phAsset, originalFilename);

// Apply rotation if needed and this is a photo
if (ImageProcessor.IsRotationNeeded(options) && IsImageFile(assetResult.FileName))
{
Expand All @@ -438,41 +438,41 @@ static FileResult DictionaryToMediaFile(NSDictionary info, MediaPickerOptions op
// If rotation fails, continue with the original file
}
}

return assetResult;
}

// Helper method to check if a file is an image based on extension
static bool IsImageFile(string fileName)
{
if (string.IsNullOrEmpty(fileName))
return false;

var ext = Path.GetExtension(fileName)?.ToLowerInvariant();
return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".heic" || ext == ".heif";
}

// Helper method to rotate an image file
static async Task<FileResult> RotateImageFile(FileResult original)
{
if (original == null)
return null;

try
{
using var originalStream = await original.OpenReadAsync();
using var rotatedStream = await ImageProcessor.RotateImageAsync(originalStream, original.FileName);

// Create a temp file for the rotated image
var tempFileName = $"{Guid.NewGuid()}{Path.GetExtension(original.FileName)}";
var tempFilePath = Path.Combine(Path.GetTempPath(), tempFileName);

using (var fileStream = File.Create(tempFilePath))
{
rotatedStream.Position = 0;
await rotatedStream.CopyToAsync(fileStream);
}

return new FileResult(tempFilePath, original.FileName);
}
catch (Exception ex)
Expand All @@ -481,7 +481,7 @@ static async Task<FileResult> RotateImageFile(FileResult original)
return original;
}
}

class PhotoPickerDelegate : UIImagePickerControllerDelegate
{
public Action<NSDictionary> CompletedHandler { get; set; }
Expand Down Expand Up @@ -739,4 +739,123 @@ public void Dispose()
GC.SuppressFinalize(this);
}
}

/// <summary>
/// Wrapper that applies compression lazily when the stream is opened.
/// This avoids iOS resource limits when processing multiple photos.
/// </summary>
class PHPickerProcessedFileResult : FileResult, IDisposable
{
readonly FileResult _originalResult;
readonly int? _maximumWidth;
readonly int? _maximumHeight;
readonly int _compressionQuality;
readonly bool _rotateImage;
readonly bool _preserveMetaData;

// Cached result of the first call to PlatformOpenReadAsync to avoid re-processing
byte[] _cachedData;

internal PHPickerProcessedFileResult(FileResult originalResult, int? maximumWidth, int? maximumHeight, int compressionQuality, bool rotateImage, bool preserveMetaData)
: base()
{
_originalResult = originalResult;
_maximumWidth = maximumWidth;
_maximumHeight = maximumHeight;
_compressionQuality = compressionQuality;
_rotateImage = rotateImage;
_preserveMetaData = preserveMetaData;

// Copy metadata from original, adjusting extension for compressed output
var originalFileName = originalResult.FileName;
var originalFullPath = originalResult.FullPath;
var originalContentType = originalResult.ContentType;

// Preserve the original format: PNG stays PNG, everything else compresses to JPEG.
// When no compression is applied (quality == 100), preserve the original extension as-is.
var originalWasPng = !string.IsNullOrEmpty(originalFileName) &&
Path.GetExtension(originalFileName).Equals(".png", StringComparison.OrdinalIgnoreCase);
string outputExtension;
if (compressionQuality == 100)
outputExtension = Path.GetExtension(originalFileName ?? string.Empty);
else if (originalWasPng)
outputExtension = ".png";
else
outputExtension = ".jpg";

FileName = !string.IsNullOrEmpty(originalFileName) && !string.IsNullOrEmpty(outputExtension)
? Path.ChangeExtension(originalFileName, outputExtension)
: originalFileName;

FullPath = !string.IsNullOrEmpty(originalFullPath) && !string.IsNullOrEmpty(outputExtension)
? Path.ChangeExtension(originalFullPath, outputExtension)
: originalFullPath;

ContentType = string.Equals(outputExtension, ".png", StringComparison.OrdinalIgnoreCase)
? "image/png"
: string.Equals(outputExtension, ".jpg", StringComparison.OrdinalIgnoreCase) || string.Equals(outputExtension, ".jpeg", StringComparison.OrdinalIgnoreCase)
? "image/jpeg"
: originalContentType;
}

internal override async Task<Stream> PlatformOpenReadAsync()
{
// Return cached result on subsequent calls to avoid re-processing
if (_cachedData is not null)
return new MemoryStream(_cachedData, writable: false);

// Load the original stream into memory once to avoid multiple expensive NSItemProvider loads.
byte[] originalData;
using (var originalStream = await _originalResult.OpenReadAsync())
using (var buffer = new MemoryStream())
{
await originalStream.CopyToAsync(buffer);
originalData = buffer.ToArray();
}

Stream processedStream = null;
try
{
// processedStream is always an independent MemoryStream from ImageProcessor.ProcessImageAsync;
// it does not reference or depend on originalStream after this call completes.
using var originalForProcessing = new MemoryStream(originalData, writable: false);
processedStream = await ImageProcessor.ProcessImageAsync(
originalForProcessing,
_maximumWidth,
_maximumHeight,
_compressionQuality,
_originalResult.FileName,
_rotateImage,
_preserveMetaData);
}
catch
{
// Swallow processing exceptions and fall back to the original data.
processedStream = null;
}

if (processedStream is null)
{
// Fall back to the original data if processing failed or returned null.
_cachedData = originalData;
}
else
{
using (processedStream)
using (var buffer = new MemoryStream())
{
await processedStream.CopyToAsync(buffer);
_cachedData = buffer.ToArray();
}
}

return new MemoryStream(_cachedData, writable: false);
}

public void Dispose()
{
(_originalResult as IDisposable)?.Dispose();
GC.SuppressFinalize(this);
}
}
}
Loading