ImageProcessing.swift (5288B)
1 // 2 // ImageProcessing.swift 3 // damus 4 // 5 // Created by KernelKind on 2/27/24. 6 // 7 8 import UIKit 9 10 /// Removes GPS data from image at url and writes changes to new file 11 func processImage(url: URL) -> URL? { 12 let fileExtension = url.pathExtension 13 guard let imageData = try? Data(contentsOf: url) else { 14 print("Failed to load image data from URL.") 15 return nil 16 } 17 18 guard let source = CGImageSourceCreateWithData(imageData as CFData, nil) else { return nil } 19 20 return processImage(source: source, fileExtension: fileExtension) 21 } 22 23 /// Removes GPS data from image and writes changes to new file 24 func processImage(image: UIImage) -> URL? { 25 let fixedImage = image.fixOrientation() 26 guard let imageData = fixedImage.jpegData(compressionQuality: 1.0) else { return nil } 27 guard let source = CGImageSourceCreateWithData(imageData as CFData, nil) else { return nil } 28 29 return processImage(source: source, fileExtension: "jpeg") 30 } 31 32 fileprivate func processImage(source: CGImageSource, fileExtension: String) -> URL? { 33 let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: fileExtension) 34 35 guard let destination = removeGPSDataFromImage(source: source, url: destinationURL) else { return nil } 36 37 if !CGImageDestinationFinalize(destination) { return nil } 38 39 return destinationURL 40 } 41 42 /// TODO: strip GPS data from video 43 func processVideo(videoURL: URL) -> URL? { 44 saveVideoToTemporaryFolder(videoURL: videoURL) 45 } 46 47 fileprivate func saveVideoToTemporaryFolder(videoURL: URL) -> URL? { 48 let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: videoURL.pathExtension) 49 50 do { 51 try FileManager.default.copyItem(at: videoURL, to: destinationURL) 52 return destinationURL 53 } catch { 54 print("Error copying file: \(error.localizedDescription)") 55 return nil 56 } 57 } 58 59 /// Generate a temporary URL with a unique filename 60 func generateUniqueTemporaryMediaURL(fileExtension: String) -> URL { 61 let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 62 let uniqueMediaName = "\(UUID().uuidString).\(fileExtension)" 63 let temporaryMediaURL = temporaryDirectoryURL.appendingPathComponent(uniqueMediaName) 64 65 return temporaryMediaURL 66 } 67 68 /** 69 Take the PreUploadedMedia payload, process it, if necessary, and convert it into a URL 70 which is ready to be uploaded to the upload service. 71 72 URLs containing media that hasn't been processed were generated from the system and were granted 73 access as a security scoped resource. The data will need to be processed to strip GPS data 74 and saved to a new location which isn't security scoped. 75 */ 76 func generateMediaUpload(_ media: PreUploadedMedia?) -> MediaUpload? { 77 guard let media else { return nil } 78 79 switch media { 80 case .uiimage(let image): 81 guard let url = processImage(image: image) else { return nil } 82 return .image(url) 83 case .unprocessed_image(let url): 84 guard let newUrl = processImage(url: url) else { return nil } 85 url.stopAccessingSecurityScopedResource() 86 return .image(newUrl) 87 case .processed_image(let url): 88 return .image(url) 89 case .processed_video(let url): 90 return .video(url) 91 case .unprocessed_video(let url): 92 guard let newUrl = processVideo(videoURL: url) else { return nil } 93 url.stopAccessingSecurityScopedResource() 94 return .video(newUrl) 95 } 96 } 97 98 extension UIImage { 99 func fixOrientation() -> UIImage { 100 guard imageOrientation != .up else { return self } 101 102 UIGraphicsBeginImageContextWithOptions(size, false, scale) 103 draw(in: CGRect(origin: .zero, size: size)) 104 let normalizedImage = UIGraphicsGetImageFromCurrentImageContext() 105 UIGraphicsEndImageContext() 106 107 return normalizedImage ?? self 108 } 109 } 110 111 func canGetSourceTypeFromUrl(url: URL) -> Bool { 112 guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { 113 print("Failed to create image source.") 114 return false 115 } 116 return CGImageSourceGetType(source) != nil 117 } 118 119 func removeGPSDataFromImageAndWrite(fromImageURL imageURL: URL) -> Bool { 120 guard let source = CGImageSourceCreateWithURL(imageURL as CFURL, nil) else { 121 print("Failed to create image source.") 122 return false 123 } 124 125 guard let destination = removeGPSDataFromImage(source: source, url: imageURL) else { return false } 126 127 return CGImageDestinationFinalize(destination) 128 } 129 130 fileprivate func removeGPSDataFromImage(source: CGImageSource, url: URL) -> CGImageDestination? { 131 let totalCount = CGImageSourceGetCount(source) 132 133 guard totalCount > 0 else { 134 print("No images found.") 135 return nil 136 } 137 138 guard let type = CGImageSourceGetType(source), 139 let destination = CGImageDestinationCreateWithURL(url as CFURL, type, totalCount, nil) else { 140 print("Failed to create image destination.") 141 return nil 142 } 143 144 let removeGPSProperties: CFDictionary = [kCGImageMetadataShouldExcludeGPS: kCFBooleanTrue] as CFDictionary 145 146 for i in 0..<totalCount { 147 CGImageDestinationAddImageFromSource(destination, source, i, removeGPSProperties) 148 } 149 150 return destination 151 }