KingfisherManager+LivePhoto.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. //
  2. // KingfisherManager+LivePhoto.swift
  3. // Kingfisher
  4. //
  5. // Created by onevcat on 2024/10/01.
  6. //
  7. // Copyright (c) 2024 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. #if !os(watchOS)
  27. @preconcurrency import Photos
  28. /// A structure that contains information about the result of loading a live photo.
  29. public struct LivePhotoLoadingInfoResult: Sendable {
  30. /// Retrieves the live photo disk URLs from this result.
  31. public let fileURLs: [URL]
  32. /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved.
  33. ///
  34. /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned.
  35. /// Otherwise, ``CacheType/disk`` will be returned for the live photo. ``CacheType/memory`` is not available for
  36. /// live photos since it may take too much memory. All cached live photos are loaded from disk only.
  37. public let cacheType: CacheType
  38. /// The ``LivePhotoSource`` to which this result is related. This indicates where the `livePhoto` referenced by
  39. /// `self` is located.
  40. public let source: LivePhotoSource
  41. /// The original ``LivePhotoSource`` from which the retrieval task begins. It may differ from the ``source`` property.
  42. /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the
  43. /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process.
  44. public let originalSource: LivePhotoSource
  45. /// Retrieves the data associated with this result.
  46. ///
  47. /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns
  48. /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache
  49. /// serializer from the loading options and returns the result.
  50. ///
  51. /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to
  52. /// use it multiple times and avoid frequent calls to this method.
  53. public let data: @Sendable () -> [Data]
  54. }
  55. extension KingfisherManager {
  56. /// Retrieves a live photo from the specified source.
  57. ///
  58. /// This method asynchronously loads a live photo from the given source, applying the specified options and
  59. /// reporting progress if a progress block is provided.
  60. ///
  61. /// - Parameters:
  62. /// - source: The ``LivePhotoSource`` from which to retrieve the live photo.
  63. /// - options: A dictionary of options to apply to the retrieval process. If `nil`, the default options will be
  64. /// used.
  65. /// - progressBlock: An optional closure to be called periodically during the download process.
  66. /// - referenceTaskIdentifierChecker: An optional closure that returns a Boolean value indicating whether the task
  67. /// should proceed.
  68. ///
  69. /// - Returns: A ``LivePhotoLoadingInfoResult`` containing information about the retrieved live photo.
  70. ///
  71. /// - Throws: An error if the retrieval process fails.
  72. ///
  73. /// - Note: This method uses `LivePhotoImageProcessor` by default. Custom processors are not supported for live photos.
  74. ///
  75. /// - Warning: Not all options are working for this method. And currently the `progressBlock` is not working.
  76. /// It will be implemented in the future.
  77. public func retrieveLivePhoto(
  78. with source: LivePhotoSource,
  79. options: KingfisherOptionsInfo? = nil,
  80. progressBlock: DownloadProgressBlock? = nil,
  81. referenceTaskIdentifierChecker: (() -> Bool)? = nil
  82. ) async throws -> LivePhotoLoadingInfoResult {
  83. let fullOptions = currentDefaultOptions + (options ?? .empty)
  84. var checkedOptions = KingfisherParsedOptionsInfo(fullOptions)
  85. if checkedOptions.processor == DefaultImageProcessor.default {
  86. // The default processor is a default behavior so we replace it silently.
  87. checkedOptions.processor = LivePhotoImageProcessor.default
  88. } else if checkedOptions.processor != LivePhotoImageProcessor.default {
  89. // Warn the framework user that the processor is not supported.
  90. assertionFailure("[Kingfisher] Using of custom processors during loading of live photo resource is not supported.")
  91. checkedOptions.processor = LivePhotoImageProcessor.default
  92. }
  93. if let checker = referenceTaskIdentifierChecker {
  94. checkedOptions.onDataReceived?.forEach {
  95. $0.onShouldApply = checker
  96. }
  97. }
  98. // TODO. We ignore the retry of live photo and the progress now to suppress the complexity.
  99. let missingResources = missingResources(source, options: checkedOptions)
  100. let resourcesResult = try await downloadAndCache(resources: missingResources, options: checkedOptions)
  101. let targetCache = checkedOptions.targetCache ?? cache
  102. var fileURLs = [URL]()
  103. for resource in source.resources {
  104. let url = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: checkedOptions)
  105. guard let url else {
  106. // This should not happen normally if the previous `downloadAndCache` done without issue, but in case.
  107. throw KingfisherError.cacheError(reason: .missingLivePhotoResourceOnDisk(resource))
  108. }
  109. fileURLs.append(url)
  110. }
  111. return LivePhotoLoadingInfoResult(
  112. fileURLs: fileURLs,
  113. cacheType: missingResources.isEmpty ? .disk : .none,
  114. source: source,
  115. originalSource: source,
  116. data: {
  117. resourcesResult.map { $0.originalData }
  118. })
  119. }
  120. // Returns the missing resources for the given source and options. If the resource is not in the cache, it will be
  121. // returned as a missing resource.
  122. func missingResources(_ source: LivePhotoSource, options: KingfisherParsedOptionsInfo) -> [LivePhotoResource] {
  123. let missingResources: [LivePhotoResource]
  124. if options.forceRefresh {
  125. missingResources = source.resources
  126. } else {
  127. let targetCache = options.targetCache ?? cache
  128. missingResources = source.resources.reduce([], { r, resource in
  129. // Check if the resource is in the cache. It includes a guess of the file extension.
  130. let cachedFileURL = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: options)
  131. if cachedFileURL == nil {
  132. return r + [resource]
  133. } else {
  134. return r
  135. }
  136. })
  137. }
  138. return missingResources
  139. }
  140. // Download the resources and store them to the cache.
  141. // If the resource does not specify a file extension (from either the URL extension or the explicit
  142. // `referenceFileType`), we infer it from the file signature.
  143. func downloadAndCache(
  144. resources: [LivePhotoResource],
  145. options: KingfisherParsedOptionsInfo
  146. ) async throws -> [LivePhotoResourceDownloadingResult] {
  147. if resources.isEmpty {
  148. return []
  149. }
  150. let downloader = options.downloader ?? downloader
  151. let cache = options.targetCache ?? cache
  152. // Download all resources concurrently.
  153. return try await withThrowingTaskGroup(of: LivePhotoResourceDownloadingResult.self) {
  154. group in
  155. for resource in resources {
  156. group.addTask {
  157. let downloadedResource: LivePhotoResourceDownloadingResult
  158. switch resource.dataSource {
  159. case .network(let urlResource):
  160. downloadedResource = try await downloader.downloadLivePhotoResource(
  161. with: urlResource.downloadURL,
  162. options: options
  163. )
  164. case .provider(let provider):
  165. downloadedResource = try await LivePhotoResourceDownloadingResult(
  166. originalData: provider.data(),
  167. url: provider.contentURL
  168. )
  169. }
  170. // We need to specify the extension so the file is saved correctly. Live photo loading requires
  171. // the file extension to be correct. Otherwise, a 3302 error will be thrown.
  172. // https://developer.apple.com/documentation/photokit/phphotoserror/code/invalidresource
  173. let fileExtension = resource.referenceFileType
  174. .determinedFileExtension(downloadedResource.originalData)
  175. try await cache.storeToDisk(
  176. downloadedResource.originalData,
  177. forKey: resource.cacheKey,
  178. processorIdentifier: options.processor.identifier,
  179. forcedExtension: fileExtension,
  180. expiration: options.diskCacheExpiration
  181. )
  182. return downloadedResource
  183. }
  184. }
  185. var result: [LivePhotoResourceDownloadingResult] = []
  186. for try await resource in group {
  187. result.append(resource)
  188. }
  189. return result
  190. }
  191. }
  192. }
  193. extension ImageCache {
  194. func possibleCacheFileURLIfOnDisk(
  195. resource: LivePhotoResource,
  196. options: KingfisherParsedOptionsInfo
  197. ) -> URL? {
  198. possibleCacheFileURLIfOnDisk(
  199. forKey: resource.cacheKey,
  200. processorIdentifier: options.processor.identifier,
  201. referenceFileType: resource.referenceFileType
  202. )
  203. }
  204. // Returns the possible cache file URL for the given key and processor identifier. If the file is on disk, it will
  205. // return the URL. Otherwise, it will return `nil`.
  206. //
  207. // This method also tries to guess the file extension if it is not specified in the `referenceFileType`.
  208. // `PHLivePhoto`'s `request` method requires the file extension to be correct on the disk, and we also stored the
  209. // downloaded data with the correct extension (if it is not specified in the `referenceFileType`, we infer it from
  210. // the file signature. See `FileType.determinedFileExtension` for more).
  211. func possibleCacheFileURLIfOnDisk(
  212. forKey key: String,
  213. processorIdentifier identifier: String,
  214. referenceFileType: LivePhotoResource.FileType
  215. ) -> URL? {
  216. switch referenceFileType {
  217. case .heic, .mov:
  218. // The extension is specified and is what necessary to load a live photo, use it.
  219. return cacheFileURLIfOnDisk(
  220. forKey: key, processorIdentifier: identifier, forcedExtension: referenceFileType.fileExtension
  221. )
  222. case .other(let ext):
  223. if ext.isEmpty {
  224. // The extension is not specified. Guess from the default set of values.
  225. let possibleFileTypes: [LivePhotoResource.FileType] = [.heic, .mov]
  226. for fileType in possibleFileTypes {
  227. let url = cacheFileURLIfOnDisk(
  228. forKey: key, processorIdentifier: identifier, forcedExtension: fileType.fileExtension
  229. )
  230. if url != nil {
  231. // Found, early return.
  232. return url
  233. }
  234. }
  235. return nil
  236. } else {
  237. // The extension is specified but maybe not valid for live photo. Trust the user and use it to find the
  238. // file.
  239. return cacheFileURLIfOnDisk(
  240. forKey: key, processorIdentifier: identifier, forcedExtension: ext
  241. )
  242. }
  243. }
  244. }
  245. }
  246. #endif