Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[image-picker][ios] Fix Animated GIF support #20034

Merged
merged 8 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/expo-image-picker/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### 🐛 Bug fixes

- Fix support for animated GIFs on iOS. ([#20034](https://github.com/expo/expo/pull/20034) by [@barthap](https://github.com/barthap))

### 💡 Others

## 14.0.1 - 2022-11-08
Expand Down
8 changes: 5 additions & 3 deletions packages/expo-image-picker/build/ImagePicker.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo-image-picker/build/ImagePicker.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions packages/expo-image-picker/build/ImagePicker.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo-image-picker/build/ImagePicker.js.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/expo-image-picker/ios/ImagePickerOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import MobileCoreServices
import PhotosUI

internal let DEFAULT_QUALITY = 0.2
internal let MAXIMUM_QUALITY = 1.0

internal let UNLIMITED_SELECTION = 0
internal let SINGLE_SELECTION = 1

Expand Down
67 changes: 45 additions & 22 deletions packages/expo-image-picker/ios/MediaHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ internal struct MediaHandler {
}

let (imageData, fileExtension) = try ImageUtils.readDataAndFileExtension(image: image,
rawData: rawData,
itemProvider: itemProvider,
options: self.options)

Expand Down Expand Up @@ -345,11 +346,17 @@ private struct ImageUtils {
return (nil, ".bmp")

case .some(let s) where s.contains("ext=GIF"):
var rawData: Data?
if let imgUrl = mediaInfo[.imageURL] as? URL {
rawData = try? Data(contentsOf: imgUrl)
}
let inputData = rawData ?? image.jpegData(compressionQuality: compressionQuality)
let metadata = mediaInfo[.mediaMetadata] as? [String: Any]
let gifData = try getGifDataFrom(image: image,
let cropRect = mediaInfo[.cropRect] as? CGRect
let gifData = try processGifData(inputData: inputData,
compressionQuality: options.quality,
initialMetadata: metadata)

initialMetadata: metadata,
cropRect: cropRect)
return (gifData, ".gif")
default:
let data = image.jpegData(compressionQuality: compressionQuality)
Expand All @@ -360,6 +367,7 @@ private struct ImageUtils {
@available(iOS 14, *)
static func readDataAndFileExtension(
image: UIImage,
rawData: Data,
itemProvider: NSItemProvider,
options: ImagePickerOptions
) throws -> (imageData: Data?, fileExtension: String) {
Expand All @@ -371,7 +379,7 @@ private struct ImageUtils {
let data = image.pngData()
return (data, ".png")
case UTType.gif.identifier:
let gifData = try getGifDataFrom(image: image,
let gifData = try processGifData(inputData: rawData,
compressionQuality: options.quality,
initialMetadata: nil)
return (gifData, ".gif")
Expand Down Expand Up @@ -508,37 +516,52 @@ private struct ImageUtils {
return exif
}

static func getGifDataFrom(image: UIImage,
static func processGifData(inputData: Data?,
compressionQuality quality: Double?,
initialMetadata: [String: Any]?) throws -> Data? {
guard let data = image.jpegData(compressionQuality: quality ?? DEFAULT_QUALITY) else {
throw FailedToReadImageDataException()
initialMetadata: [String: Any]?,
cropRect: CGRect? = nil) throws -> Data? {
// for uncropped, maximum quality image we can just pass through the raw data
if cropRect == nil,
quality == nil || quality == MAXIMUM_QUALITY {
barthap marked this conversation as resolved.
Show resolved Hide resolved
return inputData
}

let destinationData = NSMutableData()
guard let imageDestination = CGImageDestinationCreateWithData(destinationData, kUTTypeGIF, 1, nil),
let cgImage = image.cgImage
guard let sourceData = inputData,
let imageSource = CGImageSourceCreateWithData(sourceData as CFData, nil)
else {
throw FailedToCreateGifException()
throw FailedToReadImageException()
}

var metadata: [String: Any] = initialMetadata ?? [:]
if initialMetadata == nil,
let cgImageSource = CGImageSourceCreateWithData(data as CFData, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(cgImageSource, 0, nil) as? [String: Any] {
metadata = properties
let gifProperties = CGImageSourceCopyProperties(imageSource, nil) as? [String: Any]
let frameCount = CGImageSourceGetCount(imageSource)

let destinationData = NSMutableData()
guard let imageDestination = CGImageDestinationCreateWithData(destinationData, kUTTypeGIF, frameCount, nil)
else {
throw FailedToCreateGifException()
}

if quality != nil {
metadata[kCGImageDestinationLossyCompressionQuality as String] = quality
let gifMetadata = initialMetadata ?? gifProperties
CGImageDestinationSetProperties(imageDestination, gifMetadata as CFDictionary?);

for frameIndex in 0 ..< frameCount {
guard var cgImage = CGImageSourceCreateImageAtIndex(imageSource, frameIndex, nil),
var frameProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, frameIndex, nil) as? [String: Any]
else {
throw FailedToCreateGifException()
}
if cropRect != nil {
cgImage = cgImage.cropping(to: cropRect!)!
}
if quality != nil {
frameProperties[kCGImageDestinationLossyCompressionQuality as String] = quality
}
CGImageDestinationAddImage(imageDestination, cgImage, frameProperties as CFDictionary)
}

CGImageDestinationAddImage(imageDestination, cgImage, metadata as CFDictionary)

if !CGImageDestinationFinalize(imageDestination) {
throw FailedToExportGifException()
}

return destinationData as Data
}
}
Expand Down
8 changes: 5 additions & 3 deletions packages/expo-image-picker/src/ImagePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,12 @@ export async function launchCameraAsync(
* Requires `Permissions.MEDIA_LIBRARY` on iOS 10 only. On mobile web, this must be called
* immediately in a user interaction like a button press, otherwise the browser will block the
* request without a warning.
* **Animated GIFs support** If the selected image is an animated GIF, the result image will be an
* animated GIF too if and only if `quality` is set to `undefined` and `allowsEditing` is set to `false`.
*
* **Animated GIFs support:** On Android, if the selected image is an animated GIF, the result image will be an
* animated GIF too if and only if `quality` is explicitly set to `1.0` and `allowsEditing` is set to `false`.
* Otherwise compression and/or cropper will pick the first frame of the GIF and return it as the
* result (on Android the result will be a PNG, on iOS — GIF).
* result (on Android the result will be a PNG). On iOS, both quality and cropping are supported.
*
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
* Therefore, calling `launchImageLibraryAsync` in `componentDidMount`, for example, will **not**
* work as intended. The `cancelled` event will not be returned in the browser due to platform
Expand Down