import Foundation import Postbox import SwiftSignalKit import Display import AVFoundation import ImageIO import TelegramUIPrivateModule import TelegramCore private enum ResourceFileData { case data(Data) case file(path: String, size: Int) } public func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> TelegramMediaImageRepresentation? { return photo.representationForDisplayAtSize(CGSize(width: 1280.0, height: 1280.0)) } func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(fullRepresentationSize) { let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { let decodedThumbnailData = photoReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) let fetchedThumbnail: Signal if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { fetchedThumbnail = fetchedMediaResource(postbox: postbox, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) } let fetchedFullSize = fetchedMediaResource(postbox: postbox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) let anyThumbnail: [Signal] if tryAdditionalRepresentations { anyThumbnail = photoReference.media.representations.filter({ representation in return representation != largestRepresentation }).map({ representation -> Signal in return postbox.mediaBox.resourceData(representation.resource) |> take(1) }) } else { anyThumbnail = [] } let mainThumbnail = Signal { subscriber in if let decodedThumbnailData = decodedThumbnailData { subscriber.putNext(decodedThumbnailData) subscriber.putCompletion() return EmptyDisposable } else { let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } let thumbnail = combineLatest(anyThumbnail) |> mapToSignal { thumbnails -> Signal in for thumbnail in thumbnails { if thumbnail.size != 0, let data = try? Data(contentsOf: URL(fileURLWithPath: thumbnail.path), options: []) { return .single(data) } } return mainThumbnail } let fullSizeData: Signal<(Data?, Bool), NoError> if autoFetchFullSize { fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedFullSizeDisposable.dispose() fullSizeDisposable.dispose() } } } else { fullSizeData = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) |> map { next -> (Data?, Bool) in return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } return thumbnail |> mapToSignal { thumbnailData in if let thumbnailData = thumbnailData { return fullSizeData |> map { (fullSizeData, complete) in return (thumbnailData, fullSizeData, complete) } } else { return .single((thumbnailData, nil, false)) } } } } |> distinctUntilChanged(isEqual: { lhs, rhs in if (lhs.0 == nil && lhs.1 == nil) && (rhs.0 == nil && rhs.1 == nil) { return true } else { return false } }) return signal } else { return .never() } } private func chatMessageFileDatas(account: Account, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false, fetched: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { let thumbnailResource = fetched ? nil : smallestImageRepresentation(fileReference.media.previewRepresentations)?.resource let fullSizeResource = fileReference.media.resource let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource, pathExtension: pathExtension) let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in if maybeData.complete { return .single((nil, maybeData.path, true)) } else { let fetchedThumbnail: Signal if !fetched, let _ = decodedThumbnailData { fetchedThumbnail = .single(.local) } else if let thumbnailResource = thumbnailResource { fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: statsCategoryForFileWithAttributes(fileReference.media.attributes)) } else { fetchedThumbnail = .complete() } let thumbnail: Signal if !fetched, let decodedThumbnailData = decodedThumbnailData { thumbnail = .single(decodedThumbnailData) } else if let thumbnailResource = thumbnailResource { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in if next.size != 0, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) { subscriber.putNext(data) } else { subscriber.putNext(nil) } }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { thumbnail = .single(nil) } let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, option: !progressive ? .complete(waitUntilFetchStatus: false) : .incremental(waitUntilFetchStatus: false)) |> map { next -> (String?, Bool) in return (next.size == 0 ? nil : next.path, next.complete) } return thumbnail |> mapToSignal { thumbnailData in return fullSizeDataAndPath |> map { (dataPath, complete) in return (thumbnailData, dataPath, complete) } } } } |> filter({ $0.0 != nil || $0.1 != nil }) return signal } private let thumbnailGenerationMimeTypes: Set = Set([ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/heic" ]) private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { let thumbnailResource = smallestImageRepresentation(fileReference.media.previewRepresentations)?.resource let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) if !thumbnailGenerationMimeTypes.contains(fileReference.media.mimeType) { if let decodedThumbnailData = decodedThumbnailData { return .single((decodedThumbnailData, nil, false)) } else if let thumbnailResource = thumbnailResource { let fetchedThumbnail: Signal = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(thumbnailResource)) return Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in if next.size != 0, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) { subscriber.putNext((data, nil, false)) } else { subscriber.putNext((nil, nil, false)) } }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { return .single((nil, nil, false)) } } let fullSizeResource: MediaResource = fileReference.media.resource let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false, fetch: false) let fetchedFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false, fetch: true) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in if maybeData.complete { return .single((nil, maybeData.path, true)) } else { let fetchedThumbnail: Signal if let _ = fileReference.media.immediateThumbnailData { fetchedThumbnail = .complete() } else if let thumbnailResource = thumbnailResource { fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(thumbnailResource)) } else { fetchedThumbnail = .complete() } let thumbnail: Signal if let decodedThumbnailData = decodedThumbnailData { thumbnail = .single(decodedThumbnailData) } else if let thumbnailResource = thumbnailResource { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in if next.size != 0, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) { subscriber.putNext(data) } else { subscriber.putNext(nil) } }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { thumbnail = .single(nil) } let fullSizeDataAndPath = fetchedFullSize |> map { next -> (String?, Bool) in return (next.size == 0 ? nil : next.path, next.complete) } return thumbnail |> mapToSignal { thumbnailData in return fullSizeDataAndPath |> map { (dataPath, complete) in return (thumbnailData, dataPath, complete) } } } } |> filter({ $0.0 != nil || $0.1 != nil }) return signal } private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaReference, thumbnailSize: Bool = false, onlyFullSize: Bool = false) -> Signal<(Data?, (Data, String)?, Bool), NoError> { let fullSizeResource = fileReference.media.resource let thumbnailResource = smallestImageRepresentation(fileReference.media.previewRepresentations)?.resource let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false) let fetchedFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: true) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, (Data, String)?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData == nil ? nil : (loadedData!, maybeData.path), true)) } else { let thumbnail: Signal if onlyFullSize { thumbnail = .single(nil) } else if let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { thumbnail = .single(decodedThumbnailData) } else if let thumbnailResource = thumbnailResource { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedMediaResource(postbox: postbox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: .video).start() let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { thumbnail = .single(nil) } let fullSizeDataAndPath = Signal { subscriber in let dataDisposable = fetchedFullSize.start(next: { next in subscriber.putNext(next) }, completed: { subscriber.putCompletion() }) //let fetchedDisposable = fetchedPartialVideoThumbnailData(postbox: postbox, fileReference: fileReference).start() return ActionDisposable { dataDisposable.dispose() //fetchedDisposable.dispose() } } |> map { next -> ((Data, String)?, Bool) in let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) return (data == nil ? nil : (data!, next.path), next.complete) } return thumbnail |> mapToSignal { thumbnailData in return fullSizeDataAndPath |> map { (dataAndPath, complete) in return (thumbnailData, dataAndPath, complete) } } } } |> filter({ if onlyFullSize { return $0.1 != nil || $0.2 } else { return true//$0.0 != nil || $0.1 != nil || $0.2 } }) return signal } private enum Corner: Hashable { case TopLeft(Int), TopRight(Int), BottomLeft(Int), BottomRight(Int) var hashValue: Int { switch self { case let .TopLeft(radius): return radius | (1 << 24) case let .TopRight(radius): return radius | (2 << 24) case let .BottomLeft(radius): return radius | (3 << 24) case let .BottomRight(radius): return radius | (4 << 24) } } var radius: Int { switch self { case let .TopLeft(radius): return radius case let .TopRight(radius): return radius case let .BottomLeft(radius): return radius case let .BottomRight(radius): return radius } } } private func ==(lhs: Corner, rhs: Corner) -> Bool { switch lhs { case let .TopLeft(lhsRadius): switch rhs { case let .TopLeft(rhsRadius) where rhsRadius == lhsRadius: return true default: return false } case let .TopRight(lhsRadius): switch rhs { case let .TopRight(rhsRadius) where rhsRadius == lhsRadius: return true default: return false } case let .BottomLeft(lhsRadius): switch rhs { case let .BottomLeft(rhsRadius) where rhsRadius == lhsRadius: return true default: return false } case let .BottomRight(lhsRadius): switch rhs { case let .BottomRight(rhsRadius) where rhsRadius == lhsRadius: return true default: return false } } } private enum Tail: Hashable { case BottomLeft(Int) case BottomRight(Int) var hashValue: Int { switch self { case let .BottomLeft(radius): return radius | (1 << 24) case let .BottomRight(radius): return radius | (2 << 24) } } var radius: Int { switch self { case let .BottomLeft(radius): return radius case let .BottomRight(radius): return radius } } } private func ==(lhs: Tail, rhs: Tail) -> Bool { switch lhs { case let .BottomLeft(lhsRadius): switch rhs { case let .BottomLeft(rhsRadius) where rhsRadius == lhsRadius: return true default: return false } case let .BottomRight(lhsRadius): switch rhs { case let .BottomRight(rhsRadius) where rhsRadius == lhsRadius: return true default: return false } } } private var cachedCorners = Atomic<[Corner: DrawingContext]>(value: [:]) private var cachedTails = Atomic<[Tail: DrawingContext]>(value: [:]) private func cornerContext(_ corner: Corner) -> DrawingContext { let cached: DrawingContext? = cachedCorners.with { return $0[corner] } if let cached = cached { return cached } else { let context = DrawingContext(size: CGSize(width: CGFloat(corner.radius), height: CGFloat(corner.radius)), clear: true) context.withContext { c in c.setBlendMode(.copy) c.setFillColor(UIColor.black.cgColor) let rect: CGRect switch corner { case let .TopLeft(radius): rect = CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) case let .TopRight(radius): rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: 0.0), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) case let .BottomLeft(radius): rect = CGRect(origin: CGPoint(x: 0.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) case let .BottomRight(radius): rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) } c.fillEllipse(in: rect) } let _ = cachedCorners.modify { current in var current = current current[corner] = context return current } return context } } private func tailContext(_ tail: Tail) -> DrawingContext { let cached: DrawingContext? = cachedTails.with { return $0[tail] } if let cached = cached { return cached } else { let context = DrawingContext(size: CGSize(width: CGFloat(tail.radius) + 3.0, height: CGFloat(tail.radius)), clear: true) context.withContext { c in c.setBlendMode(.copy) c.setFillColor(UIColor.black.cgColor) let rect: CGRect switch tail { case let .BottomLeft(radius): rect = CGRect(origin: CGPoint(x: 3.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) c.move(to: CGPoint(x: 3.0, y: 1.0)) c.addLine(to: CGPoint(x: 3.0, y: 11.0)) c.addLine(to: CGPoint(x: 2.3, y: 13.0)) c.addLine(to: CGPoint(x: 0.0, y: 16.6)) c.addLine(to: CGPoint(x: 4.5, y: 15.5)) c.addLine(to: CGPoint(x: 6.5, y: 14.3)) c.addLine(to: CGPoint(x: 9.0, y: 12.5)) c.closePath() c.fillPath() case let .BottomRight(radius): rect = CGRect(origin: CGPoint(x: 3.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) c.translateBy(x: context.size.width / 2.0, y: context.size.height / 2.0) c.scaleBy(x: -1.0, y: 1.0) c.translateBy(x: -context.size.width / 2.0, y: -context.size.height / 2.0) c.move(to: CGPoint(x: 3.0, y: 1.0)) c.addLine(to: CGPoint(x: 3.0, y: 11.0)) c.addLine(to: CGPoint(x: 2.3, y: 13.0)) c.addLine(to: CGPoint(x: 0.0, y: 16.6)) c.addLine(to: CGPoint(x: 4.5, y: 15.5)) c.addLine(to: CGPoint(x: 6.5, y: 14.3)) c.addLine(to: CGPoint(x: 9.0, y: 12.5)) c.closePath() c.fillPath() } c.fillEllipse(in: rect) } let _ = cachedTails.modify { current in var current = current current[tail] = context return current } return context } } private func addCorners(_ context: DrawingContext, arguments: TransformImageArguments) { let corners = arguments.corners let drawingRect = arguments.drawingRect if case let .Corner(radius) = corners.topLeft, radius > CGFloat.ulpOfOne { let corner = cornerContext(.TopLeft(Int(radius))) context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.minY)) } if case let .Corner(radius) = corners.topRight, radius > CGFloat.ulpOfOne { let corner = cornerContext(.TopRight(Int(radius))) context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.minY)) } switch corners.bottomLeft { case let .Corner(radius): if radius > CGFloat.ulpOfOne { let corner = cornerContext(.BottomLeft(Int(radius))) context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius)) } case let .Tail(radius, enabled): if radius > CGFloat.ulpOfOne { if enabled { let tail = tailContext(.BottomLeft(Int(radius))) let color = context.colorAt(CGPoint(x: drawingRect.minX, y: drawingRect.maxY - 1.0)) context.withContext { c in c.clear(CGRect(x: drawingRect.minX - 3.0, y: 0.0, width: 3.0, height: drawingRect.maxY - 6.0)) c.setFillColor(color.cgColor) c.fill(CGRect(x: 0.0, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) } context.blt(tail, at: CGPoint(x: drawingRect.minX - 3.0, y: drawingRect.maxY - radius)) } else { let corner = cornerContext(.BottomLeft(Int(radius))) context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius)) } } } switch corners.bottomRight { case let .Corner(radius): if radius > CGFloat.ulpOfOne { let corner = cornerContext(.BottomRight(Int(radius))) context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) } case let .Tail(radius, enabled): if radius > CGFloat.ulpOfOne { if enabled { let tail = tailContext(.BottomRight(Int(radius))) let color = context.colorAt(CGPoint(x: drawingRect.maxX - 1.0, y: drawingRect.maxY - 1.0)) context.withContext { c in c.clear(CGRect(x: drawingRect.maxX, y: 0.0, width: 3.0, height: drawingRect.maxY - 6.0)) c.setFillColor(color.cgColor) c.fill(CGRect(x: drawingRect.maxX, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) } context.blt(tail, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) } else { let corner = cornerContext(.BottomRight(Int(radius))) context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) } } } } func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> Signal { return chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, autoFetchFullSize: true) |> map { (thumbnailData, fullSizeData, fullSizeComplete) -> UIImage? in if let fullSizeData = fullSizeData { if fullSizeComplete { return UIImage(data: fullSizeData)?.precomposed() } } if let thumbnailData = thumbnailData { return UIImage(data: thumbnailData)?.precomposed() } return nil } } public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false, tinyThumbnailData: TinyThumbnailData? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad, tinyThumbnailData: tinyThumbnailData) |> map { _, generate in return generate } } public func chatMessagePhotoInternal(photoData: Signal<(Data?, Data?, Bool), NoError>, synchronousLoad: Bool = false, tinyThumbnailData: TinyThumbnailData? = nil) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { return photoData |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return ({ return nil }, { arguments in let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { if fullSizeComplete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image } if GlobalExperimentalSettings.forceTinyThumbnailsPreview { if let fullSizeImageValue = fullSizeImage { if let data = compressTinyThumbnail(UIImage(cgImage: fullSizeImageValue)) { if let result = decompressTinyThumbnail(data: data) { fullSizeImage = nil thumbnailImage = result.cgImage } } } } else if let tinyThumbnailData = tinyThumbnailData { if let result = decompressTinyThumbnail(data: tinyThumbnailData) { thumbnailImage = result.cgImage } } var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 90.0, height: 90.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) } if thumbnailContextFittingSize.width > thumbnailContextSize.width { let additionalContextSize = thumbnailContextFittingSize let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) additionalBlurContext.withFlippedContext { c in c.interpolationQuality = .default if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) } } telegramFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) blurredThumbnailImage = additionalBlurContext.generateImage() } else { blurredThumbnailImage = thumbnailContext.generateImage() } } if let blurredThumbnailImage = blurredThumbnailImage, fullSizeImage == nil, arguments.corners.isEmpty { let context = DrawingContext(size: blurredThumbnailImage.size, scale: blurredThumbnailImage.scale, clear: true) context.withFlippedContext { c in c.setBlendMode(.copy) if let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .none drawImage(context: c, image: cgImage, orientation: imageOrientation, in: CGRect(origin: CGPoint(), size: blurredThumbnailImage.size)) c.setBlendMode(.normal) } } return context } let context = DrawingContext(size: arguments.drawingSize, clear: true) context.withFlippedContext { c in c.setBlendMode(.copy) if thumbnailImage == nil && fullSizeImage == nil { let color = arguments.emptyColor ?? UIColor.white c.setFillColor(color.cgColor) c.fill(drawingRect) } else { if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { let blurSourceImage = thumbnailImage ?? fullSizeImage if let fullSizeImage = blurSourceImage { let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) var sideBlurredImage: UIImage? if true { let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) } if thumbnailContextFittingSize.width > thumbnailContextSize.width { let additionalContextSize = thumbnailContextFittingSize let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) additionalBlurContext.withFlippedContext { c in c.interpolationQuality = .default if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) } } telegramFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) sideBlurredImage = additionalBlurContext.generateImage() } else { sideBlurredImage = thumbnailContext.generateImage() } } else { let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) sideBlurredImage = thumbnailContext.generateImage() } if let blurredImage = sideBlurredImage { let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) c.interpolationQuality = .medium c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x:arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) c.setBlendMode(.normal) c.setFillColor((arguments.emptyColor ?? UIColor.white).withAlphaComponent(0.05).cgColor) c.fill(arguments.drawingRect) c.setBlendMode(.copy) } } else { c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) c.fill(arguments.drawingRect) } } c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } } addCorners(context, arguments: arguments) return context }) } } private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: ImageMediaReference, onlyFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(fullRepresentationSize) { let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: onlyFullSize, fetch: false) let fetchedFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: onlyFullSize, fetch: true) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { let fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } let fullSizeData: Signal<(Data?, Bool), NoError> = fetchedFullSize |> map { next -> (Data?, Bool) in return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { (fullSizeData, complete) in return (thumbnailData, fullSizeData, complete) } } } } |> filter({ $0.0 != nil || $0.1 != nil }) return signal } else { return .never() } } public func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMediaReference, onlyFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessagePhotoThumbnailDatas(account: account, photoReference: photoReference, onlyFullSize: onlyFullSize) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { if fullSizeComplete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image } var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) c.fill(arguments.drawingRect) } c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } addCorners(context, arguments: arguments) return context } } } public func chatMessageVideoThumbnail(account: Account, fileReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessageVideoDatas(postbox: account.postbox, fileReference: fileReference, thumbnailSize: true) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } if arguments.intrinsicInsets != UIEdgeInsets.zero { fittedSize.width -= arguments.intrinsicInsets.left + arguments.intrinsicInsets.right fittedSize.height -= arguments.intrinsicInsets.top + arguments.intrinsicInsets.bottom } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData?.0 { if fullSizeComplete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image } var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) c.fill(arguments.drawingRect) } c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } addCorners(context, arguments: arguments) return context } } } func chatSecretPhoto(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var blurredImage: UIImage? if let fullSizeData = fullSizeData { if fullSizeComplete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { let thumbnailSize = CGSize(width: image.width, height: image.height) let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) thumbnailContext2.withFlippedContext { c in c.interpolationQuality = .none if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size)) } } telegramFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes) blurredImage = thumbnailContext2.generateImage() } } } if blurredImage == nil { if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { let thumbnailSize = CGSize(width: image.width, height: image.height) let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) thumbnailContext2.withFlippedContext { c in c.interpolationQuality = .none if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size)) } } telegramFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes) blurredImage = thumbnailContext2.generateImage() } } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) c.fill(arguments.drawingRect) } c.setBlendMode(.copy) if let blurredImage = blurredImage, let cgImage = blurredImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect) } if !arguments.insets.left.isEqual(to: 0.0) { c.clear(CGRect(origin: CGPoint(), size: CGSize(width: arguments.insets.left, height: context.size.height))) } if !arguments.insets.right.isEqual(to: 0.0) { c.clear(CGRect(origin: CGPoint(x: context.size.width - arguments.insets.right, y: 0.0), size: CGSize(width: arguments.insets.right, height: context.size.height))) } } addCorners(context, arguments: arguments) return context } } } private func avatarGalleryThumbnailDatas(postbox: Postbox, representations: [ImageRepresentationWithReference], fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = imageRepresentationLargerThan(representations.map({ $0.representation }), size: fullRepresentationSize), let smallestIndex = representations.index(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.index(where: { $0.representation == largestRepresentation }) { let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { let fetchedThumbnail = fetchedMediaResource(postbox: postbox, reference: representations[smallestIndex].reference, statsCategory: .image) let fetchedFullSize = fetchedMediaResource(postbox: postbox, reference: representations[largestIndex].reference, statsCategory: .image) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } let fullSizeData: Signal<(Data?, Bool), NoError> if autoFetchFullSize { fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedFullSizeDisposable.dispose() fullSizeDisposable.dispose() } } } else { fullSizeData = postbox.mediaBox.resourceData(largestRepresentation.resource) |> map { next -> (Data?, Bool) in return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { (fullSizeData, complete) in return (thumbnailData, fullSizeData, complete) } } } } |> distinctUntilChanged(isEqual: { lhs, rhs in if (lhs.0 == nil && lhs.1 == nil) && (rhs.0 == nil && rhs.1 == nil) { return true } else { return false } }) return signal } else { return .never() } } func avatarGalleryThumbnailPhoto(account: Account, representations: [ImageRepresentationWithReference]) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = avatarGalleryThumbnailDatas(postbox: account.postbox, representations: representations, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { if fullSizeComplete { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image } var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { c.fill(arguments.drawingRect) } c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } addCorners(context, arguments: arguments) return context } } } func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true, synchronousLoad: synchronousLoad) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { if fullSizeComplete { let options = NSMutableDictionary() options.setValue(400 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image } var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 90.0, height: 90.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) c.fill(arguments.drawingRect) } c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } addCorners(context, arguments: arguments) return context } } } func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { if let smallestRepresentation = smallestImageRepresentation(videoReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource let thumbnail = Signal { subscriber in let data = account.postbox.mediaBox.resourceData(thumbnailResource).start(next: { data in subscriber.putNext(data) }, completed: { subscriber.putCompletion() }) let fetched = fetchedMediaResource(postbox: account.postbox, reference: videoReference.resourceReference(thumbnailResource)).start() return ActionDisposable { data.dispose() fetched.dispose() } } return thumbnail |> map { data in let thumbnailData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image } var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { c.fill(arguments.drawingRect) } c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect) c.setBlendMode(.normal) } } addCorners(context, arguments: arguments) return context } } } else { return .never() } } func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize) |> map { return $0.1 } } func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal: Signal<(Data?, (Data, String)?, Bool), NoError> if let imageReference = imageReference { signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true) |> map { (thumbnailData, fullSizeData, fullSizeComplete) -> (Data?, (Data, String)?, Bool) in return (thumbnailData, fullSizeData.flatMap({ ($0, "") }), fullSizeComplete) } } else { signal = chatMessageVideoDatas(postbox: postbox, fileReference: videoReference, onlyFullSize: onlyFullSize) } return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return ({ var fullSizeImage: CGImage? if let fullSizeData = fullSizeData { if fullSizeComplete { let options = NSMutableDictionary() if let imageSource = CGImageSourceCreateWithData(fullSizeData.0 as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { fullSizeImage = image } } } if let fullSizeImage = fullSizeImage { return CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)) } return nil }, { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) if fittedSize.width < drawingRect.size.width && fittedSize.width >= drawingRect.size.width - 2.0 { fittedSize.width = drawingRect.size.width } if fittedSize.height < drawingRect.size.height && fittedSize.height >= drawingRect.size.height - 2.0 { fittedSize.height = drawingRect.size.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { if fullSizeComplete { let options = NSMutableDictionary() if let imageSource = CGImageSourceCreateWithData(fullSizeData.0 as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) CGImageSourceUpdateData(imageSource, fullSizeData.0 as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image } var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) } if thumbnailContextFittingSize.width > thumbnailContextSize.width { let additionalContextSize = thumbnailContextFittingSize let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) additionalBlurContext.withFlippedContext { c in c.interpolationQuality = .default if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) } } telegramFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) blurredThumbnailImage = additionalBlurContext.generateImage() } else { blurredThumbnailImage = thumbnailContext.generateImage() } } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { switch arguments.resizeMode { case .blurBackground: let blurSourceImage = thumbnailImage ?? fullSizeImage if let fullSizeImage = blurSourceImage { var sideBlurredImage: UIImage? let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) if true { let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) } if thumbnailContextFittingSize.width > thumbnailContextSize.width { let additionalContextSize = thumbnailContextFittingSize let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) additionalBlurContext.withFlippedContext { c in c.interpolationQuality = .default if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) } } telegramFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) sideBlurredImage = additionalBlurContext.generateImage() } else { sideBlurredImage = thumbnailContext.generateImage() } } else { let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) sideBlurredImage = thumbnailContext.generateImage() } if let blurredImage = sideBlurredImage { let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) c.interpolationQuality = .medium c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x: arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) c.setBlendMode(.normal) c.setFillColor((arguments.emptyColor ?? UIColor.white).withAlphaComponent(0.05).cgColor) c.fill(arguments.drawingRect) c.setBlendMode(.copy) } } else { c.fill(arguments.drawingRect) } case let .fill(color): c.setFillColor((arguments.emptyColor ?? color).cgColor) c.fill(arguments.drawingRect) } } c.setBlendMode(.copy) if blurredThumbnailImage == nil, fullSizeImage == nil, let emptyColor = arguments.emptyColor { c.setFillColor(emptyColor.cgColor) c.fill(arguments.drawingRect) } if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .default drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } addCorners(context, arguments: arguments) return context }) } } func chatMessagePhotoStatus(context: AccountContext, messageId: MessageId, photoReference: ImageMediaReference) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: largestRepresentation.resource) } else { return .never() } } public func standaloneChatMessagePhotoInteractiveFetched(account: Account, photoReference: ImageMediaReference) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return fetchedMediaResource(postbox: account.postbox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image, reportResultStatus: true) |> mapToSignal { type -> Signal in return .single(type) } } else { return .never() } } public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoReference: ImageMediaReference, storeToDownloadsPeerType: AutomaticMediaDownloadPeerType?) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return fetchedMediaResource(postbox: context.account.postbox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image, reportResultStatus: true) |> mapToSignal { type -> Signal in if case .remote = type, let peerType = storeToDownloadsPeerType { return storeDownloadedMedia(storeManager: context.mediaManager.downloadedMediaStoreManager, media: photoReference.abstract, peerType: peerType) |> introduceError(FetchResourceError.self) |> mapToSignal { _ -> Signal in return .complete() } |> then(.single(type)) } return .single(type) } } else { return .never() } } func chatMessagePhotoCancelInteractiveFetch(account: Account, photoReference: ImageMediaReference) { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return account.postbox.mediaBox.cancelInteractiveResourceFetch(largestRepresentation.resource) } } func chatMessageWebFileInteractiveFetched(account: Account, image: TelegramMediaWebFile) -> Signal { return fetchedMediaResource(postbox: account.postbox, reference: .standalone(resource: image.resource), statsCategory: .image) } func chatMessageWebFileCancelInteractiveFetch(account: Account, image: TelegramMediaWebFile) { return account.postbox.mediaBox.cancelInteractiveResourceFetch(image.resource) } func chatWebpageSnippetFileData(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal { let resourceData = account.postbox.mediaBox.resourceData(resource) |> map { next in return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) } return Signal { subscriber in let disposable = DisposableSet() disposable.add(resourceData.start(next: { data in subscriber.putNext(data) }, error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() })) disposable.add(fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(resource)).start()) return disposable } } func chatWebpageSnippetPhotoData(account: Account, photoReference: ImageMediaReference) -> Signal { if let closestRepresentation = photoReference.media.representationForDisplayAtSize(CGSize(width: 120.0, height: 120.0)) { let resourceData = account.postbox.mediaBox.resourceData(closestRepresentation.resource) |> map { next in return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) } return Signal { subscriber in let disposable = DisposableSet() disposable.add(resourceData.start(next: { data in subscriber.putNext(data) }, error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() })) disposable.add(fetchedMediaResource(postbox: account.postbox, reference: photoReference.resourceReference(closestRepresentation.resource)).start()) return disposable } } else { return .never() } } public func chatWebpageSnippetFile(account: Account, fileReference: FileMediaReference, representation: TelegramMediaImageRepresentation) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatWebpageSnippetFileData(account: account, fileReference: fileReference, resource: representation.resource) return signal |> map { fullSizeData in return { arguments in var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { let options = NSMutableDictionary() if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } if let fullSizeImage = fullSizeImage { let context = DrawingContext(size: arguments.drawingSize, clear: true) let fittedSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height).aspectFilled(arguments.boundingSize) let drawingRect = arguments.drawingRect let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize.width > arguments.imageSize.width || arguments.boundingSize.height > arguments.imageSize.height { c.fill(arguments.drawingRect) } c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } addCorners(context, arguments: arguments) return context } else { return nil } } } } func chatWebpageSnippetPhoto(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatWebpageSnippetPhotoData(account: account, photoReference: photoReference) return signal |> map { fullSizeData in return { arguments in var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { let options = NSMutableDictionary() if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } if let fullSizeImage = fullSizeImage { let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) let fittedSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height).aspectFilled(arguments.boundingSize) let drawingRect = arguments.drawingRect let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize.width > arguments.imageSize.width || arguments.boundingSize.height > arguments.imageSize.height { c.fill(arguments.drawingRect) } c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } addCorners(context, arguments: arguments) return context } else { return nil } } } } func chatMessageVideo(postbox: Postbox, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return mediaGridMessageVideo(postbox: postbox, videoReference: videoReference) } private func chatSecretMessageVideoData(account: Account, fileReference: FileMediaReference) -> Signal { if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource let fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(thumbnailResource)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } return thumbnail } else { return .single(nil) } } func chatSecretMessageVideo(account: Account, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatSecretMessageVideoData(account: account, fileReference: videoReference) return signal |> map { thumbnailData in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) if arguments.drawingSize.width.isLessThanOrEqualTo(0.0) || arguments.drawingSize.height.isLessThanOrEqualTo(0.0) { return context } let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var blurredImage: UIImage? if blurredImage == nil { if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { let thumbnailSize = CGSize(width: image.width, height: image.height) let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) thumbnailContext2.withFlippedContext { c in c.interpolationQuality = .none if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size)) } } telegramFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes) blurredImage = thumbnailContext2.generateImage() } } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { c.fill(arguments.drawingRect) } c.setBlendMode(.copy) if let blurredImage = blurredImage, let cgImage = blurredImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect) } if !arguments.insets.left.isEqual(to: 0.0) { c.clear(CGRect(origin: CGPoint(), size: CGSize(width: arguments.insets.left, height: context.size.height))) } if !arguments.insets.right.isEqual(to: 0.0) { c.clear(CGRect(origin: CGPoint(x: context.size.width - arguments.insets.right, y: 0.0), size: CGSize(width: arguments.insets.right, height: context.size.height))) } } addCorners(context, arguments: arguments) return context } } } private func orientationFromExif(orientation: Int) -> UIImageOrientation { switch orientation { case 1: return .up; case 3: return .down; case 8: return .left; case 6: return .right; case 2: return .upMirrored; case 4: return .downMirrored; case 5: return .leftMirrored; case 7: return .rightMirrored; default: return .up } } func imageOrientationFromSource(_ source: CGImageSource) -> UIImageOrientation { if let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) { let dict = properties as NSDictionary if let value = dict.object(forKey: kCGImagePropertyOrientation) as? NSNumber { return orientationFromExif(orientation: value.intValue) } } return .up } private func rotationFor(_ orientation: UIImageOrientation) -> CGFloat { switch orientation { case .left: return CGFloat.pi / 2.0 case .right: return -CGFloat.pi / 2.0 case .down: return -CGFloat.pi default: return 0.0 } } func drawImage(context: CGContext, image: CGImage, orientation: UIImageOrientation, in rect: CGRect) { var restore = true var drawRect = rect switch orientation { case .left: fallthrough case .right: fallthrough case .down: let angle = rotationFor(orientation) context.saveGState() context.translateBy(x: rect.midX, y: rect.midY) context.rotate(by: angle) context.translateBy(x: -rect.midX, y: -rect.midY) var t = CGAffineTransform(translationX: rect.midX, y: rect.midY) t = t.rotated(by: angle) t = t.translatedBy(x: -rect.midX, y: -rect.midY) drawRect = rect.applying(t) case .leftMirrored: context.saveGState() context.translateBy(x: rect.midX, y: rect.midY) context.rotate(by: -CGFloat.pi / 2.0) context.translateBy(x: -rect.midX, y: -rect.midY) var t = CGAffineTransform(translationX: rect.midX, y: rect.midY) t = t.rotated(by: -CGFloat.pi / 2.0) t = t.translatedBy(x: -rect.midX, y: -rect.midY) drawRect = rect.applying(t) default: restore = false } context.draw(image, in: drawRect) if restore { context.restoreGState() } } func chatMessageImageFile(account: Account, fileReference: FileMediaReference, thumbnail: Bool, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal<(Data?, String?, Bool), NoError> if thumbnail { signal = chatMessageImageFileThumbnailDatas(account: account, fileReference: fileReference) } else { signal = chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false, fetched: fetched) } return signal |> map { (thumbnailData, fullSizePath, fullSizeComplete) in return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: arguments.emptyColor == nil) let drawingRect = arguments.drawingRect var fittedSize: CGSize if thumbnail { fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize) } else { fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) } var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizePath = fullSizePath { if fullSizeComplete { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) if let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: fullSizePath) as CFURL, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image if thumbnail { fittedSize = CGSize(width: CGFloat(image.width), height: CGFloat(image.height)).aspectFilled(arguments.boundingSize) } } } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { if fullSizeImage == nil { imageOrientation = imageOrientationFromSource(imageSource) } thumbnailImage = image if thumbnail { fittedSize = CGSize(width: CGFloat(image.width), height: CGFloat(image.height)).aspectFilled(arguments.boundingSize) } } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) } if thumbnailContextFittingSize.width > thumbnailContextSize.width { let additionalContextSize = thumbnailContextFittingSize let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) additionalBlurContext.withFlippedContext { c in c.interpolationQuality = .default if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) } } telegramFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) blurredThumbnailImage = additionalBlurContext.generateImage() } else { blurredThumbnailImage = thumbnailContext.generateImage() } } context.withFlippedContext { c in if let emptyColor = arguments.emptyColor { c.setFillColor(emptyColor.cgColor) c.fill(drawingRect) } c.setBlendMode(.copy) if arguments.boundingSize != fittedSize && !fetched { c.fill(drawingRect) } c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .low drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) } if let fullSizeImage = fullSizeImage { c.setBlendMode(.normal) c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } addCorners(context, arguments: arguments) return context } } } func instantPageImageFile(account: Account, fileReference: FileMediaReference, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false, fetched: fetched) |> map { (thumbnailData, fullSizePath, fullSizeComplete) in return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizePath = fullSizePath { if fullSizeComplete { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) if let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: fullSizePath) as CFURL, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) context.withFlippedContext { c in if var fullSizeImage = fullSizeImage { if let color = arguments.emptyColor, imageRequiresInversion(fullSizeImage), let tintedImage = generateTintedImage(image: UIImage(cgImage: fullSizeImage), color: color)?.cgImage { fullSizeImage = tintedImage } c.setBlendMode(.normal) c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } addCorners(context, arguments: arguments) return context } } } private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaReference? = nil, representations: [ImageRepresentationWithReference], alwaysShowThumbnailFirst: Bool = false, autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.representation })), let smallestIndex = representations.index(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.index(where: { $0.representation == largestRepresentation }) { let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource) let decodedThumbnailData = fileReference?.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) if alwaysShowThumbnailFirst, let decodedThumbnailData = decodedThumbnailData { return .single((decodedThumbnailData, nil, false)) |> then(.single((nil, loadedData, true))) } else { return .single((nil, loadedData, true)) } } else { let fetchedThumbnail: Signal if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: representations[smallestIndex].reference) } let fetchedFullSize = fetchedMediaResource(postbox: account.postbox, reference: representations[largestIndex].reference) let thumbnail: Signal if let decodedThumbnailData = decodedThumbnailData { thumbnail = .single(decodedThumbnailData) } else { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } let fullSizeData: Signal<(Data?, Bool), NoError> if autoFetchFullSize { fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedFullSizeDisposable.dispose() fullSizeDisposable.dispose() } } } else { fullSizeData = account.postbox.mediaBox.resourceData(largestRepresentation.resource) |> map { next -> (Data?, Bool) in return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { (fullSizeData, complete) in return (thumbnailData, fullSizeData, complete) } } } } |> filter({ $0.0 != nil || $0.1 != nil }) return signal } else { return .never() } } func chatAvatarGalleryPhoto(account: Account, fileReference: FileMediaReference? = nil, representations: [ImageRepresentationWithReference], alwaysShowThumbnailFirst: Bool = false, autoFetchFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = avatarGalleryPhotoDatas(account: account, fileReference: fileReference, representations: representations, alwaysShowThumbnailFirst: alwaysShowThumbnailFirst, autoFetchFullSize: autoFetchFullSize) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { if fullSizeComplete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image } var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 90.0, height: 90.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) } if false, thumbnailContextFittingSize.width > thumbnailContextSize.width { let additionalContextSize = thumbnailContextFittingSize let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) additionalBlurContext.withFlippedContext { c in c.interpolationQuality = .default if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) } } telegramFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) blurredThumbnailImage = additionalBlurContext.generateImage() } else { blurredThumbnailImage = thumbnailContext.generateImage() } } if let blurredThumbnailImage = blurredThumbnailImage, fullSizeImage == nil { let context = DrawingContext(size: blurredThumbnailImage.size, scale: blurredThumbnailImage.scale, clear: true) context.withFlippedContext { c in c.setBlendMode(.copy) if let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .none drawImage(context: c, image: cgImage, orientation: imageOrientation, in: CGRect(origin: CGPoint(), size: blurredThumbnailImage.size)) c.setBlendMode(.normal) } } return context } let context = DrawingContext(size: arguments.drawingSize, clear: true) context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { c.fill(arguments.drawingRect) } c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .medium drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } addCorners(context, arguments: arguments) return context } } } private func builtinWallpaperData() -> Signal { return Signal { subscriber in if let filePath = frameworkBundle.path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg"), let image = UIImage(contentsOfFile: filePath) { subscriber.putNext(image) } subscriber.putCompletion() return EmptyDisposable } |> runOn(Queue.concurrentDefaultQueue()) } func settingsBuiltinWallpaperImage(account: Account) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return builtinWallpaperData() |> map { fullSizeImage in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect var fittedSize = fullSizeImage.size.aspectFilled(drawingRect.size) if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) context.withFlippedContext { c in c.setBlendMode(.copy) if let fullSizeImage = fullSizeImage.cgImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: .up, in: fittedRect) } } addCorners(context, arguments: arguments) return context } } } func chatMapSnapshotData(account: Account, resource: MapSnapshotMediaResource) -> Signal { return Signal { subscriber in let fetchedDisposable = account.postbox.mediaBox.fetchedResource(resource, parameters: nil).start() let dataDisposable = account.postbox.mediaBox.resourceData(resource).start(next: { next in if next.size != 0 { subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) } }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() dataDisposable.dispose() } } } private let locationPinImage = UIImage(named: "ModernMessageLocationPin")?.precomposed() func chatMapSnapshotImage(account: Account, resource: MapSnapshotMediaResource) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMapSnapshotData(account: account, resource: resource) return signal |> map { fullSizeData in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } if let fullSizeImage = fullSizeImage { let drawingRect = arguments.drawingRect var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { c.fill(arguments.drawingRect) } c.setBlendMode(.copy) c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) if let locationPinImage = locationPinImage { c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) } } } else { context.withFlippedContext { c in c.setBlendMode(.copy) c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) c.fill(arguments.drawingRect) c.setBlendMode(.normal) if let locationPinImage = locationPinImage { c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) } } } } addCorners(context, arguments: arguments) return context } } } func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return account.postbox.mediaBox.resourceData(file.resource) |> map { fullSizeData in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if fullSizeData.complete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: fullSizeData.path) as CFURL, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } if let fullSizeImage = fullSizeImage { let drawingRect = arguments.drawingRect var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { c.fill(arguments.drawingRect) } c.setBlendMode(.copy) c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) c.setBlendMode(.normal) } } else { context.withFlippedContext { c in c.setBlendMode(.copy) c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) c.fill(arguments.drawingRect) c.setBlendMode(.normal) } } } addCorners(context, arguments: arguments) return context } } } private let precomposedSmallAlbumArt = Atomic(value: nil) private func albumArtThumbnailData(postbox: Postbox, thumbnail: MediaResource) -> Signal<(Data?), NoError> { let thumbnailResource = postbox.mediaBox.resourceData(thumbnail) let signal = thumbnailResource |> take(1) |> mapToSignal { maybeData -> Signal<(Data?), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((loadedData)) } else { let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnail, parameters: nil) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = thumbnailResource.start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } return thumbnail } } |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs == nil && rhs == nil { return true } else { return false } }) return signal } private func albumArtFullSizeDatas(postbox: Postbox, thumbnail: MediaResource, fullSize: MediaResource, autoFetchFullSize: Bool = true) -> Signal<(Data?, Data?, Bool), NoError> { let fullSizeResource = postbox.mediaBox.resourceData(fullSize) let thumbnailResource = postbox.mediaBox.resourceData(thumbnail) let signal = fullSizeResource |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnail, parameters: nil) let fetchedFullSize = postbox.mediaBox.fetchedResource(fullSize, parameters: nil) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = thumbnailResource.start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } let fullSizeData: Signal<(Data?, Bool), NoError> if autoFetchFullSize { fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = fullSizeResource.start(next: { next in subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedFullSizeDisposable.dispose() fullSizeDisposable.dispose() } } } else { fullSizeData = fullSizeResource |> map { next -> (Data?, Bool) in return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { (fullSizeData, complete) in return (thumbnailData, fullSizeData, complete) } } } } |> distinctUntilChanged(isEqual: { lhs, rhs in if (lhs.0 == nil && lhs.1 == nil) && (rhs.0 == nil && rhs.1 == nil) { return true } else { return false } }) return signal } private func drawAlbumArtPlaceholder(into c: CGContext, arguments: TransformImageArguments, thumbnail: Bool) { c.setBlendMode(.copy) c.setFillColor(UIColor(rgb: 0xeeeeee).cgColor) c.fill(arguments.drawingRect) c.setBlendMode(.normal) if thumbnail { var image: UIImage? let precomposed = precomposedSmallAlbumArt.with { $0 } if let precomposed = precomposed { image = precomposed } else { if let sourceImage = UIImage(bundleImageName: "GlobalMusicPlayer/AlbumArtPlaceholder"), let cgImage = sourceImage.cgImage { let fittedSize = sourceImage.size.aspectFitted(CGSize(width: 28.0, height: 28.0)) image = generateImage(fittedSize, contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) }) if let image = image { let _ = precomposedSmallAlbumArt.swap(image) } } } if let image = image, let cgImage = image.cgImage { c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor(arguments.drawingRect.size.width - image.size.width) / 2.0, y: floor(arguments.drawingRect.size.height - image.size.height) / 2.0), size: image.size)) } } else { if let sourceImage = UIImage(bundleImageName: "GlobalMusicPlayer/AlbumArtPlaceholder"), let cgImage = sourceImage.cgImage { let fittedSize = sourceImage.size.aspectFitted(CGSize(width: floor(arguments.drawingRect.size.width * 0.66), height: floor(arguments.drawingRect.size.width * 0.66))) c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor(arguments.drawingRect.size.width - fittedSize.width) / 2.0, y: floor(arguments.drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)) } } } func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?, albumArt: SharedMediaPlaybackAlbumArt?, thumbnail: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { var fileArtworkData: Signal = .single(nil) if let fileReference = fileReference, let size = fileReference.media.resource.size { fileArtworkData = fileArtworkData |> then(postbox.mediaBox.resourceData(fileReference.media.resource, size: size, in: 0 ..< min(size, 1024 * 384)) |> mapToSignal { data -> Signal in return .single(albumArtworkData(data)) }) } var immediateArtworkData: Signal<(Data?, Data?, Bool), NoError> = .single((nil, nil, false)) if let fileReference = fileReference, let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource let fetchedThumbnail = fetchedMediaResource(postbox: postbox, reference: fileReference.resourceReference(thumbnailResource)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } immediateArtworkData = thumbnail |> map { thumbnailData in return (thumbnailData, nil, false) } } else if let albumArt = albumArt { if thumbnail { immediateArtworkData = albumArtThumbnailData(postbox: postbox, thumbnail: albumArt.thumbnailResource) |> map { thumbnailData in return (thumbnailData, nil, false) } } else { immediateArtworkData = albumArtFullSizeDatas(postbox: postbox, thumbnail: albumArt.thumbnailResource, fullSize: albumArt.fullSizeResource) } } return combineLatest(fileArtworkData, immediateArtworkData) |> map { fileArtworkData, remoteArtworkData in let remoteThumbnailData = remoteArtworkData.0 let remoteFullSizeData = remoteArtworkData.1 let remoteFullSizeComplete = remoteArtworkData.2 return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) var sourceImage: UIImage? if let fileArtworkData = fileArtworkData, let image = UIImage(data: fileArtworkData) { sourceImage = image } else if remoteFullSizeComplete, let fullSizeData = remoteFullSizeData, let image = UIImage(data: fullSizeData) { sourceImage = image } else if let thumbnailData = remoteThumbnailData, let image = UIImage(data: thumbnailData) { sourceImage = image } if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage { let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size) context.withFlippedContext { c in c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize)) } } else { context.withFlippedContext { c in drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) } } addCorners(context, arguments: arguments) return context } } } func photoWallpaper(postbox: Postbox, photoLibraryResource: PhotoLibraryMediaResource) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let thumbnail = fetchPhotoLibraryImage(localIdentifier: photoLibraryResource.localIdentifier, thumbnail: true) let fullSize = fetchPhotoLibraryImage(localIdentifier: photoLibraryResource.localIdentifier, thumbnail: false) return (thumbnail |> then(fullSize)) |> map { result in var sourceImage = result?.0 let isThumbnail = result?.1 ?? false return { arguments in let context = DrawingContext(size: arguments.drawingSize, scale: 1.0, clear: true) let dimensions = sourceImage?.size if let thumbnailImage = sourceImage?.cgImage, isThumbnail { var fittedSize = arguments.imageSize if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.width = arguments.boundingSize.width } if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { fittedSize.height = arguments.boundingSize.height } let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) } if thumbnailContextFittingSize.width > thumbnailContextSize.width { let additionalContextSize = thumbnailContextFittingSize let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) additionalBlurContext.withFlippedContext { c in c.interpolationQuality = .default if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) } } telegramFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) sourceImage = additionalBlurContext.generateImage() } else { sourceImage = thumbnailContext.generateImage() } } context.withFlippedContext { c in c.setBlendMode(.copy) if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage, let dimensions = dimensions { let imageSize = dimensions.aspectFilled(arguments.drawingRect.size) let fittedRect = CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize) c.draw(cgImage, in: fittedRect) } } return context } } } func securePhoto(account: Account, resource: TelegramMediaResource, accessContext: SecureIdAccessContext) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return securePhotoInternal(account: account, resource: resource, accessContext: accessContext) |> map { $0.1 } } func securePhotoInternal(account: Account, resource: TelegramMediaResource, accessContext: SecureIdAccessContext) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal = Signal { subscriber in let fetched = account.postbox.mediaBox.fetchedResource(resource, parameters: nil).start() let data = account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)).start(next: { next in subscriber.putNext(next) }, completed: { subscriber.putCompletion() }) return ActionDisposable { fetched.dispose() data.dispose() } } |> map { next -> Data? in if next.size == 0 { return nil } else { return decryptedResourceData(data: next, resource: resource, params: accessContext) } } return signal |> map { fullSizeData in return ({ if let fullSizeData = fullSizeData, let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil) { let options = NSMutableDictionary() options.setObject(true as NSNumber, forKey: kCGImagePropertyPixelWidth as NSString) options.setObject(true as NSNumber, forKey: kCGImagePropertyPixelHeight as NSString) if let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, options as CFDictionary) { let dict = properties as NSDictionary if let width = dict.object(forKey: kCGImagePropertyPixelWidth as NSString), let height = dict.object(forKey: kCGImagePropertyPixelHeight as NSString) { if let width = width as? NSNumber, let height = height as? NSNumber { return CGSize(width: CGFloat(width.floatValue), height: CGFloat(height.floatValue)) } } } } return CGSize(width: 128.0, height: 128.0) }, { arguments in var fullSizeImage: CGImage? var imageOrientation: UIImageOrientation = .up if let fullSizeData = fullSizeData { let options = NSMutableDictionary() if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { imageOrientation = imageOrientationFromSource(imageSource) fullSizeImage = image } } if let fullSizeImage = fullSizeImage { let context = DrawingContext(size: arguments.drawingSize, clear: true) let fittedSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height).aspectFilled(arguments.boundingSize) let drawingRect = arguments.drawingRect let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize.width > arguments.imageSize.width || arguments.boundingSize.height > arguments.imageSize.height { c.fill(arguments.drawingRect) } c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } addCorners(context, arguments: arguments) return context } else { return nil } }) } } private func openInAppIconData(postbox: Postbox, appIcon: MediaResource) -> Signal<(Data?), NoError> { let appIconResource = postbox.mediaBox.resourceData(appIcon) let signal = appIconResource |> take(1) |> mapToSignal { maybeData -> Signal<(Data?), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((loadedData)) } else { let fetchedAppIcon = postbox.mediaBox.fetchedResource(appIcon, parameters: nil) let appIcon = Signal { subscriber in let fetchedDisposable = fetchedAppIcon.start() let appIconDisposable = appIconResource.start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() appIconDisposable.dispose() } } return appIcon } } |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs == nil && rhs == nil { return true } else { return false } }) return signal } private func drawOpenInAppIconBorder(into c: CGContext, arguments: TransformImageArguments) { c.setBlendMode(.normal) c.setStrokeColor(UIColor(rgb: 0xeeeeee).cgColor) c.setLineWidth(1.0) var radius: CGFloat = 0.0 if case let .Corner(cornerRadius) = arguments.corners.topLeft, cornerRadius > CGFloat.ulpOfOne { radius = max(0, cornerRadius - 0.5) } let rect = arguments.drawingRect.insetBy(dx: 0.5, dy: 0.5) c.move(to: CGPoint(x: rect.minX, y: rect.midY)) c.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.midX, y: rect.minY), radius: radius) c.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.midY), radius: radius) c.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.midX, y: rect.maxY), radius: radius) c.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY), tangent2End: CGPoint(x: rect.minX, y: rect.midY), radius: radius) c.closePath() c.strokePath() } enum OpenInAppIcon { case resource(resource: TelegramMediaResource) case image(image: UIImage) } func openInAppIcon(postbox: Postbox, appIcon: OpenInAppIcon) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { switch appIcon { case let .resource(resource): return openInAppIconData(postbox: postbox, appIcon: resource) |> map { data in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) var sourceImage: UIImage? if let data = data, let image = UIImage(data: data) { sourceImage = image } if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage { let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size) context.withFlippedContext { c in c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize)) drawOpenInAppIconBorder(into: c, arguments: arguments) } } else { context.withFlippedContext { c in drawOpenInAppIconBorder(into: c, arguments: arguments) } } addCorners(context, arguments: arguments) return context } } case let .image(image): return .single({ arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) context.withFlippedContext { c in c.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: arguments.drawingSize)) drawOpenInAppIconBorder(into: c, arguments: arguments) } addCorners(context, arguments: arguments) return context }) } } func callDefaultBackground() -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return .single({ arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) context.withFlippedContext { c in let colors = [UIColor(rgb: 0x466f92).cgColor, UIColor(rgb: 0x244f74).cgColor] var locations: [CGFloat] = [1.0, 0.0] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! c.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: arguments.drawingSize.height), options: CGGradientDrawingOptions()) } return context }) }