Skip to content

[image_picker] Incorrect image rotation when selecting & resizing a portrait photo on iOS 14 with Limited Photos permission #74014

@MichaelMarner

Description

@MichaelMarner

Using the image_picker plugin, selecting and resizing a portrait-oriented photo on iOS 14 will cause incorrect rotation to be applied - but only if the app has "Selected Photos" (limited) permission.

Background

iOS 14 introduces a new, limited permission for accessing photos from the gallery. If chosen, the app can only access photos selected by user, and most importantly to this bug, attempting to get a PHAsset will fail (as a PHAsset may include more metadata than what iOS wants to give you).

Why is this error occurring

We can step through the typical image picking flow to understand what is happening:

  1. Flutter app requests an image from the gallery, and asks for it to be resized (by setting maxWidth or maxHeight
  2. The plugin checks or requests photos permissions. The plugin uses the older, simpler, deprecated permissions request, and therefore does not explicitly handle limited access. However, that option is presented to the user, and if they choose "Select Photos..." we will see the problem.
  3. An image is chosen by the user
  4. As we have set a max dimension, the plugin will scale the image. The implementation here is that the scale occurs with a hard-coded orientation, assuming that the exif rotation data will be added later. This is where the issue originates
  5. Once the image has been resized, the plugin will attempt to add all the original metadata (including orientation flag) to the newly created, scaled, image. It does this by accessing a PHAsset of the original image. PHAsset will be nil because of the limited photo permission
  6. Because the asset returned is nil, the image is saved without adding any of the original metadata. Without the original metadata, the scaling operation from Step 4 means that the image is now saved rotated incorrectly

Why is it only occuring if we are scaling?

The plugin uses the original image returned from the UIImagePicker if we aren't scaling. This image has the correct orientation flag, and so the image keeps the correct orientation.

Why is it only occuring if the app has limited access?

The Apple docs are fairly non-existent here, but access to PHAsset isn't allowed if the app only has limited photo access. However, with full access granted, the PHAsset is successfully used to copy the metadata (including orientation flag), and so the resized image has all the correct metadata.

What about other metadata?

Good pickup - The real title of this bug report should be "scaled image is missing all metadata if chosen from Gallery on iOS14 with limited photo permission". However, incorrect image rotation is the most visible symptom and is most likely to be the problem other users find.

Possible Solutions

There are a few different ways of handling this issue.

Ask for full photos permission on iOS 14

Edit: This doesn't seem possible on iOS 14

Using the new requesetAuthorizationForAccessLevel method, the plugin could ask for full permissions. This would make the existing resizing and metadata copying work, at the expense of Flutter apps asking for higher permissions than necessary (full access to all photos)

Manually attach the orientation flag to the scaled image's metadata

The image returned from the UIImagePickerController includes the orientation flag, without requiring access to the PHAsset. Therefore, the plugin could just attach the orientation flag as metadata, even if the PHAsset could not be retrieved.

Not hardcoding an orientation when scaling an image, then discarding the orientation flag from the original image.

Edit: this seems to be the Most Correct Solution

The scaling code hard-codes an orientation to prevent the scale from automatically rotating the image. Otherwise, when the orientation metadata is copied from the original image, the final result is incorrect. However, another approach would be to accept the automatic rotation and then disregard the orientation flag when copying the metadata.

Steps to Reproduce

This issue can be replicated using image_picker's example app.

  1. Have an iOS device with iOS 14+ installed
  2. Take a photo using the iOS Camera app in portrait orientation
  3. Run the image_picker example app
  4. Tap the Gallery button
  5. Enter a maxWidth or maxHeight, which will cause the plugin to resize. The dimension entered is not important
  6. Press Pick
  7. The access request will appear on screen. Tap Select Photos...
  8. Select the portrait-oriented image you took in step 1
  9. Press Done
  10. Select the portrait-oriented image again (Apple's UI for this is a bit clunky)
  11. The image you selected will be shown rotated 90 degrees

Expected results:

The photo should be displayed on screen in the correct orientation. This screenshot was generated using the steps above, but omitting the dimension, so no resize occurs.

Actual results:

This is affecting real users of our app today. As a workaround we are going to modify the plugin to ignore the original metadata and just bake the rotation into the scaled image. However, I'd be happy to help resolving this issue, depending on the preferred path.

Details
[✓] Flutter (Channel master, 1.26.0-2.0.pre.356, on Mac OS X 10.15.7 19H2 darwin-x64, locale en-AU)
    • Flutter version 1.26.0-2.0.pre.356 at /Users/michael/.bin/flutter
    • Framework revision 8d72307c47 (3 hours ago), 2021-01-14 16:59:04 -0800
    • Engine revision effb529ece
    • Dart version 2.12.0 (build 2.12.0-224.0.dev)

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
    • Android SDK at /Users/michael/Library/Android/sdk
    • Platform android-29, build-tools 29.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)
    • All Android licenses accepted.

[!] Xcode - develop for iOS and macOS
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.3, Build version 12C33
    ! CocoaPods 1.9.3 out of date (1.10.0 is recommended).
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To upgrade see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 3.6)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 45.1.1
    • Dart plugin version 192.7761
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)

[✓] VS Code (version 1.52.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.18.1

[✓] Connected device (3 available)
    • iPhone (mobile) • c3ee8096a2df059e475378199066cd0fec8b6a48 • ios            • iOS 14.3
    • macOS (desktop)           • macos                                    • darwin-x64     • Mac OS X 10.15.7 19H2 darwin-x64
    • Chrome (web)              • chrome                                   • web-javascript • Google Chrome 87.0.4280.141

! Doctor found issues in 1 category.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work liste: OS-version specificAffects only some versions of the relevant operating systemfound in release: 1.22Found to occur in 1.22has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: image_pickerThe Image Picker plugin.packageflutter/packages repository. See also p: labels.platform-iosiOS applications specificallyr: fixedIssue is closed as already fixed in a newer version

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions