import Foundation import UIKit import Postbox import SwiftSignalKit import Display import AVFoundation import ImageIO import TelegramCore import WebP import TelegramUIPreferences import MediaResources import AccountContext import Tuples import ImageBlur import TinyThumbnail import ImageTransparency 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)) } public 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, 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, NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData, true)) } else { let decodedThumbnailData = photoReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) let fetchedThumbnail: Signal if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) } let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, 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, NoError> if autoFetchFullSize { fullSizeData = Signal, 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(Tuple(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 -> Tuple2 in return Tuple(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 { value in return Tuple(thumbnailData, value._0, value._1) } } else { return .single(Tuple(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, 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, NoError> in if maybeData.complete { return .single(Tuple(nil, maybeData.path, true)) } else { let fetchedThumbnail: Signal if !fetched, let _ = decodedThumbnailData { fetchedThumbnail = .single(.local) } else if let thumbnailResource = thumbnailResource { fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, 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 -> Tuple2 in return Tuple(next.size == 0 ? nil : next.path, next.complete) } return thumbnail |> mapToSignal { thumbnailData in return fullSizeDataAndPath |> map { value -> Tuple3 in return Tuple3(thumbnailData, value._0, value._1) } } } } |> 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, autoFetchFullSizeThumbnail: Bool = false) -> Signal, NoError> { let thumbnailRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) let thumbnailResource = thumbnailRepresentation?.resource let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) if !thumbnailGenerationMimeTypes.contains(fileReference.media.mimeType) { if let decodedThumbnailData = decodedThumbnailData { if autoFetchFullSizeThumbnail, let thumbnailRepresentation = thumbnailRepresentation, (thumbnailRepresentation.dimensions.width > 200.0 || thumbnailRepresentation.dimensions.height > 200.0) { return Signal { subscriber in let fetchedDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailRepresentation.resource), statsCategory: .video).start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailRepresentation.resource, attemptSynchronously: false).start(next: { next in let data: Data? = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) subscriber.putNext(Tuple(data ?? decodedThumbnailData, nil, false)) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { return .single(Tuple(decodedThumbnailData, nil, false)) } } else if let thumbnailResource = thumbnailResource { let fetchedThumbnail: Signal = fetchedMediaResource(mediaBox: account.postbox.mediaBox, 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(Tuple(data, nil, false)) } else { subscriber.putNext(Tuple(nil, nil, false)) } }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { return .single(Tuple(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, NoError> in if maybeData.complete { return .single(Tuple(nil, maybeData.path, true)) } else { let fetchedThumbnail: Signal if let _ = fileReference.media.immediateThumbnailData { fetchedThumbnail = .complete() } else if let thumbnailResource = thumbnailResource { fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, 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 -> Tuple2in return Tuple(next.size == 0 ? nil : next.path, next.complete) } return thumbnail |> mapToSignal { thumbnailData in return fullSizeDataAndPath |> map { value in return Tuple(thumbnailData, value._0, value._1) } } } } |> filter({ $0._0 != nil || $0._1 != nil }) return signal } private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaReference, thumbnailSize: Bool = false, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal?, Bool>, NoError> { let fullSizeResource = fileReference.media.resource let thumbnailRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) let thumbnailResource = thumbnailRepresentation?.resource let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false, attemptSynchronously: synchronousLoad) let fetchedFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: true, attemptSynchronously: synchronousLoad) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal?, Bool>, NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData == nil ? nil : Tuple(loadedData!, maybeData.path), true)) } else { let thumbnail: Signal if onlyFullSize { thumbnail = .single(nil) } else if let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { if autoFetchFullSizeThumbnail, let thumbnailRepresentation = thumbnailRepresentation, (thumbnailRepresentation.dimensions.width > 200.0 || thumbnailRepresentation.dimensions.height > 200.0) { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: fileReference.resourceReference(thumbnailRepresentation.resource), statsCategory: .video).start() let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in let data: Data? = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) subscriber.putNext(data ?? decodedThumbnailData) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { thumbnail = .single(decodedThumbnailData) } } else if let thumbnailResource = thumbnailResource { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: .video).start() let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource, 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() } } } 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 -> Tuple2?, Bool> in let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) return Tuple(data == nil ? nil : Tuple(data!, next.path), next.complete) } return thumbnail |> mapToSignal { thumbnailData in return fullSizeDataAndPath |> map { value in return Tuple(thumbnailData, value._0, value._1) } } } } |> 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 } } public 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)) } } } } public func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> Signal { return chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, autoFetchFullSize: true) |> map { value -> UIImage? in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) |> map { _, generate in return generate } } public func chatMessagePhotoInternal(photoData: Signal, NoError>, synchronousLoad: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { return photoData |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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: UIImage.Orientation = .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) if thumbnailSize.width > 200.0 && thumbnailSize.height > 200.0 { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { 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)) } } imageFastBlur(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)) } imageFastBlur(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)) } } imageFastBlur(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)) } imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) imageFastBlur(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, 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, NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData, true)) } else { let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, 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, NoError> = fetchedFullSize |> map { next -> Tuple2 in return Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { value in return Tuple(thumbnailData, value._0, value._1) } } } } |> 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 { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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: UIImage.Orientation = .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)) } imageFastBlur(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, autoFetchFullSizeThumbnail: true) return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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: UIImage.Orientation = .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 { if max(thumbnailImage.width, thumbnailImage.height) > 200 { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { 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)) } imageFastBlur(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 chatSecretPhoto(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference) return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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)) } imageFastBlur(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)) } } imageFastBlur(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)) } imageFastBlur(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)) } } imageFastBlur(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, 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, NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData, true)) } else { let fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: representations[smallestIndex].reference, statsCategory: .image) let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, 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, NoError> if autoFetchFullSize { fullSizeData = Signal, NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in subscriber.putNext(Tuple(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 -> Tuple2 in return Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { value in return Tuple(thumbnailData, value._0, value._1) } } } } |> 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() } } public 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 { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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: UIImage.Orientation = .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)) } imageFastBlur(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 } } } public func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference, fullRepresentationSize: fullRepresentationSize, autoFetchFullSize: true, synchronousLoad: synchronousLoad) return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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: UIImage.Orientation = .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 } } } public 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(mediaBox: account.postbox.mediaBox, 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)) } imageFastBlur(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() } } public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail) |> map { return $0.1 } } public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal: Signal?, Bool>, NoError> if let imageReference = imageReference { signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad) |> map { value -> Tuple3?, Bool> in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 return Tuple(thumbnailData, fullSizeData.flatMap({ Tuple($0, "") }), fullSizeComplete) } } else { signal = chatMessageVideoDatas(postbox: postbox, fileReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail) } return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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 drawingSize: CGSize if case .aspectFill = arguments.resizeMode { drawingSize = arguments.imageSize.aspectFilled(arguments.boundingSize) } else { drawingSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) } if drawingSize.width < drawingRect.size.width && drawingSize.width >= drawingRect.size.width - 2.0 { drawingSize.width = drawingRect.size.width } if drawingSize.height < drawingRect.size.height && drawingSize.height >= drawingRect.size.height - 2.0 { drawingSize.height = drawingRect.size.height } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - drawingSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - drawingSize.height) / 2.0), size: drawingSize) var fullSizeImage: CGImage? var imageOrientation: UIImage.Orientation = .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 { if max(thumbnailImage.width, thumbnailImage.height) > Int(min(200.0, min(drawingSize.width, drawingSize.height))) { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let initialThumbnailContextFittingSize = drawingSize.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)) } } imageFastBlur(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 = drawingSize.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)) } imageFastBlur(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)) } } imageFastBlur(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)) } imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) imageFastBlur(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) case .aspectFill: break } } 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 }) } } public 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(mediaBox: account.postbox.mediaBox, 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: MediaAutoDownloadPeerType?) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image, reportResultStatus: true) |> mapToSignal { type -> Signal in if case .remote = type, let peerType = storeToDownloadsPeerType { return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerType: peerType) |> introduceError(FetchResourceError.self) |> mapToSignal { _ -> Signal in return .complete() } |> then(.single(type)) } return .single(type) } } else { return .never() } } public func chatMessagePhotoCancelInteractiveFetch(account: Account, photoReference: ImageMediaReference) { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return account.postbox.mediaBox.cancelInteractiveResourceFetch(largestRepresentation.resource) } } public func chatMessageWebFileInteractiveFetched(account: Account, image: TelegramMediaWebFile) -> Signal { return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .standalone(resource: image.resource), statsCategory: .image) } public func chatMessageWebFileCancelInteractiveFetch(account: Account, image: TelegramMediaWebFile) { return account.postbox.mediaBox.cancelInteractiveResourceFetch(image.resource) } public 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(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(resource)).start()) return disposable } } public 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(mediaBox: account.postbox.mediaBox, 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: UIImage.Orientation = .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 } } } } public 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: UIImage.Orientation = .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 } } } } public 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(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource)) let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) subscriber.putNext(data ?? decodedThumbnailData) }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } return thumbnail } else { return .single(nil) } } public 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)) } imageFastBlur(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)) } } imageFastBlur(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) -> UIImage.Orientation { 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 } } public func imageOrientationFromSource(_ source: CGImageSource) -> UIImage.Orientation { 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: UIImage.Orientation) -> 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 } } public func drawImage(context: CGContext, image: CGImage, orientation: UIImage.Orientation, 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() } } public func chatMessageImageFile(account: Account, fileReference: FileMediaReference, thumbnail: Bool, fetched: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal, NoError> if thumbnail { signal = chatMessageImageFileThumbnailDatas(account: account, fileReference: fileReference, autoFetchFullSizeThumbnail: true) } else { signal = chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false, fetched: fetched) } return signal |> map { value in let thumbnailData = value._0 let fullSizePath = value._1 let fullSizeComplete = value._2 return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) 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: UIImage.Orientation = .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? var clearContext = false if let thumbnailData = thumbnailData { if 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) } } else if let image = WebP.convert(fromWebP: thumbnailData) { thumbnailImage = image.cgImage clearContext = true if thumbnail { fittedSize = CGSize(width: CGFloat(image.size.width), height: CGFloat(image.size.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 { if max(thumbnailImage.width, thumbnailImage.height) > 200 { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { 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, clear: clearContext) thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } imageFastBlur(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, clear: clearContext) additionalBlurContext.withFlippedContext { c in c.interpolationQuality = .default if let image = thumbnailContext.generateImage()?.cgImage { c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) } } imageFastBlur(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 } } } public func instantPageImageFile(account: Account, fileReference: FileMediaReference, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false, fetched: fetched) |> map { value in let thumbnailData = value._0 let fullSizePath = value._1 let fullSizeComplete = value._2 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: UIImage.Orientation = .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], autoFetchFullSize: Bool = false) -> Signal, 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 signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal, NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData, true)) } else { let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[smallestIndex].reference) let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[largestIndex].reference) 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, NoError> if autoFetchFullSize { fullSizeData = Signal, NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in subscriber.putNext(Tuple(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 -> Tuple2 in return Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { value in return Tuple(thumbnailData, value._0, value._1) } } } } |> filter({ $0._0 != nil || $0._1 != nil }) return signal } else { return .never() } } public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = avatarGalleryPhotoDatas(account: account, representations: representations, autoFetchFullSize: autoFetchFullSize) return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 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: UIImage.Orientation = .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)) } } imageFastBlur(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 } } } public 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() public 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: UIImage.Orientation = .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 } } } public 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: UIImage.Orientation = .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 { let thumbnailResource = postbox.mediaBox.resourceData(thumbnail) let signal = thumbnailResource |> take(1) |> mapToSignal { maybeData -> Signal 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, NoError> { let fullSizeResource = postbox.mediaBox.resourceData(fullSize) let thumbnailResource = postbox.mediaBox.resourceData(thumbnail) let signal = fullSizeResource |> take(1) |> mapToSignal { maybeData -> Signal, NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(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, NoError> if autoFetchFullSize { fullSizeData = Signal, NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = fullSizeResource.start(next: { next in subscriber.putNext(Tuple(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 -> Tuple2 in return Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { value in return Tuple(thumbnailData, value._0, value._1) } } } } |> 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)) } } } public 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 = thumbnail ? CGSize(width: 48.0, height: 48.0) : CGSize(width: 320.0, height: 320.0) fileArtworkData = fileArtworkData |> then( postbox.mediaBox.cachedResourceRepresentation(fileReference.media.resource, representation: CachedAlbumArtworkRepresentation(size: size), complete: false, fetch: true) |> map { data -> Data? in if data.complete, let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: .mappedRead) { return fileData } else { return nil } } ) } var immediateArtworkData: Signal, NoError> = .single(Tuple(nil, nil, false)) if let fileReference = fileReference, let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource let fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, 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 Tuple(thumbnailData, nil, false) } } else if let albumArt = albumArt { if thumbnail { immediateArtworkData = albumArtThumbnailData(postbox: postbox, thumbnail: albumArt.thumbnailResource) |> map { thumbnailData in return Tuple(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 } } } public func securePhoto(account: Account, resource: TelegramMediaResource, accessContext: SecureIdAccessContext) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return securePhotoInternal(account: account, resource: resource, accessContext: accessContext) |> map { $0.1 } } public 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: UIImage.Orientation = .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 { let appIconResource = postbox.mediaBox.resourceData(appIcon) let signal = appIconResource |> take(1) |> mapToSignal { maybeData -> Signal 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() } public enum OpenInAppIcon { case resource(resource: TelegramMediaResource) case image(image: UIImage) } public 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 }) } } public 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 }) }