Files
damus/damus/Views/MediaPicker.swift
T
Daniel D’Aquino c6d9e0b3c9 Fix GIF uploads
This commit fixes GIF uploads and improves GIF support:
- MediaPicker will now skip location data removal processing, as it is not needed on GIF images and causes them to be converted to JPEG images
- The uploader now sets more accurate MIME types on the upload request

Issue Repro
-----------

Device: iPhone 13 Mini
iOS: 17.4.1
Damus: `ada99418f6fcdb1354bc5c1c3f3cc3b4db994ce6`
Steps:
1. Download a GIF from GIPHY to the iOS photo gallery
2. Upload that and attach into a post in Damus
3. Check if GIF is animated.
Results: GIF is not animated. Issue is reproduced.

Testing
-------

PASS

Device: iPhone 13 Mini
iOS: 17.4.1
Damus: this commit
Steps:
1. Create a new post
2. Upload the same GIF as the repro and post
3. Make sure GIF is animated. PASS
4. Create a new post
5. Upload a new GIF image (that has never been uploaded by the user on the app) and post
6. Make sure the GIF is animated on the post. PASS
7. Make sure that JPEGs can still be successfully uploaded. PASS
8. Make sure that MP4s can be uploaded.
9. Make a new post that contains 1 JPEG, 1 MP4 file, and 2 GIF files. Make sure they are all uploaded correctly and all GIF files are animated. PASS

Closes: https://github.com/damus-io/damus/issues/2157
Changelog-Fixed: Fix broken GIF uploads
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-01 10:27:05 -07:00

129 lines
5.9 KiB
Swift

//
// ImagePicker.swift
// damus
//
// Created by Swift on 3/31/23.
//
import UIKit
import SwiftUI
import PhotosUI
struct MediaPicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
@Binding private var presentationMode
@Binding var image_upload_confirm: Bool
var imagesOnly: Bool = false
let onMediaPicked: (PreUploadedMedia) -> Void
final class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: MediaPicker
init(_ parent: MediaPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
if results.isEmpty {
self.parent.presentationMode.dismiss()
}
for result in results {
if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
result.itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (item, error) in
guard let url = item as? URL else { return }
if(url.pathExtension == "gif") {
// GIFs do not natively support location metadata (See https://superuser.com/a/556320 and https://www.w3.org/Graphics/GIF/spec-gif89a.txt)
// It is better to avoid any GPS data processing at all, as it can cause the image to be converted to JPEG.
// Therefore, we should load the file directtly and deliver it as "already processed".
// Load the data for the GIF image
// - Don't load it as an UIImage since that can only get exported into JPEG/PNG
// - Don't load it as a file representation because it gets deleted before the upload can occur
_ = result.itemProvider.loadDataRepresentation(for: .gif, completionHandler: { imageData, error in
guard let imageData else { return }
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: "gif")
do {
try imageData.write(to: destinationURL)
Task {
await self.chooseMedia(.processed_image(destinationURL))
}
}
catch {
Log.error("Failed to write GIF image data from Photo picker into a local copy", for: .image_uploading)
}
})
}
else if canGetSourceTypeFromUrl(url: url) {
// Media was not taken from camera
self.attemptAcquireResourceAndChooseMedia(
url: url,
fallback: processImage,
unprocessedEnum: {.unprocessed_image($0)},
processedEnum: {.processed_image($0)}
)
} else {
// Media was taken from camera
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
if let image = image as? UIImage, error == nil {
self.chooseMedia(.uiimage(image))
}
}
}
}
} else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { (url, error) in
guard let url, error == nil else { return }
self.attemptAcquireResourceAndChooseMedia(
url: url,
fallback: processVideo,
unprocessedEnum: {.unprocessed_video($0)},
processedEnum: {.processed_video($0)}
)
}
}
}
}
private func chooseMedia(_ media: PreUploadedMedia) {
self.parent.onMediaPicked(media)
self.parent.image_upload_confirm = true
}
private func attemptAcquireResourceAndChooseMedia(url: URL, fallback: (URL) -> URL?, unprocessedEnum: (URL) -> PreUploadedMedia, processedEnum: (URL) -> PreUploadedMedia) {
if url.startAccessingSecurityScopedResource() {
// Have permission from system to use url out of scope
print("Acquired permission to security scoped resource")
self.chooseMedia(unprocessedEnum(url))
} else {
// Need to copy URL to non-security scoped location
guard let newUrl = fallback(url) else { return }
self.chooseMedia(processedEnum(newUrl))
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.selectionLimit = 1
configuration.filter = imagesOnly ? .images : .any(of: [.images, .videos])
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator as any PHPickerViewControllerDelegate
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
}