Skip to content

[iOS/Android] MediaPicker: Fix image orientation when RotateImage=true#33892

Merged
kubaflo merged 11 commits intodotnet:inflight/currentfrom
michalpobuta:Issue32650
Mar 5, 2026
Merged

[iOS/Android] MediaPicker: Fix image orientation when RotateImage=true#33892
kubaflo merged 11 commits intodotnet:inflight/currentfrom
michalpobuta:Issue32650

Conversation

@michalpobuta
Copy link
Copy Markdown
Contributor

@michalpobuta michalpobuta commented Feb 4, 2026

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.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 ✅

@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Feb 4, 2026
@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Feb 4, 2026

✨ +100 points to Gryffindor for using the agent! 🦁

@michalpobuta
Copy link
Copy Markdown
Contributor Author

@kubaflo this PR agent is great! I will use it for every issue now

@dotnet dotnet deleted a comment from rmarinho Feb 4, 2026
kubaflo

This comment was marked as duplicate.

kubaflo

This comment was marked as duplicate.

@kubaflo kubaflo changed the title Fix 32650 Image Orientation [iOS/Android] MediaPicker: Fix image orientation when RotateImage=true Feb 4, 2026
@rmarinho
Copy link
Copy Markdown
Member

rmarinho commented Feb 14, 2026

🤖 AI Summary

📊 Expand Full Review
🔍 Pre-Flight — Context & Validation
📝 Review SessionRemoved tests · b86b2a4

Issue: #32650 - MediaPicker.PickPhotosAsync does not preserve image orientation
Platforms Affected: iOS ✅, Android ✅ (issue reported for iOS; community also confirmed Android)
Files Changed: 2 implementation files (tests removed in latest commit b86b2a4)

Summary

When MediaPicker.PickPhotosAsync is called with RotateImage=true and PreserveMetaData=true, images were not being properly rotated to their correct orientation. The PR fixes the actual pixel rotation (not just metadata) on both iOS and Android.

Root Cause (from PR description)

iOS (ImageProcessor.ios.cs):

  • Previous implementation used UIImage.FromImage() with UIImageOrientation.Up which only changed the orientation metadata flag, not the actual pixel data
  • Now creates a graphics context with proper dimensions and applies CTM transformations to actually rotate pixels

Android (ImageProcessor.android.cs):

  • Previous implementation used matrix.SetRotate(0) which performed no actual rotation
  • Now passes actual EXIF orientation value and uses matrix.PostRotate() with correct angle based on EXIF orientation

Prior Agent Review Status

A prior agent review was completed with the following outcome:

  • Pre-Flight: ✅ COMPLETE
  • Gate: ❌ FAILED (tests attempted to automate system UI which cannot be reliably automated)
  • Fix: ⏭️ SKIPPED (cannot proceed without functional tests)
  • Report: ✅ COMPLETE (requested changes)

Changes Requested by Prior Agent:

  1. iOS graphics context null check - UIGraphics.GetCurrentContext() can return null (HIGH SEVERITY)
  2. Android EXIF orientation 4 logic error - Incorrectly flips horizontal instead of vertical (HIGH SEVERITY)
  3. Android indentation inconsistency - Code formatting (MEDIUM SEVERITY)

PR Author Response:

  • Latest commit (b86b2a4) "Removed tests" - PR author removed the UI tests that were blocking Gate
  • The HIGH SEVERITY code issues have NOT been addressed

Key Issue Discussion Points

From Issue #32650:

  • Reporter (Dreamescaper): Bug reproduces with sample images on iOS emulator, provided test image IMG_0089.HEIC
  • Community (REDECODE): Also affects Android 10 with MAUI 10.0.11, both PickPhotosAsync and CapturePhotoAsync
  • Critical observation: Android code uses matrix.SetRotate(0) which just removes EXIF data without rotating pixels
  • Verification (HarishKumarSF4517): Validated on MAUI 10.0.10, 10.0-rc2, and 10.0.0-preview7

From Copilot Reviewer:

  • iOS context can be null (HIGH SEVERITY)
  • Android EXIF orientation 4 is mapped incorrectly (flipH when should be flipV)
  • Android matrix transforms applied at origin, can crop images for 90°/270° rotations
  • iOS image.Draw() double-applies orientation (should use context.DrawImage(image.CGImage, ...))
  • iOS rotatedImage and NSData not disposed
  • Tests cannot automate system photo picker UI

From kubaflo:

  • CHANGES_REQUESTED: "The UI Test is not working :/"

Current State of Code (After Latest Commit)

iOS (ImageProcessor.ios.cs) - Outstanding Issues:

  1. context from UIGraphics.GetCurrentContext() is used without null check
  2. image.Draw(CGPoint.Empty) may double-apply orientation; should use context.DrawImage(image.CGImage, ...)
  3. rotatedImage and imageData (NSData) not disposed with using

Android (ImageProcessor.android.cs) - Outstanding Issues:

  1. EXIF orientation 4 = flip vertical, but code sets flipH = true (horizontal flip) for orientation 4 - INCORRECT
  2. Matrix transforms applied at origin, not center; can shift/crop 90°/270° rotated images
  3. Indentation inconsistency in the outer try block

PR Discussion Points

