mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			175 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Swift
		
	
		
		
			
		
	
	
			175 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Swift
		
	
| 
											3 years ago
										 | // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. | ||
|  | 
 | ||
|  | import Foundation | ||
|  | import AVFoundation | ||
|  | import SessionUtilitiesKit | ||
|  | 
 | ||
|  | public class ThumbnailService { | ||
|  |     // MARK: - Singleton class | ||
|  | 
 | ||
|  |     public static let shared: ThumbnailService = ThumbnailService() | ||
|  | 
 | ||
|  |     public typealias SuccessBlock = (LoadedThumbnail) -> Void | ||
|  |     public typealias FailureBlock = (Error) -> Void | ||
|  | 
 | ||
|  |     private let serialQueue = DispatchQueue(label: "ThumbnailService") | ||
|  | 
 | ||
|  |     // This property should only be accessed on the serialQueue. | ||
|  |     // | ||
|  |     // We want to process requests in _reverse_ order in which they | ||
|  |     // arrive so that we prioritize the most recent view state. | ||
|  |     private var requestStack = [Request]() | ||
|  | 
 | ||
|  |     private func canThumbnailAttachment(attachment: Attachment) -> Bool { | ||
|  |         return attachment.isImage || attachment.isAnimated || attachment.isVideo | ||
|  |     } | ||
|  | 
 | ||
|  |     public func ensureThumbnail( | ||
|  |         for attachment: Attachment, | ||
|  |         dimensions: UInt, | ||
|  |         success: @escaping SuccessBlock, | ||
|  |         failure: @escaping FailureBlock | ||
|  |     ) { | ||
|  |         serialQueue.async { | ||
|  |             self.requestStack.append( | ||
|  |                 Request( | ||
|  |                     attachment: attachment, | ||
|  |                     dimensions: dimensions, | ||
|  |                     success: success, | ||
|  |                     failure: failure | ||
|  |                 ) | ||
|  |             ) | ||
|  | 
 | ||
|  |             self.processNextRequestSync() | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     private func processNextRequestAsync() { | ||
|  |         serialQueue.async { | ||
|  |             self.processNextRequestSync() | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     // This should only be called on the serialQueue. | ||
|  |     private func processNextRequestSync() { | ||
|  |         guard let thumbnailRequest = requestStack.popLast() else { return } | ||
|  | 
 | ||
|  |         do { | ||
|  |             let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest) | ||
|  |             DispatchQueue.global().async { | ||
|  |                 thumbnailRequest.success(loadedThumbnail) | ||
|  |             } | ||
|  |         } | ||
|  |         catch { | ||
|  |             DispatchQueue.global().async { | ||
|  |                 thumbnailRequest.failure(error) | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     // This should only be called on the serialQueue. | ||
|  |     // | ||
|  |     // It should be safe to assume that an attachment will never end up with two thumbnails of | ||
|  |     // the same size since: | ||
|  |     // | ||
|  |     // * Thumbnails are only added by this method. | ||
|  |     // * This method checks for an existing thumbnail using the same connection. | ||
|  |     // * This method is performed on the serial queue. | ||
|  |     private func process(thumbnailRequest: Request) throws -> LoadedThumbnail { | ||
|  |         let attachment = thumbnailRequest.attachment | ||
|  |          | ||
|  |         guard canThumbnailAttachment(attachment: attachment) else { | ||
|  |             throw ThumbnailError.failure(description: "Cannot thumbnail attachment.") | ||
|  |         } | ||
|  |          | ||
|  |         let thumbnailPath = attachment.thumbnailPath(for: thumbnailRequest.dimensions) | ||
|  |          | ||
|  |         if FileManager.default.fileExists(atPath: thumbnailPath) { | ||
|  |             guard let image = UIImage(contentsOfFile: thumbnailPath) else { | ||
|  |                 throw ThumbnailError.failure(description: "Could not load thumbnail.") | ||
|  |             } | ||
|  |             return LoadedThumbnail(image: image, filePath: thumbnailPath) | ||
|  |         } | ||
|  | 
 | ||
|  |         let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent | ||
|  |          | ||
|  |         guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else { | ||
|  |             throw ThumbnailError.failure(description: "Could not create attachment's thumbnail directory.") | ||
|  |         } | ||
|  |         guard let originalFilePath = attachment.originalFilePath else { | ||
|  |             throw ThumbnailError.failure(description: "Missing original file path.") | ||
|  |         } | ||
|  |          | ||
|  |         let maxDimension = CGFloat(thumbnailRequest.dimensions) | ||
|  |         let thumbnailImage: UIImage | ||
|  |          | ||
|  |         if attachment.isImage || attachment.isAnimated { | ||
|  |             thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension) | ||
|  |         } | ||
|  |         else if attachment.isVideo { | ||
|  |             thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension) | ||
|  |         } | ||
|  |         else { | ||
|  |             throw ThumbnailError.assertionFailure(description: "Invalid attachment type.") | ||
|  |         } | ||
|  |          | ||
|  |         guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else { | ||
|  |             throw ThumbnailError.failure(description: "Could not convert thumbnail to JPEG.") | ||
|  |         } | ||
|  |          | ||
|  |         do { | ||
|  |             try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath, isDirectory: false), options: .atomic) | ||
|  |         } | ||
|  |         catch let error as NSError { | ||
|  |             throw ThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error) | ||
|  |         } | ||
|  |          | ||
|  |         OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath) | ||
|  |          | ||
|  |         return LoadedThumbnail(image: thumbnailImage, data: thumbnailData) | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | public extension ThumbnailService { | ||
|  |     enum ThumbnailError: Error { | ||
|  |         case failure(description: String) | ||
|  |         case assertionFailure(description: String) | ||
|  |         case externalError(description: String, underlyingError: Error) | ||
|  |     } | ||
|  |      | ||
|  |     struct LoadedThumbnail { | ||
|  |         public typealias DataSourceBlock = () throws -> Data | ||
|  | 
 | ||
|  |         public let image: UIImage | ||
|  |         public let dataSourceBlock: DataSourceBlock | ||
|  | 
 | ||
|  |         public init(image: UIImage, filePath: String) { | ||
|  |             self.image = image | ||
|  |             self.dataSourceBlock = { | ||
|  |                 return try Data(contentsOf: URL(fileURLWithPath: filePath)) | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         public init(image: UIImage, data: Data) { | ||
|  |             self.image = image | ||
|  |             self.dataSourceBlock = { | ||
|  |                 return data | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         public func data() throws -> Data { | ||
|  |             return try dataSourceBlock() | ||
|  |         } | ||
|  |     } | ||
|  |      | ||
|  |     private struct Request { | ||
|  |         public typealias SuccessBlock = (LoadedThumbnail) -> Void | ||
|  |         public typealias FailureBlock = (Error) -> Void | ||
|  | 
 | ||
|  |         let attachment: Attachment | ||
|  |         let dimensions: UInt | ||
|  |         let success: SuccessBlock | ||
|  |         let failure: FailureBlock | ||
|  |     } | ||
|  | } |