ImageURLSenderItemCreator.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. import Foundation
  21. import CoreServices
  22. import PromiseKit
  23. import CocoaLumberjackSwift
  24. @objc public class ImageURLSenderItemCreator : NSObject {
  25. static let kImageSizeSmall : CGFloat = 640
  26. static let kImageSizeMedium: CGFloat = 1024
  27. static let kImageSizeLarge : CGFloat = 1600
  28. static let kImageSizeXLarge : CGFloat = 2592
  29. static let kImageSizeOriginal: CGFloat = 0
  30. private var userSettingsImageSize : String
  31. /// Initialisation with the image scale settings chosen by the user
  32. public override init() {
  33. guard let settings = UserSettings.shared() else {
  34. self.userSettingsImageSize = "medium"
  35. return
  36. }
  37. self.userSettingsImageSize = settings.imageSize
  38. }
  39. /// Initialisation with a given scale setting. Should only be used for testing.
  40. /// - Parameter userSettingsImageSize: Can be set to any of the image sizes returned by imageSizes()
  41. @objc init(with overrideUserSettingsSizeSize : String, forceSize : Bool) {
  42. if UserSettings.shared()?.imageSize != nil && !forceSize {
  43. let settingsImageSize = ImageURLSenderItemCreator.getImageSizeForString(size: UserSettings.shared()!.imageSize)
  44. let inputImageSize = ImageURLSenderItemCreator.getImageSizeForString(size: overrideUserSettingsSizeSize)
  45. self.userSettingsImageSize = (settingsImageSize != 0 && settingsImageSize <= inputImageSize) ? UserSettings.shared()!.imageSize : overrideUserSettingsSizeSize
  46. } else {
  47. self.userSettingsImageSize = overrideUserSettingsSizeSize
  48. }
  49. super.init()
  50. }
  51. /// Create an URLSenderItem from an image represented as Data.
  52. /// GIFs and PNGs can be rendered as Sticker type.
  53. /// The image data will not be converted
  54. /// - Parameters:
  55. /// - image: Must be jpg, gif or png image
  56. /// - uti: The UTI of the image in image. The UTI must be validated before passing it into this function
  57. /// - Returns: An URLSenderItem representing the image
  58. @objc public func senderItem(from image : Data, uti : String) -> URLSenderItem? {
  59. guard let img = UIImage(data: image) else {
  60. return nil
  61. }
  62. let maxSize: CGFloat = imageMaxSize(nil)
  63. var imageData : Data?
  64. var renderType : NSNumber = 1
  65. var finalUti : String = uti
  66. if UTIConverter.isGifMimeType(UTIConverter.mimeType(fromUTI: uti)) {
  67. renderType = 2
  68. } else if ImageURLSenderItemCreator.isPNGSticker(image: img, uti: uti) {
  69. renderType = 2
  70. guard let convData = MediaConverter.scaleImageData(to: image, toMaxSize: maxSize, useJPEG: false) else {
  71. return nil
  72. }
  73. imageData = convData
  74. } else {
  75. guard let convJpgData = MediaConverter.scaleImageData(to: image, toMaxSize: maxSize, useJPEG: true) else {
  76. return nil
  77. }
  78. imageData = convJpgData
  79. finalUti = kUTTypeJPEG as String
  80. }
  81. let mimeType = UTIConverter.mimeType(fromUTI: finalUti)
  82. let filename = FileUtility.getTemporarySendableFileName(base: "image") + "." + UTIConverter.preferedFileExtension(forMimeType: mimeType)
  83. return URLSenderItem(data: imageData ?? image,
  84. fileName: filename,
  85. type: finalUti,
  86. renderType: renderType,
  87. sendAsFile: true)
  88. }
  89. @objc public func senderItem(url : URL, uti : String) -> URLSenderItem? {
  90. let maxSize: CGFloat = imageMaxSize()
  91. var imageData : Data?
  92. var renderType : NSNumber = 1
  93. var finalUti : String = uti
  94. if UTIConverter.isGifMimeType(UTIConverter.mimeType(fromUTI: uti)) {
  95. renderType = 2
  96. do {
  97. imageData = try Data(contentsOf: url)
  98. } catch {
  99. DDLogError(error.localizedDescription)
  100. return nil
  101. }
  102. } else {
  103. guard let scaledImage = MediaConverter.scaleImageUrl(url, toMaxSize: maxSize) else {
  104. return nil
  105. }
  106. if ImageURLSenderItemCreator.isPNGSticker(image: scaledImage, uti: uti) {
  107. renderType = 2
  108. imageData = scaledImage.pngData()
  109. } else {
  110. guard let convJpgData = scaledImage.jpegData(compressionQuality: CGFloat(kJPEGCompressionQuality)) else {
  111. return nil
  112. }
  113. imageData = convJpgData
  114. finalUti = kUTTypeJPEG as String
  115. }
  116. }
  117. let mimeType = UTIConverter.mimeType(fromUTI: finalUti)
  118. let filename = FileUtility.getTemporarySendableFileName(base: "image") + "." + UTIConverter.preferedFileExtension(forMimeType: mimeType)
  119. return URLSenderItem(data: imageData,
  120. fileName: filename,
  121. type: finalUti,
  122. renderType: renderType,
  123. sendAsFile: true)
  124. }
  125. /// Create an URLSenderItem from an image represented by an URL.
  126. /// The image may be converted to jpeg if it is not of a valid type e.g. HEIC will be converted to jpg. PNG will never be converted to jpg.
  127. /// - Parameter url: The URL pointing to a valid image in any format readable by UIImage and convertable by UIImage.jpegData.
  128. /// - Returns: An URLSenderItem for the image
  129. @objc public func senderItem(from url : URL) -> URLSenderItem? {
  130. guard let scheme = url.scheme else {
  131. return nil
  132. }
  133. if (scheme != "file") || !FileManager.default.fileExists(atPath: url.relativePath) {
  134. return nil
  135. }
  136. guard var uti = UTIConverter.uti(forFileURL: url) else {
  137. return nil
  138. }
  139. do {
  140. var data = try Data(contentsOf: url)
  141. if !ImageURLSenderItemCreator.isAllowedUTI(uti: uti) {
  142. guard let image = UIImage(data: data) else {
  143. return nil
  144. }
  145. guard let imageData = image.jpegData(compressionQuality: 1.0) else {
  146. return nil
  147. }
  148. data = imageData
  149. uti = kUTTypeJPEG as String
  150. guard let item = self.senderItem(from: data, uti: uti) else {
  151. DDLogError("Could not create item")
  152. return nil
  153. }
  154. return item
  155. } else {
  156. guard let item = self.senderItem(url: url, uti: uti) else {
  157. DDLogError("Could not create item")
  158. return nil
  159. }
  160. return item
  161. }
  162. } catch {
  163. DDLogError(error.localizedDescription)
  164. }
  165. return nil
  166. }
  167. /// Create an URLSenderItem from an image represented by an UIImage object
  168. /// The image will always be converted to jpg
  169. /// - Parameter image: An image
  170. /// - Returns: An URLSenderItem for the image
  171. @available(*, deprecated, message: "Is only available for to support legacy Objective-C code. Please use any of the other functions")
  172. @objc func senderItem(fromImage image : UIImage) -> URLSenderItem? {
  173. guard let image = MediaConverter.scale(image, toMaxSize: imageMaxSize(image)) else {
  174. return nil
  175. }
  176. let data = image.jpegData(compressionQuality: 1.0)!
  177. let type = kUTTypeJPEG as String
  178. let renderType : NSNumber = 1
  179. let ext = UTIConverter.preferedFileExtension(forMimeType: UTIConverter.mimeType(fromUTI: type))!
  180. let filename = FileUtility.getTemporarySendableFileName(base: "image") + ext
  181. return URLSenderItem(data: data,
  182. fileName:filename,
  183. type: type,
  184. renderType: renderType,
  185. sendAsFile: true)
  186. }
  187. // MARK: Public static helper functions
  188. /// Checks if the given png UIImage and uti combination could be represented as a sticker (render type 2)
  189. /// - Parameters:
  190. /// - image: any png image
  191. /// - uti: any uti type
  192. /// - Returns: true if it can be represented as a sticker and false otherwise
  193. @objc static func isPNGSticker(image : UIImage, uti : String) -> Bool {
  194. if UTIConverter.isPNGImageMimeType(UTIConverter.mimeType(fromUTI: uti)) {
  195. guard let cgImage = image.cgImage else {
  196. return false
  197. }
  198. let hasAlpha = ImageURLSenderItemCreator.hasAlpha(image: cgImage)
  199. let isTransparent = ImageURLSenderItemCreator.hasTransparentPixel(cgImage: cgImage)
  200. return hasAlpha && isTransparent
  201. }
  202. return false
  203. }
  204. public static func hasAlpha(image : CGImage) -> Bool {
  205. let alpha: CGImageAlphaInfo = image.alphaInfo
  206. return alpha == .first || alpha == .last || alpha == .premultipliedFirst || alpha == .premultipliedLast
  207. }
  208. public static func hasTransparentPixel(cgImage : CGImage) -> Bool {
  209. if !(cgImage.alphaInfo == .last || cgImage.alphaInfo == .premultipliedLast || cgImage.alphaInfo == .first || cgImage.alphaInfo == .premultipliedFirst) {
  210. return false
  211. }
  212. if cgImage.colorSpace?.model != .rgb {
  213. // We only deal with rgb
  214. return false
  215. }
  216. let bytesPerComponent = cgImage.bitsPerComponent / 8
  217. if bytesPerComponent > 8 {
  218. // Might overflow UInt64
  219. return false
  220. }
  221. guard let data = cgImage.dataProvider?.data,
  222. let bytes = CFDataGetBytePtr(data) else {
  223. fatalError("Couldn't access image data")
  224. }
  225. let alphaPosition = cgImage.alphaInfo == .last || cgImage.alphaInfo == .premultipliedLast ? 3 : 0
  226. let newData = NSData(bytes: bytes + (alphaPosition * bytesPerComponent), length: bytesPerComponent)
  227. let alpha : UInt64 = newData.bytes.load(as: UInt64.self)
  228. return alpha == 0
  229. }
  230. /// Returns the UTI from Data by checking the first byte. Not all UTTypes are covered, check
  231. /// that all possible UTTypes for your object are covered.
  232. /// - Parameter data: any Data object
  233. /// - Returns: A CFString with the UTType of the Data object.
  234. @objc static func getUTI(for data : Data) -> CFString? {
  235. var values = [UInt8](repeating:0, count:1)
  236. data.copyBytes(to: &values, count: 1)
  237. switch (values[0]) {
  238. case 0xFF:
  239. return kUTTypeJPEG
  240. case 0x89:
  241. return kUTTypePNG
  242. case 0x47:
  243. return kUTTypeGIF
  244. default:
  245. break
  246. }
  247. return nil
  248. }
  249. @objc public static func createCorrelationID() -> String {
  250. return SwiftUtils.pseudoRandomString(length: 32)
  251. }
  252. // MARK: Public Helper Functions
  253. func imageMaxSize(_ image: UIImage? = nil) -> CGFloat {
  254. var maxSize : CGFloat
  255. switch self.userSettingsImageSize {
  256. case "small":
  257. maxSize = ImageURLSenderItemCreator.kImageSizeSmall
  258. case "large":
  259. maxSize = ImageURLSenderItemCreator.kImageSizeLarge
  260. case "xlarge":
  261. maxSize = ImageURLSenderItemCreator.kImageSizeXLarge
  262. case "original":
  263. if let image = image {
  264. maxSize = max(image.size.width, image.size.height)
  265. } else {
  266. maxSize = 0
  267. }
  268. default:
  269. maxSize = ImageURLSenderItemCreator.kImageSizeMedium
  270. }
  271. return maxSize
  272. }
  273. // MARK: Private Helper Functions
  274. /// Returns true if the given uti is supported by the file message spec
  275. /// - Parameter uti: any UTI represented as String
  276. /// - Returns: true if the given uti is supported by the file message spec false otherwise
  277. static func isAllowedUTI(uti : String) -> Bool {
  278. let isJPEG = uti == (kUTTypeJPEG as String)
  279. let isGIF = uti == (kUTTypeGIF as String)
  280. let isPNG = uti == (kUTTypePNG as String)
  281. return isJPEG || isGIF || isPNG
  282. }
  283. static func getImageSizeForString(size : String) -> CGFloat {
  284. switch size {
  285. case "small":
  286. return ImageURLSenderItemCreator.kImageSizeSmall
  287. case "large":
  288. return ImageURLSenderItemCreator.kImageSizeLarge
  289. case "xlarge":
  290. return ImageURLSenderItemCreator.kImageSizeXLarge
  291. case "original":
  292. return ImageURLSenderItemCreator.kImageSizeOriginal
  293. default:
  294. return ImageURLSenderItemCreator.kImageSizeMedium
  295. }
  296. }
  297. /// Returns the number of supported image sizes
  298. /// - Returns:
  299. @objc static func getImageSizeNo() -> Int {
  300. guard let imageSizes = imageSizes() else {
  301. return 0
  302. }
  303. return imageSizes.count
  304. }
  305. ///
  306. /// - Returns: A list of the descriptions of the available image sizes as String
  307. @objc static func imageSizes() -> [AnyHashable]? {
  308. return ["small", "medium", "large", "xlarge", "original"]
  309. }
  310. ///
  311. /// - Returns: A list of the sizes of the available image sizes as Float
  312. @objc static func imagePixelSizes() -> [AnyHashable]? {
  313. return [
  314. NSNumber(value: Float(kImageSizeSmall)),
  315. NSNumber(value: Float(kImageSizeMedium)),
  316. NSNumber(value: Float(kImageSizeLarge)),
  317. NSNumber(value: Float(kImageSizeXLarge)),
  318. NSNumber(value: Float(kImageSizeOriginal))
  319. ]
  320. }
  321. }