import Foundation import SwiftSignalKit import Postbox import TelegramCore import UIKit import TinyThumbnail import Display import FastBlur import MozjpegBinding import Accelerate private func generateBlurredThumbnail(image: UIImage) -> UIImage? { let thumbnailContextSize = CGSize(width: 32.0, height: 32.0) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) let filledSize = image.size.aspectFilled(thumbnailContextSize) let imageRect = CGRect(origin: CGPoint(x: (thumbnailContextSize.width - filledSize.width) / 2.0, y: (thumbnailContextSize.height - filledSize.height) / 2.0), size: filledSize) thumbnailContext.withFlippedContext { c in c.draw(image.cgImage!, in: imageRect) } telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) return thumbnailContext.generateImage() } private func storeImage(context: DrawingContext, to path: String) -> UIImage? { if context.size.width <= 70.0 && context.size.height <= 70.0 { guard let file = ManagedFile(queue: nil, path: path, mode: .readwrite) else { return nil } var header: UInt32 = 0xcaf2 let _ = file.write(&header, count: 4) var width: UInt16 = UInt16(context.size.width) let _ = file.write(&width, count: 2) var height: UInt16 = UInt16(context.size.height) let _ = file.write(&height, count: 2) var source = vImage_Buffer() source.width = UInt(context.size.width) source.height = UInt(context.size.height) source.rowBytes = context.bytesPerRow source.data = context.bytes var target = vImage_Buffer() target.width = UInt(context.size.width) target.height = UInt(context.size.height) target.rowBytes = Int(context.size.width) * 2 let targetLength = Int(target.height) * target.rowBytes let targetData = malloc(targetLength)! defer { free(targetData) } target.data = targetData vImageConvert_BGRA8888toRGB565(&source, &target, vImage_Flags(kvImageDoNotTile)) let _ = file.write(targetData, count: targetLength) return context.generateImage() } else { guard let image = context.generateImage(), let resultData = image.jpegData(compressionQuality: 0.7) else { return nil } let _ = try? resultData.write(to: URL(fileURLWithPath: path)) return image } } private func loadImage(data: Data) -> UIImage? { if data.count > 4 + 2 + 2 + 2 { var header: UInt32 = 0 withUnsafeMutableBytes(of: &header, { header in data.copyBytes(to: header.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 0 ..< 4) }) if header == 0xcaf1 { var width: UInt16 = 0 withUnsafeMutableBytes(of: &width, { width in data.copyBytes(to: width.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 4 ..< (4 + 2)) }) var height: UInt16 = 0 withUnsafeMutableBytes(of: &height, { height in data.copyBytes(to: height.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (4 + 2) ..< (4 + 2 + 2)) }) var bytesPerRow: UInt16 = 0 withUnsafeMutableBytes(of: &bytesPerRow, { bytesPerRow in data.copyBytes(to: bytesPerRow.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (4 + 2 + 2) ..< (4 + 2 + 2 + 2)) }) let imageData = data.subdata(in: (4 + 2 + 2 + 2) ..< data.count) guard let dataProvider = CGDataProvider(data: imageData as CFData) else { return nil } if let image = CGImage( width: Int(width), height: Int(height), bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent, bitsPerPixel: DeviceGraphicsContextSettings.shared.bitsPerPixel, bytesPerRow: Int(bytesPerRow), space: DeviceGraphicsContextSettings.shared.colorSpace, bitmapInfo: DeviceGraphicsContextSettings.shared.opaqueBitmapInfo, provider: dataProvider, decode: nil, shouldInterpolate: true, intent: .defaultIntent ) { return UIImage(cgImage: image, scale: 1.0, orientation: .up) } else { return nil } } else if header == 0xcaf2 { var width: UInt16 = 0 withUnsafeMutableBytes(of: &width, { width in data.copyBytes(to: width.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 4 ..< (4 + 2)) }) var height: UInt16 = 0 withUnsafeMutableBytes(of: &height, { height in data.copyBytes(to: height.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (4 + 2) ..< (4 + 2 + 2)) }) return data.withUnsafeBytes { data -> UIImage? in let sourceBytes = data.baseAddress! var source = vImage_Buffer() source.width = UInt(width) source.height = UInt(height) source.rowBytes = Int(width * 2) source.data = UnsafeMutableRawPointer(mutating: sourceBytes.advanced(by: 4 + 2 + 2)) let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: true, clear: false) var target = vImage_Buffer() target.width = UInt(width) target.height = UInt(height) target.rowBytes = context.bytesPerRow target.data = context.bytes vImageConvert_RGB565toBGRA8888(0xff, &source, &target, vImage_Flags(kvImageDoNotTile)) return context.generateImage() } } } if let decompressedImage = decompressImage(data) { return decompressedImage } return UIImage(data: data) } public final class DirectMediaImageCache { public final class GetMediaResult { public let image: UIImage? public let loadSignal: Signal? init(image: UIImage?, loadSignal: Signal?) { self.image = image self.loadSignal = loadSignal } } private enum ImageType { case blurredThumbnail case square(width: Int) } private let account: Account public init(account: Account) { self.account = account } private func getCachePath(resourceId: MediaResourceId, imageType: ImageType) -> String { let representationId: String switch imageType { case .blurredThumbnail: representationId = "blurred32" case let .square(width): representationId = "shm\(width)" } return self.account.postbox.mediaBox.cachedRepresentationPathForId(resourceId.stringRepresentation, representationId: representationId, keepDuration: .general) } private func getLoadSignal(resource: MediaResourceReference, width: Int) -> Signal? { let cachePath = self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)) return Signal { subscriber in let fetch = fetchedMediaResource(mediaBox: self.account.postbox.mediaBox, reference: resource).start() let data = (self.account.postbox.mediaBox.resourceData(resource.resource) |> filter { data in return data.complete } |> take(1)).start(next: { data in if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: dataValue) { let scaledSize = CGSize(width: CGFloat(width), height: CGFloat(width)) let scaledContext = DrawingContext(size: scaledSize, scale: 1.0, opaque: true) scaledContext.withFlippedContext { context in let filledSize = image.size.aspectFilled(scaledSize) let imageRect = CGRect(origin: CGPoint(x: (scaledSize.width - filledSize.width) / 2.0, y: (scaledSize.height - filledSize.height) / 2.0), size: filledSize) context.draw(image.cgImage!, in: imageRect) } if let scaledImage = storeImage(context: scaledContext, to: cachePath) { subscriber.putNext(scaledImage) subscriber.putCompletion() } } }) return ActionDisposable { fetch.dispose() data.dispose() } } } private func getResource(message: Message, image: TelegramMediaImage) -> MediaResourceReference? { guard let representation = image.representations.last else { return nil } return MediaReference.message(message: MessageReference(message), media: image).resourceReference(representation.resource) } private func getResource(message: Message, file: TelegramMediaFile) -> MediaResourceReference? { if let representation = file.previewRepresentations.last { return MediaReference.message(message: MessageReference(message), media: file).resourceReference(representation.resource) } else { return nil } } public func getImage(message: Message, media: Media, width: Int, synchronous: Bool) -> GetMediaResult? { if synchronous { var immediateThumbnailData: Data? var resource: MediaResourceReference? if let image = media as? TelegramMediaImage { immediateThumbnailData = image.immediateThumbnailData resource = self.getResource(message: message, image: image) } else if let file = media as? TelegramMediaFile { immediateThumbnailData = file.immediateThumbnailData resource = self.getResource(message: message, file: file) } if let resource = resource { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)))), let image = loadImage(data: data) { return GetMediaResult(image: image, loadSignal: nil) } var blurredImage: UIImage? if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) { blurredImage = image } else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) { if let blurredImageValue = generateBlurredThumbnail(image: image) { blurredImage = blurredImageValue } } return GetMediaResult(image: blurredImage, loadSignal: self.getLoadSignal(resource: resource, width: width)) } else { return nil } } else { return GetMediaResult(image: nil, loadSignal: Signal { subscriber in var immediateThumbnailData: Data? var resource: MediaResourceReference? if let image = media as? TelegramMediaImage { immediateThumbnailData = image.immediateThumbnailData resource = self.getResource(message: message, image: image) } else if let file = media as? TelegramMediaFile { immediateThumbnailData = file.immediateThumbnailData resource = self.getResource(message: message, file: file) } if let resource = resource { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)))), let image = loadImage(data: data) { subscriber.putNext(image) subscriber.putCompletion() return EmptyDisposable } var blurredImage: UIImage? if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) { blurredImage = image } else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) { if let blurredImageValue = generateBlurredThumbnail(image: image) { blurredImage = blurredImageValue } } if let blurredImage = blurredImage { subscriber.putNext(blurredImage) } if let signal = self.getLoadSignal(resource: resource, width: width) { return signal.start(next: subscriber.putNext, completed: subscriber.putCompletion) } else { subscriber.putNext(nil) subscriber.putCompletion() return EmptyDisposable } } else { subscriber.putNext(nil) subscriber.putCompletion() return EmptyDisposable } } |> runOn(.concurrentDefaultQueue())) } } }