[iOS/Android] MediaPicker: Fix image orientation when RotateImage=true#33892
[iOS/Android] MediaPicker: Fix image orientation when RotateImage=true#33892kubaflo merged 11 commits intodotnet:inflight/currentfrom
Conversation
|
✨ +100 points to Gryffindor for using the agent! 🦁 |
|
@kubaflo this PR agent is great! I will use it for every issue now |
🤖 AI Summary📊 Expand Full Review🔍 Pre-Flight — Context & Validation📝 Review Session — Removed tests ·
|
| File:Line | Reviewer Says | Author Says | Status |
|---|---|---|---|
| ios.cs:60 | Add null check for context | Not addressed | |
| ios.cs:93 | image.Draw() double-applies orientation |
Not addressed | |
| ios.cs:118 | Dispose rotatedImage/imageData | Not addressed | |
| android.cs:199 | Orientation 4 logic incorrect; need center transforms | Not addressed | |
| android.cs:65 | Indentation inconsistency | Not addressed |
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #33892 | iOS: Graphics context + CTM transforms; Android: EXIF-based matrix rotation | ⏳ PENDING (Gate) | 2 files | Has unaddressed HIGH SEVERITY bugs from prior review |
🚦 Gate — Test Verification
📝 Review Session — Removed tests · b86b2a4
Result: ❌ NO TESTS EXIST
Platform: Android (requested)
Mode: N/A - Cannot run verification
Status
No tests exist for this fix. The PR initially included UI tests (Issue32650.*), but they were removed in the latest commit (b86b2a4 - "Removed tests") in response to reviewer feedback that "The UI Test is not working".
Search Results
- Searched PR files (current): No test files exist for Issue32650
- Searched
src/Essentials/test/UnitTests/: No MediaPicker or ImageProcessor tests - Searched
src/Controls/tests/TestCases.HostApp/Issues/: No Issue32650 files - Searched
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/: No Issue32650 files
Why Tests Were Removed
From the prior agent review:
Gate: ❌ FAILED - Tests attempt to automate system UI (photo picker) which cannot be reliably automated
The UI tests tried to tap buttons that open the system photo picker, which blocks/hangs automation. Rather than redesigning the tests, the PR author chose to remove them entirely.
Impact
Gate cannot proceed to full verification without tests. Per the PR workflow:
- This phase is BLOCKED due to missing tests
- Proceeding to Fix phase to explore alternatives and perform code analysis
- Final recommendation must note missing tests as a required change before merge
🔧 Fix — Analysis & Comparison
📝 Review Session — Removed tests · b86b2a4
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-sonnet-4.5) | Android: Fix orientation 4 to vertical flip + center-based rotation using SetRotate with pivot | 1 file | Correct logic, compiles OK | |
| 2 | try-fix (claude-opus-4.6) | iOS: Null check for context + CGContext.DrawImage(cgImage) instead of image.Draw() + using for resources | 1 file | Correct logic, compiles OK | |
| 3 | try-fix (gpt-5.2) | Android: Canvas-based transforms; iOS: CGImageSource decode-time transform | 2 files | Builds OK | |
| 4 | try-fix (gpt-5.2-codex) | Translation-aware matrix per orientation + UIKit orientation-aware drawing + EXIF metadata normalization to 1 | 2 files | Builds OK; best overall approach - includes metadata reset | |
| 5 | try-fix (gemini-3-pro-preview) | Combined: Android PreScale for orientation 4 + iOS null check + using blocks | 2 files | Builds OK | |
| PR | PR #33892 | iOS: Graphics context + CTM transforms; Android: EXIF-based matrix rotation | 2 files | Has unaddressed bugs: orientation 4 wrong, no null check, resource leaks |
Cross-Pollination Round 2
| Model | Response |
|---|---|
| claude-sonnet-4.5 | NEW IDEA: Use direct matrix value arrays (9-element float arrays) on Android + UIGraphicsImageRenderer on iOS |
| claude-opus-4.6 | NEW IDEA: Use CGBitmapContext directly on iOS + reset EXIF orientation to 1 in output stream on Android |
| gpt-5.2 | NEW IDEA: Normalize EXIF orientation tag to 1 after rotation to prevent double-application by downstream |
| gpt-5.2-codex | NEW IDEA: CGImageSource thumbnail with CreateThumbnailWithTransform for iOS orientation handling |
| gemini-3-pro-preview | NEW IDEA: Use UIGraphicsImageRenderer to burn orientation into pixels on iOS |
Assessment: All Round 2 ideas are variations on approaches already covered in attempts 1-5. The EXIF normalization idea (gpt-5.2) was already implemented in Attempt 4. The CGImageSource thumbnail (gpt-5.2-codex) was covered in Attempt 3. The UIGraphicsImageRenderer approach (gemini) is essentially the same as the UIGraphics approach in the PR. No fundamentally different approaches emerge.
Exhausted: Yes - all 5 models ran, cross-pollination complete. No new fundamentally different approaches.
Selected Fix: No single try-fix attempt "wins" since none could be empirically tested.
Best Approach Based on Code Analysis:
The best combined fix (informed by all attempts) should:
- Android: Fix orientation 4 to vertical flip + apply translation after rotation to avoid cropping (Attempt 4's approach) + fix indentation
- iOS: Add null check for context + use
usingfor rotatedImage/imageData + optionally use CGContext.DrawImage instead of image.Draw
The PR's approach is fundamentally correct but has these bugs that must be fixed before merge:
- Android orientation 4 mapped to horizontal flip instead of vertical flip
- Android matrix transforms at origin (can crop rotated images)
- iOS no null check for context (risk of NullReferenceException)
- iOS resource leaks (rotatedImage and imageData not disposed)
- Android code indentation inconsistency
📋 Report — Final Recommendation
📝 Review Session — Removed tests · b86b2a4
⚠️ Final Recommendation: REQUEST CHANGES
Summary
This PR fixes a real issue where MediaPicker.PickPhotosAsync with RotateImage=true and PreserveMetaData=true doesn't properly rotate images. The fix approach (rotating actual pixel data instead of just metadata) is fundamentally correct. However, the implementation contains HIGH SEVERITY bugs that must be fixed before merge, and automated tests are missing.
Review Status
| Phase | Status | Result |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Context gathered; prior agent review analyzed |
| Gate | ❌ NO TESTS | Tests removed from PR (commit b86b2a4); no existing tests found |
| Fix | ✅ COMPLETE | 5 models × try-fix + cross-pollination; all BLOCKED (no empirical tests) |
| Report | ✅ COMPLETE | This report |
Root Cause
iOS: Previous code did UIImage.FromImage(image.CGImage, scale, UIImageOrientation.Up) — this creates a new UIImage with orientation metadata set to "Up" but uses the SAME underlying CGImage (pixel data unchanged). Result: metadata changed, pixels unchanged → image appears incorrectly oriented.
Android: Previous code used matrix.SetRotate(0) which rotated by 0 degrees (no-op). EXIF data was stripped without rotating pixels.
Both cases: the pixel data was never actually transformed.
Critical Issues Requiring Fixes
1. Android: EXIF Orientation 4 Mapped Incorrectly (HIGH SEVERITY)
File: src/Essentials/src/MediaPicker/ImageProcessor.android.cs:178
// BUG: orientation 4 = flip VERTICAL, but code treats it as horizontal flip
flipH = orientation is 2 or 4 or 5 or 7; // orientation 4 should NOT be hereEXIF specification: Orientation 4 = flip vertical = mirror across horizontal axis = PostScale(1, -1)
Fix:
bool flipH = orientation is 2 or 5 or 7;
bool flipV = orientation is 4;2. Android: Matrix Rotation Applied at Origin (MEDIUM SEVERITY)
File: src/Essentials/src/MediaPicker/ImageProcessor.android.cs:187-192
// BUG: Rotation at origin (0,0) can shift/crop image for 90°/270°
matrix.PostRotate(rotationAngle); // Should rotate around centerFix: Use matrix.PostRotate(rotationAngle, bitmap.Width / 2f, bitmap.Height / 2f) or apply translation compensation.
3. iOS: Missing Null Check for Graphics Context (HIGH SEVERITY)
File: src/Essentials/src/MediaPicker/ImageProcessor.ios.cs:53
var context = UIGraphics.GetCurrentContext();
// BUG: context can be null here, but used immediately below:
context.TranslateCTM(rotatedSize.Width, 0); // NullReferenceException riskFix:
var context = UIGraphics.GetCurrentContext();
if (context is null)
return inputStream;4. iOS: image.Draw() Double-Applies Orientation (MEDIUM SEVERITY)
File: src/Essentials/src/MediaPicker/ImageProcessor.ios.cs:93
image.Draw(CGPoint.Empty); // UIImage.Draw respects orientation metadataUIImage.Draw() applies orientation automatically, but CTM transforms were already manually applied for orientation. This double-applies orientation.
Fix: Use context.DrawImage(new CGRect(CGPoint.Empty, size), image.CGImage) with a Y-flip to draw raw pixels without orientation metadata being applied again.
5. iOS: Native Resource Leaks (LOW SEVERITY)
File: src/Essentials/src/MediaPicker/ImageProcessor.ios.cs:96-127
rotatedImage (UIImage) and imageData (NSData) are native resources but not disposed via using.
Fix:
using var rotatedImage = UIGraphics.GetImageFromCurrentImageContext();
// ...
using NSData? imageData = extension == ".png" ? rotatedImage.AsPNG() : rotatedImage.AsJPEG(1f);6. Android: Code Indentation Inconsistency (LOW SEVERITY)
File: src/Essentials/src/MediaPicker/ImageProcessor.android.cs:54-65
New lines at 54-65 use 2-tab indentation while surrounding code uses 3 tabs (inside the try block).
7. Missing Automated Tests (HIGH SEVERITY)
PR initially had UI tests but they were removed (commit b86b2a4) because UI tests that automate system photo picker hang. Tests should be added as unit tests that directly call ApplyExifOrientation() / RotateImageAsync() with known test images containing specific EXIF orientations.
PR Description Review
Title: ✅ Good - [iOS/Android] MediaPicker: Fix image orientation when RotateImage=true
Description: ✅ Good structure - has NOTE block, root cause, changes description, platforms.
Missing from description:
- The orientation 4 bug is not mentioned
- The EXIF tag is not reset to 1 in output (downstream apps could double-apply rotation)
What the PR Gets Right
- ✅ Approach is correct: Rotating pixel data is the right fix
- ✅ iOS try/finally:
EndImageContext()always called via try/finally - ✅ iOS rotated size: Correctly swaps W/H for Left/Right/LeftMirrored/RightMirrored
- ✅ iOS all orientations: All 7 non-Up cases are handled
- ✅ Android EXIF reading: Using ExifInterface correctly to get orientation value
- ✅ Android orientation early exit: Returns early for orientation 1 (normal)
- ✅ Error handling: Both files have try-catch with fallback to original stream
Labels Expected
s/agent-reviewed✅s/agent-changes-requested✅s/agent-gate-failed✅ (no tests exist)
📋 Expand PR Finalization Review
Title: ✅ Good
Current: [iOS/Android] MediaPicker: Fix image orientation when RotateImage=true
Description: ✅ Good
Description needs updates. See details below.
Code Review: ⚠️ Issues Found
Code Review: PR #33892
PR: [iOS/Android] MediaPicker: Fix image orientation when RotateImage=true
Files reviewed:
src/Essentials/src/MediaPicker/ImageProcessor.ios.cssrc/Essentials/src/MediaPicker/ImageProcessor.android.cs
🔴 Critical Issues
1. iOS: Null Dereference on UIGraphics.GetCurrentContext()
File: ImageProcessor.ios.cs line ~60
Problem: UIGraphics.GetCurrentContext() can return null. The code immediately calls context.TranslateCTM(...) etc. without a null check, which will throw a NullReferenceException and silently return inputStream (via the finally block calling EndImageContext), causing the image to be returned unrotated.
var context = UIGraphics.GetCurrentContext();
// Apply the appropriate transformation based on orientation
switch (image.Orientation)
{
case UIImageOrientation.Right:
context.TranslateCTM(rotatedSize.Width, 0); // ← NRE if context is nullRecommendation: Add a null check after getting the context:
var context = UIGraphics.GetCurrentContext();
if (context is null)
{
return inputStream;
}2. Android: Incorrect EXIF Orientation Mapping for Orientation 4 (Vertical Flip)
File: ImageProcessor.android.cs
Problem: EXIF orientation 4 is a vertical flip (flip on horizontal axis), but the code handles it as a horizontal flip by including it in flipH = orientation is 2 or 4 or 5 or 7. This will produce incorrect output for images with orientation 4.
EXIF spec:
- 2 = horizontal flip (mirror left-right) ✅ handled correctly as
flipH - 4 = vertical flip (mirror top-bottom) ❌ incorrectly handled as
flipH
The correct behavior for orientation 4 is matrix.PostScale(1, -1) (flip vertical), not matrix.PostScale(-1, 1) (flip horizontal).
Also, when PostScale(-1, 1) is applied around the origin (not the center), the image will be shifted entirely off-canvas. Matrix transforms should be applied around the bitmap center for correct results:
matrix.PostScale(-1, 1, bitmap.Width / 2f, bitmap.Height / 2f); // flip around centerRecommendation: Separate flip axes:
bool flipH = orientation is 2 or 5 or 7;
bool flipV = orientation is 4;
if (flipH) matrix.PostScale(-1, 1, bitmap.Width / 2f, bitmap.Height / 2f);
if (flipV) matrix.PostScale(1, -1, bitmap.Width / 2f, bitmap.Height / 2f);🟡 Significant Issues
3. iOS: image.Draw(CGPoint.Empty) May Double-Apply Orientation
File: ImageProcessor.ios.cs line ~93
Problem: UIImage.Draw(CGPoint) respects the image's Orientation property and applies its own orientation-aware drawing. Because CTM transforms are already applied manually in the switch statement, calling image.Draw(...) may double-apply the orientation (or produce other incorrect results for some orientations).
The safer approach is to draw the underlying CGImage directly, bypassing UIImage's orientation semantics:
// Instead of:
image.Draw(CGPoint.Empty);
// Use:
var rect = new CGRect(CGPoint.Empty, rotatedSize); // use original size for CGImage draw
context.DrawImage(rect, image.CGImage);Note: CGContext.DrawImage uses a flipped coordinate system (bottom-left origin), so you may need an additional ScaleCTM(1, -1) + TranslateCTM(0, -rotatedSize.Height) when switching from UIKit to CoreGraphics coordinates. The existing UIKit approach (UIGraphics.BeginImageContextWithOptions + image.Draw) is actually the standard iOS pattern for this. If image.Draw with the UIKit context is used, the CTM transforms should NOT include UIKit's implicit orientation — the transforms in the switch statement must account for this double-application, or the code should use image.CGImage for drawing. This needs careful verification or a test.
4. iOS: Missing Disposal of NSData / UIImage Resources
File: ImageProcessor.ios.cs lines ~106-118
Problem: rotatedImage (a UIImage) and imageData (an NSData) are disposable and hold native memory. Neither is disposed after encoding:
var rotatedImage = UIGraphics.GetImageFromCurrentImageContext();
// ...
imageData = rotatedImage.AsPNG(); // rotatedImage never disposed
// ...
await imageData.AsStream().CopyToAsync(outputStream); // imageData never disposed, AsStream() stream not disposedRecommendation:
using var rotatedImage = UIGraphics.GetImageFromCurrentImageContext();
// ...
using var imageData = rotatedImage.AsPNG();
using var imageDataStream = imageData.AsStream();
await imageDataStream.CopyToAsync(outputStream);5. Android: Indentation Inconsistency
File: ImageProcessor.android.cs lines ~54-66
Problem: The code added in RotateImageAsync (calling ApplyExifOrientation and checking the result) is mis-indented — it uses fewer tabs than the surrounding try-block code, making the structure visually misleading:
try
{
// ... (3-tab indent)
// Apply EXIF orientation correction ← WRONG: 2-tab indent, should be 3
Bitmap? rotatedBitmap = ApplyExifOrientation(originalBitmap, orientation);
if (rotatedBitmap is null)
{
return new MemoryStream(bytes);
}
// ... (back to 3-tab indent)Recommendation: Re-indent these lines to match the surrounding try block (3 tabs).
6. Android: Fragile Temp File Approach for EXIF Reading
File: ImageProcessor.android.cs — GetExifOrientation()
Problem: The method writes bytes to a temp file, creates an ExifInterface from it, then tries to delete it. This approach:
- Leaves temp files if an exception occurs between write and delete
- Creates file I/O overhead for every image
ExifInterfacesupports reading from a stream or byte array on Android API 24+ viaExifInterface(InputStream)
Recommendation: Use ExifInterface with a MemoryStream-backed InputStream:
using var inputStream = new Java.IO.ByteArrayInputStream(imageBytes);
var exif = new ExifInterface(inputStream);
return exif.GetAttributeInt(ExifInterface.TagOrientation, 1);This eliminates the temp file entirely.
🟢 Looks Good
- Core approach is correct: The fundamental fix — actually rotating pixel data rather than just changing metadata flags — is the right solution to MediaPicker.PickPhotosAsync does not preserve image orientation #32650.
- iOS handles
UIImageOrientation.Upearly exit: Before entering the expensive graphics context creation, the code correctly short-circuits forUporientation. ✅ - Android correctly short-circuits orientation 1: The method returns the original bitmap when
orientation == 1is detected beforeApplyExifOrientationis called. ✅ finallyblock forUIGraphics.EndImageContext(): Proper cleanup of the UIKit graphics context is ensured. ✅- Android uses
usingon the Matrix: TheMatrixobject is properly disposed after use. ✅ - The description matches the implementation: The PR description accurately explains what was changed on both platforms.
Previously Flagged (by Automated Reviewer) - Now Outdated
The following review comments are marked as "outdated" in GitHub, likely because the test files they referenced were removed/revised:
Issue32650.xaml.cs:ImageSource.FromStreamwith reusedMemoryStream(outdated - test files appear removed)Issue32650Tests.csnaming convention (outdated)UITestCategories.cs-MediaPickercategory not in pipeline YAML (outdated)- Tests not validating orientation correctness (outdated)
- Tests calling
App.Tapopening system picker (outdated)
Since these are marked outdated, they may no longer apply to the current state of the PR.
There was a problem hiding this comment.
Pull request overview
Fixes MediaPicker image rotation behavior on iOS/Android when RotateImage=true (not just updating orientation metadata), and adds a HostApp repro page plus UI test scaffolding for issue #32650.
Changes:
- iOS: replace metadata-only orientation fix with a graphics-context based pixel rotation path.
- Android: update EXIF-based rotation path to use the actual EXIF orientation value.
- Tests: add Issue32650 HostApp page + UI tests and introduce a new
UITestCategories.MediaPickercategory.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Essentials/src/MediaPicker/ImageProcessor.ios.cs | New context-based rotation logic intended to rotate pixels instead of metadata. |
| src/Essentials/src/MediaPicker/ImageProcessor.android.cs | Passes EXIF orientation into rotation routine and updates matrix logic. |
| src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs | Adds a new MediaPicker UI test category constant. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32650Tests.cs | Adds UI tests intended to cover the issue scenario. |
| src/Controls/tests/TestCases.HostApp/Issues/Issue32650.xaml | Adds a HostApp repro UI with buttons + image/label for verification. |
| src/Controls/tests/TestCases.HostApp/Issues/Issue32650.cs | Implements MediaPicker flows (pick/capture) and displays selected image. |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32650Tests.cs
Outdated
Show resolved
Hide resolved
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32650.cs
Outdated
Show resolved
Hide resolved
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32650.cs
Outdated
Show resolved
Hide resolved
jfversluis
left a comment
There was a problem hiding this comment.
Verification Results — Tested on Device with Actual PR Code
I built the Essentials sample app with InternalsVisibleTo access to call ImageProcessor.RotateImageAsync directly (the actual PR code, not a copy), deployed to both an Android emulator (API 34) and iOS simulator (iPhone 16 Pro, iOS 18.5), and ran automated tests covering all 8 EXIF orientations with both RotateImage=true and RotateImage=false.
✅ Android: 16/16 passed
All 8 EXIF orientations produce correct output dimensions, and RotateImage=false correctly passes raw bytes through unchanged.
| EXIF | RotateImage=true | RotateImage=false |
|---|---|---|
| Normal (1) | 200×120 ✅ | 200×120 ✅ |
| Rotate180 (3) | 200×120 ✅ | 200×120 ✅ |
| Rotate90CW (6) | 120×200 ✅ | 200×120 ✅ |
| Rotate270CW (8) | 120×200 ✅ | 200×120 ✅ |
| FlipH (2) | 200×120 ✅ | 200×120 ✅ |
| FlipV (4) | 200×120 ✅ | 200×120 ✅ |
| Transpose (5) | 120×200 ✅ | 200×120 ✅ |
| Transverse (7) | 120×200 ✅ | 200×120 ✅ |
The Android fix correctly restores the rotation logic that was lost during the refactoring from MediaPicker.android.cs into ImageProcessor.android.cs (commit 2871493 replaced matrix.PostRotate(rotationAngle) with a no-op matrix.SetRotate(0)).
❌ iOS: 12/16 — Bug in 90°/270° rotations
| Orientation | RotateImage=true | RotateImage=false |
|---|---|---|
| Up | 200×120 ✅ | 200×120 ✅ |
| Down (180°) | 200×120 ✅ | 200×120 ✅ |
| Left (90° CCW) | 200×120 ❌ (expected 120×200) | 200×120 ✅ |
| Right (90° CW) | 200×120 ❌ (expected 120×200) | 200×120 ✅ |
| UpMirrored | 200×120 ✅ | 200×120 ✅ |
| DownMirrored | 200×120 ✅ | 200×120 ✅ |
| LeftMirrored | 200×120 ❌ (expected 120×200) | 200×120 ✅ |
| RightMirrored | 200×120 ❌ (expected 120×200) | 200×120 ✅ |
Root cause: double-swap of dimensions
On iOS, image.Size returns orientation-corrected dimensions (not raw pixel dimensions). For a 200×120 raw image with Left orientation, image.Size = (120, 200) — iOS already accounts for the rotation.
The code on line 44 then swaps it again:
=> new CGSize(size.Height, size.Width) // (200, 120) — back to raw dims!This creates a graphics context with the wrong dimensions (200×120 instead of 120×200), causing the rotation output to be incorrect.
Additionally, LeftMirrored on line 88 has TranslateCTM(0, 0) which is a no-op — this is suspicious and likely a bug.
Suggested fix: Replace the manual CTM transforms with image.Draw()
The entire 60-line switch/case block (lines 40-98) can be replaced with 3 lines:
// image.Size is already orientation-corrected on iOS
// image.Draw() handles all rotation/flip transforms automatically
UIGraphics.BeginImageContextWithOptions(image.Size, false, image.CurrentScale);
image.Draw(new CGRect(CGPoint.Empty, image.Size));I verified this approach on the same iOS simulator — 16/16 passed with correct dimensions for all orientations including mirrored variants.
Bonus: UIGraphics.BeginImageContextWithOptions is deprecated
Both the current PR code and the suggested image.Draw() fix use UIGraphics.BeginImageContextWithOptions which is deprecated since iOS 17. The modern replacement is UIGraphicsImageRenderer. This is not a blocker for this PR but worth noting for a follow-up.
Summary
- Android fix: ✅ Ship it — Correct and verified
- iOS fix: ❌ Needs the
image.Draw()simplification to fix the double-swap bug on 90°/270° rotations
jfversluis
left a comment
There was a problem hiding this comment.
🔍 Comprehensive Re-Review — PR #33892
Device Verification Results
After the author applied the suggested image.Draw() fix for iOS, I rebuilt and retested the actual PR code on both platforms:
| Platform | Result | Details |
|---|---|---|
| Android (Pixel 5, API 34) | ✅ 16/16 passed | All 8 EXIF orientations × RotateImage=true/false |
| iOS (iPhone 16 Pro, iOS 18.5) | ✅ 16/16 passed | All 8 EXIF orientations × RotateImage=true/false |
The previous iOS failures (Left, Right, LeftMirrored, RightMirrored) are now resolved with image.Draw().
Code Review Findings
🔴 Critical — Android Transform Order for EXIF 5 & 7
In ImageProcessor.android.cs ApplyExifOrientation(), orientations 5 (Transpose) and 7 (Transverse) apply PostScale before PostRotate. Since PostConcat(T) builds M' = T * M, the resulting transform is flip→rotate when the EXIF standard requires rotate→flip.
The dimension-based tests pass because both orderings produce the same width/height swap, but pixel content would be incorrect (the results for orient 5 and 7 are effectively swapped).
Fix: Apply PostRotate first, then PostScale:
if (rotationAngle != 0)
matrix.PostRotate(rotationAngle, centerX, centerY);
if (flipHorizontal)
matrix.PostScale(-1, 1, centerX, centerY);⚠️ Medium — Temp file leaks on Android
GetExifOrientation, ExtractMetadataAsync, and ApplyMetadataAsync each create temp files that leak if an exception occurs between creation and deletion. Use try/finally for cleanup.
⚠️ Medium — ExifInterface objects never disposed on Android
ExifInterface extends Java.Lang.Object (implements IDisposable) but is never disposed in GetExifOrientation, ExtractMetadataAsync, or ApplyMetadataAsync. Should use using var exif = ....
⚠️ Medium — iOS EndImageContext after await
UIGraphics.BeginImageContextWithOptions uses a thread-local context stack. The await encodedStream.CopyToAsync(outputStream) could theoretically resume on a different thread, calling EndImageContext on the wrong stack. In practice this completes synchronously (in-memory streams), but it's safer to move EndImageContext before the await.
⚠️ Medium — iOS NSMutableData leak in ApplyMetadataAsync
NSMutableData.FromCapacity(0) at line 152 is returned via AsStream() but never disposed. The native memory may accumulate before GC collects it.
⚠️ Low — Metadata extraction order
In ProcessImageAsync, metadata is extracted from inputStream after rotation consumes it. Android seeks back (Position = 0), but iOS doesn't explicitly reset position before NSData.FromStream. Consider extracting metadata before rotation.
ℹ️ Info — UIGraphics.BeginImageContextWithOptions deprecated (iOS 17+)
Still works fine, but UIGraphicsImageRenderer is the modern replacement. Not blocking but worth noting for a future follow-up.
ℹ️ Info — Unrelated test project changes
Essentials.DeviceTests.csprojremoves a stale import (file doesn't exist)Essentials.UnitTests.csprojaddsDebugType=portable
Both are harmless but unrelated to the ImageProcessor fix. Consider splitting into a separate commit.
Summary
The core rotation logic works correctly on both platforms — verified with device testing. The image.Draw() approach for iOS is clean and handles all 8 orientations properly. The Android EXIF handling correctly restores the rotation that was accidentally removed in a previous refactoring.
Blocking issue: The Android transform order for EXIF orientations 5 and 7 should be corrected (pixel content is swapped between these two orientations). Everything else is non-blocking and can be addressed in follow-up work.
Overall: 👍 Great improvement over the previous state. With the orient 5/7 fix, this is ready to go.
…ream disposal - Add AutomationId attributes to XAML elements so Appium can find them - Set Issue property to match the [Issue] attribute Description for navigation - Copy stream to MemoryStream before passing to ImageSource.FromStream to avoid ObjectDisposedException - Simplify MediaPickerPreservesImageOrientation test to verify page elements exist
Renamed src/Controls/tests/TestCases.HostApp/Issues/Issue32650.cs to Issue32650.xaml.cs and src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32650Tests.cs to Issue32650.cs. Updated the test class name from Issue32650Tests to Issue32650 and adjusted the constructor to match.
… NSMutableData leak, iOS thread affinity - Android ApplyExifOrientation: apply PostRotate before PostScale so orientations 5 & 7 match EXIF spec (rotate-then-flip) - Android GetExifOrientation/ExtractMetadataAsync/ApplyMetadataAsync: use 'using var exif' to dispose ExifInterface and restructure with try/finally to guarantee temp file cleanup even on exceptions - iOS RotateImageAsync: call UIGraphics.EndImageContext() before any await to respect UIGraphics' thread-local context stack - iOS ApplyMetadataAsync: use 'using var outputData' on NSMutableData and return MemoryStream copy instead of stream backed by native memory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33892Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33892" |
|
@jfversluis done, also PR was updated to newest main |
jfversluis
left a comment
There was a problem hiding this comment.
Review Summary
The core rotation fix is correct and verified — all 8 EXIF orientations produce correct pixel-level results on both Android and iOS.
Android: The PostRotate-then-PostScale matrix approach is mathematically correct for all orientations including the tricky transpose (5) and transverse (7) cases. Resource management improvements (ExifInterface using, temp file try/finally) look good.
iOS: The image.Draw() approach correctly leverages UIKit's built-in orientation handling. EndImageContext is properly called before any await (thread-affinity fix). NSMutableData disposal with ToArray() copy is correct.
Pre-existing issues in ProcessImageAsync (shared file, not changed by this PR)
While reviewing the full pipeline, two pre-existing issues were identified in ImageProcessor.shared.cs that are not introduced by this PR but worth tracking separately:
-
EXIF Orientation tag re-applied after pixel rotation — When both
rotateImageandpreserveMetaDataare true,ExtractMetadataAsyncpreserves the original Orientation tag, thenApplyMetadataAsyncwrites it back to the already-rotated image. On Android this could cause double-rotation by viewers. On iOS this is masked by issue 2. -
iOS metadata silently lost when rotation is performed —
RotateImageAsyncconsumesinputStreamviaNSData.FromStream(). ThenExtractMetadataAsync(inputStream)is called on the same consumed stream without resetting position → reads 0 bytes → metadata silently discarded. (Android'sExtractMetadataAsyncdoes reset position, so it doesn't have this problem.)
These may be related to #33827 (EXIF data loss on Android). I'd recommend tracking them as separate issues.
The rotation fix itself is solid. Nice work! 👍
#33892) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause When `MediaPicker.PickPhotosAsync` is called with `RotateImage=true` and `PreserveMetaData=true`, images were not being properly rotated: - **iOS**: The previous implementation used `UIImage.FromImage()` with `UIImageOrientation.Up` which only changed the orientation metadata flag, not the actual pixel data - **Android**: The previous implementation used `matrix.SetRotate(0)` which performed no actual rotation ### Description of Change **iOS (`ImageProcessor.ios.cs`):** - Now creates a graphics context with proper dimensions based on orientation - Applies appropriate CTM transformations (translate, rotate, scale) based on the image's `UIImageOrientation` - Actually draws the rotated image pixels, ensuring proper display regardless of how the consuming app reads the image **Android (`ImageProcessor.android.cs`):** - Now passes the actual EXIF orientation value to `ApplyExifOrientation()` - Uses `matrix.PostRotate()` with the correct angle based on EXIF orientation (1=0°, 3=180°, 6=90°, 8=270°) - Returns original bitmap when no rotation needed (orientation 1) ### Issues Fixed Fixes #32650 ### Platforms Affected - iOS ✅ - Android ✅ --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <kubaflo123@gmail.com> Co-authored-by: michalpobuta <mpobuta.consultant@ra.org>
#33892) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause When `MediaPicker.PickPhotosAsync` is called with `RotateImage=true` and `PreserveMetaData=true`, images were not being properly rotated: - **iOS**: The previous implementation used `UIImage.FromImage()` with `UIImageOrientation.Up` which only changed the orientation metadata flag, not the actual pixel data - **Android**: The previous implementation used `matrix.SetRotate(0)` which performed no actual rotation ### Description of Change **iOS (`ImageProcessor.ios.cs`):** - Now creates a graphics context with proper dimensions based on orientation - Applies appropriate CTM transformations (translate, rotate, scale) based on the image's `UIImageOrientation` - Actually draws the rotated image pixels, ensuring proper display regardless of how the consuming app reads the image **Android (`ImageProcessor.android.cs`):** - Now passes the actual EXIF orientation value to `ApplyExifOrientation()` - Uses `matrix.PostRotate()` with the correct angle based on EXIF orientation (1=0°, 3=180°, 6=90°, 8=270°) - Returns original bitmap when no rotation needed (orientation 1) ### Issues Fixed Fixes #32650 ### Platforms Affected - iOS ✅ - Android ✅ --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <kubaflo123@gmail.com> Co-authored-by: michalpobuta <mpobuta.consultant@ra.org>
#33892) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause When `MediaPicker.PickPhotosAsync` is called with `RotateImage=true` and `PreserveMetaData=true`, images were not being properly rotated: - **iOS**: The previous implementation used `UIImage.FromImage()` with `UIImageOrientation.Up` which only changed the orientation metadata flag, not the actual pixel data - **Android**: The previous implementation used `matrix.SetRotate(0)` which performed no actual rotation ### Description of Change **iOS (`ImageProcessor.ios.cs`):** - Now creates a graphics context with proper dimensions based on orientation - Applies appropriate CTM transformations (translate, rotate, scale) based on the image's `UIImageOrientation` - Actually draws the rotated image pixels, ensuring proper display regardless of how the consuming app reads the image **Android (`ImageProcessor.android.cs`):** - Now passes the actual EXIF orientation value to `ApplyExifOrientation()` - Uses `matrix.PostRotate()` with the correct angle based on EXIF orientation (1=0°, 3=180°, 6=90°, 8=270°) - Returns original bitmap when no rotation needed (orientation 1) ### Issues Fixed Fixes #32650 ### Platforms Affected - iOS ✅ - Android ✅ --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <kubaflo123@gmail.com> Co-authored-by: michalpobuta <mpobuta.consultant@ra.org>
#33892) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause When `MediaPicker.PickPhotosAsync` is called with `RotateImage=true` and `PreserveMetaData=true`, images were not being properly rotated: - **iOS**: The previous implementation used `UIImage.FromImage()` with `UIImageOrientation.Up` which only changed the orientation metadata flag, not the actual pixel data - **Android**: The previous implementation used `matrix.SetRotate(0)` which performed no actual rotation ### Description of Change **iOS (`ImageProcessor.ios.cs`):** - Now creates a graphics context with proper dimensions based on orientation - Applies appropriate CTM transformations (translate, rotate, scale) based on the image's `UIImageOrientation` - Actually draws the rotated image pixels, ensuring proper display regardless of how the consuming app reads the image **Android (`ImageProcessor.android.cs`):** - Now passes the actual EXIF orientation value to `ApplyExifOrientation()` - Uses `matrix.PostRotate()` with the correct angle based on EXIF orientation (1=0°, 3=180°, 6=90°, 8=270°) - Returns original bitmap when no rotation needed (orientation 1) ### Issues Fixed Fixes #32650 ### Platforms Affected - iOS ✅ - Android ✅ --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <kubaflo123@gmail.com> Co-authored-by: michalpobuta <mpobuta.consultant@ra.org>
#33892) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause When `MediaPicker.PickPhotosAsync` is called with `RotateImage=true` and `PreserveMetaData=true`, images were not being properly rotated: - **iOS**: The previous implementation used `UIImage.FromImage()` with `UIImageOrientation.Up` which only changed the orientation metadata flag, not the actual pixel data - **Android**: The previous implementation used `matrix.SetRotate(0)` which performed no actual rotation ### Description of Change **iOS (`ImageProcessor.ios.cs`):** - Now creates a graphics context with proper dimensions based on orientation - Applies appropriate CTM transformations (translate, rotate, scale) based on the image's `UIImageOrientation` - Actually draws the rotated image pixels, ensuring proper display regardless of how the consuming app reads the image **Android (`ImageProcessor.android.cs`):** - Now passes the actual EXIF orientation value to `ApplyExifOrientation()` - Uses `matrix.PostRotate()` with the correct angle based on EXIF orientation (1=0°, 3=180°, 6=90°, 8=270°) - Returns original bitmap when no rotation needed (orientation 1) ### Issues Fixed Fixes #32650 ### Platforms Affected - iOS ✅ - Android ✅ --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <kubaflo123@gmail.com> Co-authored-by: michalpobuta <mpobuta.consultant@ra.org>
## What's Coming .NET MAUI inflight/candidate introduces significant improvements across all platforms with focus on quality, performance, and developer experience. This release includes 66 commits with various improvements, bug fixes, and enhancements. ## Activityindicator - [Android] Implemented material3 support for ActivityIndicator by @Dhivya-SF4094 in #33481 <details> <summary>🔧 Fixes</summary> - [Implement material3 support for ActivityIndicator](#33479) </details> - [iOS] Fix: ActivityIndicator IsRunning ignores IsVisible when set to true by @bhavanesh2001 in #28983 <details> <summary>🔧 Fixes</summary> - [[iOS] [ActivityIndicator] `IsRunning` ignores `IsVisible` when set to `true`](#28968) </details> ## Button - [iOS] Button RTL text and image overlap - fix by @kubaflo in #29041 ## Checkbox - [iOS/MacCatalyst] Fix CheckBox foreground color not resetting when set to null by @Ahamed-Ali in #34284 <details> <summary>🔧 Fixes</summary> - [[iOS] Color of the checkBox control is not properly worked on dynamic scenarios](#34278) </details> ## CollectionView - [iOS] Fix: CollectionView does not clear selection when SelectedItem is set to null by @Tamilarasan-Paranthaman in #30420 <details> <summary>🔧 Fixes</summary> - [CollectionView not being able to remove selected item highlight on iOS](#30363) - [[MAUI] Select items traces are preserved](#26187) </details> - [iOS] CV2 ItemsLayout update by @kubaflo in #28675 <details> <summary>🔧 Fixes</summary> - [CollectionView CollectionViewHandler2 doesnt change ItemsLayout on DataTrigger](#28656) - [iOS CollectionView doesn't respect a change to ItemsLayout when using Items2.CollectionViewHandler2](#31259) </details> - [iOS][CV2] Fix CollectionView renders large empty space at bottom of view by @devanathan-vaithiyanathan in #31215 <details> <summary>🔧 Fixes</summary> - [[iOS] [MacCatalyst] CollectionView renders large empty space at bottom of view](#17799) - [[iOS/Mac] CollectionView2 EmptyView takes up large horizontal space even when the content is small](#33201) </details> - [iOS] Fixed issue where group Header/Footer template was set to all items when IsGrouped was true for an ObservableCollection by @Tamilarasan-Paranthaman in #29144 <details> <summary>🔧 Fixes</summary> - [[iOS] Group Header/Footer Repeated for All Items When IsGrouped is True for ObservableCollection in CollectionView](#29141) </details> - [Android] Fix CollectionView selection crash with HeaderTemplate by @NirmalKumarYuvaraj in #34275 <details> <summary>🔧 Fixes</summary> - [[Bug] [Android] System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index](#34247) </details> ## DateTimePicker - [iOS] Fix TimePicker AM/PM frequently changes when the app is closed and reopened by @devanathan-vaithiyanathan in #31066 <details> <summary>🔧 Fixes</summary> - [[iOS] TimePicker AM/PM frequently changes when the app is closed and reopened](#30837) - [Maui 10 iOS TimePicker Strange Characters in place of AM/PM](#33722) </details> - Android TimePicker ignores 24 hour system setting when using Format Property - fix by @kubaflo in #28797 <details> <summary>🔧 Fixes</summary> - [Android TimePicker ignores 24 hour system setting when using Format Property](#28784) </details> ## Drawing - [iOS, Mac, Windows] GraphicsView: Fix Background/BackgroundColor not updating by @NirmalKumarYuvaraj in #31254 <details> <summary>🔧 Fixes</summary> - [[iOS, Mac, Windows] GraphicsView does not change the Background/BackgroundColor](#31239) </details> - [iOS] GraphicsView DrawString - fix by @kubaflo in #26304 <details> <summary>🔧 Fixes</summary> - [DrawString not rendering in iOS.](#24450) - [GraphicsView DrawString not rendering in iOS](#8486) - [DrawString doesn't work on maccatalyst](#4993) </details> - [Android] - Fix Shadow Rendering For Transparent Fill, Stroke (Lines), and Text on Shapes by @prakashKannanSf3972 in #29528 <details> <summary>🔧 Fixes</summary> - [Ellipse Transparency Not Rendered When Drawing Arc Inside the Ellipse Using GraphicsView on Android](#29394) </details> - Revert "[iOS, Mac, Windows] GraphicsView: Fix Background/BackgroundColor not updating (#31254)" by @Ahamed-Ali via @Copilot in #34508 ## Entry - [iOS 26] Fix Entry MaxLength not enforced due to new multi-range delegate by @kubaflo in #32045 <details> <summary>🔧 Fixes</summary> - [iOS 26 - The MaxLength property value is not respected on an Entry control.](#32016) - [.NET MAUI Entry Maximum Length not working on iOS and macOS](#33316) </details> - [iOS] Fixed Entry with IsPassword toggling loses previously entered text by @SubhikshaSf4851 in #30572 <details> <summary>🔧 Fixes</summary> - [Entry with IsPassword toggling loses previously entered text on iOS when IsPassword is re-enabled](#30085) </details> ## Essentials - Fix for FilePicker PickMultipleAsync nullable reference type by @SuthiYuvaraj in #33163 <details> <summary>🔧 Fixes</summary> - [FilePicker PickMultipleAsync nullable reference type](#33114) </details> - Replace deprecated NetworkReachability with NWPathMonitor on iOS/macOS by @jfversluis via @Copilot in #32354 <details> <summary>🔧 Fixes</summary> - [NetworkReachability is obsolete on iOS/maccatalyst 17.4+](#32312) - [Use NWPathMonitor on iOS for Essentials Connectivity](#2574) </details> ## Essentials Connectivity - Update Android Connectivity implementation to use modern APIs by @jfversluis via @Copilot in #30348 <details> <summary>🔧 Fixes</summary> - [Update the Android Connectivity implementation to user modern APIs](#30347) </details> ## Flyout - [iOS] Fixed Flyout icon not updating when root page changes using InsertPageBefore by @Vignesh-SF3580 in #29924 <details> <summary>🔧 Fixes</summary> - [[iOS] Flyout icon not replaced by back button when root page is changed using InsertPageBefore](#29921) </details> ## Flyoutpage - [iOS] Flyout Items Not Displayed in RightToLeft FlowDirection in Landscape - fix by @kubaflo in #26762 <details> <summary>🔧 Fixes</summary> - [Flyout Items Not Displayed in RightToLeft FlowDirection on iOS in Landscape Orientation and Hamburger Icon Positioned Incorrectly](#26726) </details> ## Image - [Android] Implemented Material3 support for Image by @Dhivya-SF4094 in #33661 <details> <summary>🔧 Fixes</summary> - [Implement Material3 support for Image](#33660) </details> ## Keyboard - [iOS] Fix gap at top of view after rotating device while Entry keyboard is visible by @praveenkumarkarunanithi in #34328 <details> <summary>🔧 Fixes</summary> - [Focusing and entering texts on entry control causes a gap at the top after rotating simulator.](#33407) </details> ## Label - [Android] Support for images inside HTML label by @kubaflo in #21679 <details> <summary>🔧 Fixes</summary> - [Label with HTML TextType does not display images on Android](#21044) </details> - [fix] ContentLabel Moved to a nested class to prevent CS0122 in external source generators by @SubhikshaSf4851 in #34514 <details> <summary>🔧 Fixes</summary> - [[MAUI] Building Maui App with sample content results CS0122 errors.](#34512) </details> ## Layout - Optimize ordering of children in Flex layout by @symbiogenesis in #21961 - [Android] Fix control size properties not available during Loaded event by @Vignesh-SF3580 in #31590 <details> <summary>🔧 Fixes</summary> - [CollectionView on Android does not provide height, width, logical children once loaded, works fine on Windows](#14364) - [Control's Loaded event invokes before calling its measure override method.](#14160) </details> ## Mediapicker - [iOS/Android] MediaPicker: Fix image orientation when RotateImage=true by @michalpobuta in #33892 <details> <summary>🔧 Fixes</summary> - [MediaPicker.PickPhotosAsync does not preserve image orientation](#32650) </details> ## Modal - [Windows] Fix modal page keyboard focus not shifting to newly opened modal by @jfversluis in #34212 <details> <summary>🔧 Fixes</summary> - [Keyboard focus does not shift to a newly opened modal page: Pressing enter clicks the button on the page beneath the modal page](#22938) </details> ## Navigation - [iOS26] Apply view margins in title view by @kubaflo in #32205 <details> <summary>🔧 Fixes</summary> - [NavigationPage TitleView iOS 26](#32200) </details> - [iOS] System.NullReferenceException at NavigationRenderer.SetStatusBarStyle() by @kubaflo in #29564 <details> <summary>🔧 Fixes</summary> - [System.NullReferenceException at NavigationRenderer.SetStatusBarStyle()](#29535) </details> - [iOS 26] Fix back button color not applied for NavigationPage by @Shalini-Ashokan in #34326 <details> <summary>🔧 Fixes</summary> - [[iOS] Color not applied to the Back button text or image on iOS 26](#33966) </details> ## Picker - Fix Picker layout on Mac Catalyst 26+ by @kubaflo in #33146 <details> <summary>🔧 Fixes</summary> - [[MacOS 26] Text on picker options are not centered on macOS 26.1](#33229) </details> ## Progressbar - [Android] Implemented Material3 support for ProgressBar by @SyedAbdulAzeemSF4852 in #33926 <details> <summary>🔧 Fixes</summary> - [Implement Material3 support for Progressbar](#33925) </details> ## RadioButton - [iOS, Mac] Fix for RadioButton TextColor for plain Content not working by @HarishwaranVijayakumar in #31940 <details> <summary>🔧 Fixes</summary> - [RadioButton: TextColor for plain Content not working on iOS](#18011) </details> - [All Platforms] Fix RadioButton warning when ControlTemplate is set with View content by @kubaflo in #33839 <details> <summary>🔧 Fixes</summary> - [Seeking clarification on RadioButton + ControlTemplate + Content documentation](#33829) </details> - Visual state change for disabled RadioButton by @kubaflo in #23471 <details> <summary>🔧 Fixes</summary> - [RadioButton disabled UI issue - iOS](#18668) </details> ## SafeArea - [Android] Fix for TabbedPage BottomNavigation BarBackgroundColor not extending to system navigation bar by @praveenkumarkarunanithi in #33428 <details> <summary>🔧 Fixes</summary> - [[Android] TabbedPage BottomNavigation BarBackgroundColor does not extend to system navigation bar area in Edge-to-Edge mode](#33344) </details> ## ScrollView - [Android] ScrollView: Fix HorizontalScrollBarVisibility not updating immediately at runtime by @SubhikshaSf4851 in #33528 <details> <summary>🔧 Fixes</summary> - [Runtime Scrollbar visibility not updating correctly on Android and macOS platforms.](#33400) </details> - Fixed crash when calling ItemsView.ScrollTo on unloaded CollectionView by @kubaflo in #25444 <details> <summary>🔧 Fixes</summary> - [App crashes when calling ItemsView.ScrollTo on unloaded CollectionView](#23014) </details> ## Shell - [Shell] Update logic for iOS large title display in ShellItemRenderer by @kubaflo in #33246 - [iOS][Shell] Fix navigation lifecycle and back button for More tab (>5 tabs) by @kubaflo in #27932 <details> <summary>🔧 Fixes</summary> - [OnAppearing and OnNavigatedTo does not work when using extended Tabbar (tabbar with more than 5 tabs) on IOS.](#27799) - [Shell.BackButtonBehavior does not work when using extended Tabbar (tabbar with more than 5 tabs)on IOS.](#27800) - [Shell TabBar More button causes ViewModel command binding disconnection on back navigation](#30862) - [Content page onappearing not firing if tabs are on the more tab on IOS](#31166) </details> - [iOS 26] Fix tab bar ghosting when navigating from modal to tabbed Shell content by @SubhikshaSf4851 in #34254 <details> <summary>🔧 Fixes</summary> - [[iOS] Tab bar ghosting issue on iOS 26 (liquid glass)](#34143) </details> - Fix for Shell tab visibility not updating when navigating back multiple pages by @BagavathiPerumal in #34403 <details> <summary>🔧 Fixes</summary> - [Changing Shell Tab Visibility when navigating back multiple pages ignores Shell Tab Visibility](#33351) </details> - [iOS/Mac] Fixed OnBackButtonPressed not firing for Shell Navigation Bar Button by @Dhivya-SF4094 in #34401 <details> <summary>🔧 Fixes</summary> - [[iOS] OnBackButtonPressed not firing for Shell Navigation Bar button](#34190) </details> ## Slider - [iOS] Fix for Slider ThumbImageSource is not centered properly on iOS 26 by @HarishwaranVijayakumar in #34019 <details> <summary>🔧 Fixes</summary> - [[iOS 26] Slider ThumbImageSource is not centered properly](#33967) </details> - [Android] Fix improper rendering of ThumbimageSource in Slider by @NirmalKumarYuvaraj in #34064 <details> <summary>🔧 Fixes</summary> - [[Slider] MAUI Slider thumb image is big on android](#13258) </details> ## Stepper - [iOS] Fix Stepper layout overlap in landscape on iOS 26 by @Vignesh-SF3580 in #34325 <details> <summary>🔧 Fixes</summary> - [[.NET10] D10 - Customize cursor position - Rotating simulator makes the button and label overlap](#34273) </details> ## SwipeView - [iOS] SwipeView: Honor FontImageSource.Color in SwipeItem icon by @kubaflo in #27389 <details> <summary>🔧 Fixes</summary> - [[iOS] SwipeView: SwipeItem.IconImageSource.FontImageSource color value not honored](#27377) </details> ## Switch - [Android] Fix Switch thumb shadow missing when ThumbColor is set by @Shalini-Ashokan in #33960 <details> <summary>🔧 Fixes</summary> - [Android Switch Control Thumb Shadow](#19676) </details> ## Toolbar - [iOS/Mac Catalyst 26] Fix Shell.ForegroundColor not applied to ToolbarItems by @SyedAbdulAzeemSF4852 in #34085 <details> <summary>🔧 Fixes</summary> - [[iOS26] Shell.ForegroundColor is not applied to ToolbarItems](#34083) </details> - [Android] VoiceOver on Toolbar Item by @kubaflo in #29596 <details> <summary>🔧 Fixes</summary> - [VoiceOver on Toolbar Item](#29573) - [SemanticProperties do not work on ToolbarItems](#23623) </details> <details> <summary>🧪 Testing (11)</summary> - [Testing] Additional Feature Matrix Test Cases for CollectionView by @TamilarasanSF4853 in #32432 - [Testing] Feature Matrix UITest Cases for VisualStateManager by @LogishaSelvarajSF4525 in #34146 - [Testing] Feature Matrix UITest Cases for Clip by @TamilarasanSF4853 in #34121 - [Testing] Feature matrix UITest Cases for Map Control by @HarishKumarSF4517 in #31656 - [Testing] Feature matrix UITest Cases for Visual Transform Control by @HarishKumarSF4517 in #32799 - [Testing] Feature Matrix UITest Cases for Shell Pages by @NafeelaNazhir in #33945 - [Testing] Feature Matrix UITest Cases for Triggers by @HarishKumarSF4517 in #34152 - [Testing] Refactoring Feature Matrix UITest Cases for CheckBox Control by @LogishaSelvarajSF4525 in #34283 - Resolve UI test Build Sample failures - Candidate March 16 by @Ahamed-Ali in #34442 - Fix the failures in the Candidate branch- March 16 by @Ahamed-Ali in #34453 <details> <summary>🔧 Fixes</summary> - [March 16th, Candidate](#34437) </details> - Fixed the iOS 18.5 Candidate failures (March 16,2026) by @Ahamed-Ali in #34593 <details> <summary>🔧 Fixes</summary> - [March 16th, Candidate](#34437) </details> </details> <details> <summary>📦 Other (2)</summary> - Fixed candidate test failures caused by PR #33428. by @Ahamed-Ali in #34515 <details> <summary>🔧 Fixes</summary> - [[.NET10] On Android, there's a big space at the top for I, M and N2 & N3](#34509) </details> - Revert "[iOS] Button RTL text and image overlap - fix (#29041)" in b0497af </details> <details> <summary>📝 Issue References</summary> Fixes #2574, Fixes #4993, Fixes #8486, Fixes #13258, Fixes #14160, Fixes #14364, Fixes #17799, Fixes #18011, Fixes #18668, Fixes #19676, Fixes #21044, Fixes #22938, Fixes #23014, Fixes #23623, Fixes #24450, Fixes #26187, Fixes #26726, Fixes #27377, Fixes #27799, Fixes #27800, Fixes #28656, Fixes #28784, Fixes #28968, Fixes #29141, Fixes #29394, Fixes #29535, Fixes #29573, Fixes #29921, Fixes #30085, Fixes #30347, Fixes #30363, Fixes #30837, Fixes #30862, Fixes #31166, Fixes #31239, Fixes #31259, Fixes #32016, Fixes #32200, Fixes #32312, Fixes #32650, Fixes #33114, Fixes #33201, Fixes #33229, Fixes #33316, Fixes #33344, Fixes #33351, Fixes #33400, Fixes #33407, Fixes #33479, Fixes #33660, Fixes #33722, Fixes #33829, Fixes #33925, Fixes #33966, Fixes #33967, Fixes #34083, Fixes #34143, Fixes #34190, Fixes #34247, Fixes #34273, Fixes #34278, Fixes #34437, Fixes #34509, Fixes #34512 </details> **Full Changelog**: main...inflight/candidate
dotnet#33892) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause When `MediaPicker.PickPhotosAsync` is called with `RotateImage=true` and `PreserveMetaData=true`, images were not being properly rotated: - **iOS**: The previous implementation used `UIImage.FromImage()` with `UIImageOrientation.Up` which only changed the orientation metadata flag, not the actual pixel data - **Android**: The previous implementation used `matrix.SetRotate(0)` which performed no actual rotation ### Description of Change **iOS (`ImageProcessor.ios.cs`):** - Now creates a graphics context with proper dimensions based on orientation - Applies appropriate CTM transformations (translate, rotate, scale) based on the image's `UIImageOrientation` - Actually draws the rotated image pixels, ensuring proper display regardless of how the consuming app reads the image **Android (`ImageProcessor.android.cs`):** - Now passes the actual EXIF orientation value to `ApplyExifOrientation()` - Uses `matrix.PostRotate()` with the correct angle based on EXIF orientation (1=0°, 3=180°, 6=90°, 8=270°) - Returns original bitmap when no rotation needed (orientation 1) ### Issues Fixed Fixes dotnet#32650 ### Platforms Affected - iOS ✅ - Android ✅ --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jakub Florkowski <kubaflo123@gmail.com> Co-authored-by: michalpobuta <mpobuta.consultant@ra.org>
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Root Cause
When
MediaPicker.PickPhotosAsyncis called withRotateImage=trueandPreserveMetaData=true, images were not being properly rotated:UIImage.FromImage()withUIImageOrientation.Upwhich only changed the orientation metadata flag, not the actual pixel datamatrix.SetRotate(0)which performed no actual rotationDescription of Change
iOS (
ImageProcessor.ios.cs):UIImageOrientationAndroid (
ImageProcessor.android.cs):ApplyExifOrientation()matrix.PostRotate()with the correct angle based on EXIF orientation (1=0°, 3=180°, 6=90°, 8=270°)Issues Fixed
Fixes #32650
Platforms Affected