File:Line Reviewer Says Author Says Status
ios.cs:60 Add null check for context Not addressed ⚠️ OPEN
ios.cs:93 image.Draw() double-applies orientation Not addressed ⚠️ OPEN
ios.cs:118 Dispose rotatedImage/imageData Not addressed ⚠️ OPEN
android.cs:199 Orientation 4 logic incorrect; need center transforms Not addressed ⚠️ OPEN
android.cs:65 Indentation inconsistency Not addressed ⚠️ OPEN

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 SessionRemoved 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 SessionRemoved 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 ⚠️ BLOCKED (no tests) 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 ⚠️ BLOCKED (no tests) 1 file Correct logic, compiles OK
3 try-fix (gpt-5.2) Android: Canvas-based transforms; iOS: CGImageSource decode-time transform ⚠️ BLOCKED (no tests) 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 ⚠️ BLOCKED (no tests) 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 ⚠️ BLOCKED (no tests) 2 files Builds OK
PR PR #33892 iOS: Graphics context + CTM transforms; Android: EXIF-based matrix rotation ⚠️ PENDING 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:

  1. Android: Fix orientation 4 to vertical flip + apply translation after rotation to avoid cropping (Attempt 4's approach) + fix indentation
  2. iOS: Add null check for context + use using for 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 SessionRemoved 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 here

EXIF 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 center

Fix: 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 risk

Fix:

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 metadata

UIImage.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

  1. Approach is correct: Rotating pixel data is the right fix
  2. iOS try/finally: EndImageContext() always called via try/finally
  3. iOS rotated size: Correctly swaps W/H for Left/Right/LeftMirrored/RightMirrored
  4. iOS all orientations: All 7 non-Up cases are handled
  5. Android EXIF reading: Using ExifInterface correctly to get orientation value
  6. Android orientation early exit: Returns early for orientation 1 (normal)
  7. 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.cs
  • src/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 null

Recommendation: 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 center

Recommendation: 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 disposed

Recommendation:

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.csGetExifOrientation()
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
  • ExifInterface supports reading from a stream or byte array on Android API 24+ via ExifInterface(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.Up early exit: Before entering the expensive graphics context creation, the code correctly short-circuits for Up orientation. ✅
  • Android correctly short-circuits orientation 1: The method returns the original bitmap when orientation == 1 is detected before ApplyExifOrientation is called. ✅
  • finally block for UIGraphics.EndImageContext(): Proper cleanup of the UIKit graphics context is ensured. ✅
  • Android uses using on the Matrix: The Matrix object 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.FromStream with reused MemoryStream (outdated - test files appear removed)
  • Issue32650Tests.cs naming convention (outdated)
  • UITestCategories.cs - MediaPicker category not in pipeline YAML (outdated)
  • Tests not validating orientation correctness (outdated)
  • Tests calling App.Tap opening system picker (outdated)

Since these are marked outdated, they may no longer apply to the current state of the PR.


@rmarinho rmarinho added s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) labels Feb 14, 2026
kubaflo

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.MediaPicker category.

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.

kubaflo

This comment was marked as resolved.

@kubaflo kubaflo added s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation and removed s/agent-fix-win AI found a better alternative fix than the PR labels Feb 19, 2026
Copy link
Copy Markdown
Member

@jfversluis jfversluis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@kubaflo kubaflo removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-failed AI could not verify tests catch the bug labels Feb 25, 2026
Copy link
Copy Markdown
Member

@jfversluis jfversluis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.csproj removes a stale import (file doesn't exist)
  • Essentials.UnitTests.csproj adds DebugType=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.

michalpobuta and others added 10 commits March 5, 2026 09:55
…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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 5, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33892

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33892"

@michalpobuta
Copy link
Copy Markdown
Contributor Author

@jfversluis done, also PR was updated to newest main

@michalpobuta michalpobuta requested a review from kubaflo March 5, 2026 09:18
Copy link
Copy Markdown
Member

@jfversluis jfversluis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. EXIF Orientation tag re-applied after pixel rotation — When both rotateImage and preserveMetaData are true, ExtractMetadataAsync preserves the original Orientation tag, then ApplyMetadataAsync writes it back to the already-rotated image. On Android this could cause double-rotation by viewers. On iOS this is masked by issue 2.

  2. iOS metadata silently lost when rotation is performedRotateImageAsync consumes inputStream via NSData.FromStream(). Then ExtractMetadataAsync(inputStream) is called on the same consumed stream without resetting position → reads 0 bytes → metadata silently discarded. (Android's ExtractMetadataAsync does 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! 👍

@kubaflo kubaflo changed the base branch from main to inflight/current March 5, 2026 17:25
@kubaflo kubaflo merged commit 6109d0c into dotnet:inflight/current Mar 5, 2026
29 checks passed
PureWeen added a commit that referenced this pull request Mar 11, 2026
#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>
github-actions bot added a commit that referenced this pull request Mar 11, 2026
#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>
@PureWeen PureWeen mentioned this pull request Mar 17, 2026
PureWeen added a commit that referenced this pull request Mar 19, 2026
#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>
github-actions bot added a commit that referenced this pull request Mar 20, 2026
#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>
github-actions bot added a commit that referenced this pull request Mar 22, 2026
#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>
@kubaflo kubaflo added the s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) label Mar 23, 2026
PureWeen added a commit that referenced this pull request Mar 24, 2026
## 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
KarthikRajaKalaimani pushed a commit to KarthikRajaKalaimani/maui that referenced this pull request Mar 30, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MediaPicker.PickPhotosAsync does not preserve image orientation

6 participants