c6d9e0b3c9
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>
152 lines
5.2 KiB
Swift
152 lines
5.2 KiB
Swift
//
|
|
// ImageProcessing.swift
|
|
// damus
|
|
//
|
|
// Created by KernelKind on 2/27/24.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
/// Removes GPS data from image at url and writes changes to new file
|
|
func processImage(url: URL) -> URL? {
|
|
let fileExtension = url.pathExtension
|
|
guard let imageData = try? Data(contentsOf: url) else {
|
|
print("Failed to load image data from URL.")
|
|
return nil
|
|
}
|
|
|
|
guard let source = CGImageSourceCreateWithData(imageData as CFData, nil) else { return nil }
|
|
|
|
return processImage(source: source, fileExtension: fileExtension)
|
|
}
|
|
|
|
/// Removes GPS data from image and writes changes to new file
|
|
func processImage(image: UIImage) -> URL? {
|
|
let fixedImage = image.fixOrientation()
|
|
guard let imageData = fixedImage.jpegData(compressionQuality: 1.0) else { return nil }
|
|
guard let source = CGImageSourceCreateWithData(imageData as CFData, nil) else { return nil }
|
|
|
|
return processImage(source: source, fileExtension: "jpeg")
|
|
}
|
|
|
|
fileprivate func processImage(source: CGImageSource, fileExtension: String) -> URL? {
|
|
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: fileExtension)
|
|
|
|
guard let destination = removeGPSDataFromImage(source: source, url: destinationURL) else { return nil }
|
|
|
|
if !CGImageDestinationFinalize(destination) { return nil }
|
|
|
|
return destinationURL
|
|
}
|
|
|
|
/// TODO: strip GPS data from video
|
|
func processVideo(videoURL: URL) -> URL? {
|
|
saveVideoToTemporaryFolder(videoURL: videoURL)
|
|
}
|
|
|
|
fileprivate func saveVideoToTemporaryFolder(videoURL: URL) -> URL? {
|
|
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: videoURL.pathExtension)
|
|
|
|
do {
|
|
try FileManager.default.copyItem(at: videoURL, to: destinationURL)
|
|
return destinationURL
|
|
} catch {
|
|
print("Error copying file: \(error.localizedDescription)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Generate a temporary URL with a unique filename
|
|
func generateUniqueTemporaryMediaURL(fileExtension: String) -> URL {
|
|
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
let uniqueMediaName = "\(UUID().uuidString).\(fileExtension)"
|
|
let temporaryMediaURL = temporaryDirectoryURL.appendingPathComponent(uniqueMediaName)
|
|
|
|
return temporaryMediaURL
|
|
}
|
|
|
|
/**
|
|
Take the PreUploadedMedia payload, process it, if necessary, and convert it into a URL
|
|
which is ready to be uploaded to the upload service.
|
|
|
|
URLs containing media that hasn't been processed were generated from the system and were granted
|
|
access as a security scoped resource. The data will need to be processed to strip GPS data
|
|
and saved to a new location which isn't security scoped.
|
|
*/
|
|
func generateMediaUpload(_ media: PreUploadedMedia?) -> MediaUpload? {
|
|
guard let media else { return nil }
|
|
|
|
switch media {
|
|
case .uiimage(let image):
|
|
guard let url = processImage(image: image) else { return nil }
|
|
return .image(url)
|
|
case .unprocessed_image(let url):
|
|
guard let newUrl = processImage(url: url) else { return nil }
|
|
url.stopAccessingSecurityScopedResource()
|
|
return .image(newUrl)
|
|
case .processed_image(let url):
|
|
return .image(url)
|
|
case .processed_video(let url):
|
|
return .video(url)
|
|
case .unprocessed_video(let url):
|
|
guard let newUrl = processVideo(videoURL: url) else { return nil }
|
|
url.stopAccessingSecurityScopedResource()
|
|
return .video(newUrl)
|
|
}
|
|
}
|
|
|
|
extension UIImage {
|
|
func fixOrientation() -> UIImage {
|
|
guard imageOrientation != .up else { return self }
|
|
|
|
UIGraphicsBeginImageContextWithOptions(size, false, scale)
|
|
draw(in: CGRect(origin: .zero, size: size))
|
|
let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
UIGraphicsEndImageContext()
|
|
|
|
return normalizedImage ?? self
|
|
}
|
|
}
|
|
|
|
func canGetSourceTypeFromUrl(url: URL) -> Bool {
|
|
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
|
|
print("Failed to create image source.")
|
|
return false
|
|
}
|
|
return CGImageSourceGetType(source) != nil
|
|
}
|
|
|
|
func removeGPSDataFromImageAndWrite(fromImageURL imageURL: URL) -> Bool {
|
|
guard let source = CGImageSourceCreateWithURL(imageURL as CFURL, nil) else {
|
|
print("Failed to create image source.")
|
|
return false
|
|
}
|
|
|
|
guard let destination = removeGPSDataFromImage(source: source, url: imageURL) else { return false }
|
|
|
|
return CGImageDestinationFinalize(destination)
|
|
}
|
|
|
|
fileprivate func removeGPSDataFromImage(source: CGImageSource, url: URL) -> CGImageDestination? {
|
|
let totalCount = CGImageSourceGetCount(source)
|
|
|
|
guard totalCount > 0 else {
|
|
print("No images found.")
|
|
return nil
|
|
}
|
|
|
|
guard let type = CGImageSourceGetType(source),
|
|
let destination = CGImageDestinationCreateWithURL(url as CFURL, type, totalCount, nil) else {
|
|
print("Failed to create image destination.")
|
|
return nil
|
|
}
|
|
|
|
let removeGPSProperties: CFDictionary = [kCGImageMetadataShouldExcludeGPS: kCFBooleanTrue] as CFDictionary
|
|
|
|
for i in 0..<totalCount {
|
|
CGImageDestinationAddImageFromSource(destination, source, i, removeGPSProperties)
|
|
}
|
|
|
|
return destination
|
|
}
|