import Foundation import UIKit import Postbox import SwiftSignalKit import Display import AVFoundation import ImageIO import TelegramCore import WebPBinding import TelegramUIPreferences import MediaResources import AccountContext import Tuples import ImageBlur import FastBlur import TinyThumbnail import ImageTransparency import AppBundle import MusicAlbumArtResources import Svg import RangeSet import Accelerate import ImageCompression private enum ResourceFileData { case data(Data) case file(path: String, size: Int) } public func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> TelegramMediaImageRepresentation? { if let progressiveRepresentation = progressiveImageRepresentation(photo.representations) { return progressiveRepresentation } return photo.representationForDisplayAtSize(PixelDimensions(width: 1280, height: 1280)) } private let progressiveRangeMap: [(Int, [Int])] = [ (100, [0]), (400, [3]), (600, [4]), (Int(Int32.max), [2, 3, 4]) ] public func representationFetchRangeForDisplayAtSize(representation: TelegramMediaImageRepresentation, dimension: Int?) -> Range? { if representation.progressiveSizes.count > 1, let dimension = dimension { var largestByteSize = Int64(representation.progressiveSizes[0]) for (maxDimension, byteSizes) in progressiveRangeMap { largestByteSize = Int64(representation.progressiveSizes[min(representation.progressiveSizes.count - 1, byteSizes.last!)]) if maxDimension >= dimension { break } } return 0 ..< largestByteSize } return nil } public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal, NoError> { return chatMessagePhotoDatas(mediaBox: postbox.mediaBox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: photoReference, fullRepresentationSize: fullRepresentationSize, autoFetchFullSize: autoFetchFullSize, tryAdditionalRepresentations: tryAdditionalRepresentations, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: forceThumbnail, automaticFetch: automaticFetch) } func chatMessagePhotoDatas(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal, NoError> { if !forceThumbnail, let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 { enum SizeSource { case miniThumbnail(data: Data) case image(size: Int64) } var sources: [SizeSource] = [] let thumbnailByteSize = Int(progressiveRepresentation.progressiveSizes[0]) var largestByteSize = Int(progressiveRepresentation.progressiveSizes[0]) for (maxDimension, byteSizes) in progressiveRangeMap { if Int(fullRepresentationSize.width) > 100 && maxDimension <= 100 { continue } sources.append(contentsOf: byteSizes.compactMap { sizeIndex -> SizeSource? in if progressiveRepresentation.progressiveSizes.count - 1 < sizeIndex { return nil } return .image(size: Int64(progressiveRepresentation.progressiveSizes[sizeIndex])) }) largestByteSize = Int(progressiveRepresentation.progressiveSizes[min(progressiveRepresentation.progressiveSizes.count - 1, byteSizes.last!)]) if maxDimension >= Int(fullRepresentationSize.width) { break } } if sources.isEmpty { sources.append(.image(size: Int64(largestByteSize))) } if let miniThumbnail = photoReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { sources.insert(.miniThumbnail(data: miniThumbnail), at: 0) } return Signal { subscriber in let signals: [Signal<(SizeSource, Data?), NoError>] = sources.map { source -> Signal<(SizeSource, Data?), NoError> in switch source { case let .miniThumbnail(data): return .single((source, data)) case let .image(size): return mediaBox.resourceData(progressiveRepresentation.resource, size: Int64(progressiveRepresentation.progressiveSizes.last!), in: 0 ..< size, mode: .incremental, notifyAboutIncomplete: true, attemptSynchronously: synchronousLoad) |> map { (data, _) -> (SizeSource, Data?) in return (source, data) } } } let dataDisposable = combineLatest(signals).start(next: { results in var foundData = false loop: for i in (0 ..< results.count).reversed() { let isLastSize = i == results.count - 1 switch results[i].0 { case .image: if let data = results[i].1, data.count != 0 { if Int(fullRepresentationSize.width) > 100 && i <= 1 && !isLastSize { continue } subscriber.putNext(Tuple4(nil, data, .full, isLastSize)) foundData = true if isLastSize { subscriber.putCompletion() } break loop } case let .miniThumbnail(thumbnailData): subscriber.putNext(Tuple4(thumbnailData, nil, .blurred, false)) foundData = true break loop } } if !foundData { subscriber.putNext(Tuple4(nil, nil, .blurred, false)) } }) var fetchDisposable: Disposable? if automaticFetch { if autoFetchFullSize { fetchDisposable = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() } else if useMiniThumbnailIfAvailable { fetchDisposable = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() } } return ActionDisposable { dataDisposable.dispose() fetchDisposable?.dispose() } } } if !forceThumbnail || photoReference.media.immediateThumbnailData == nil, let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let fullRepresentation = largestImageRepresentation(photoReference.media.representations) { let maybeFullSize = mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) let maybeLargestSize = mediaBox.resourceData(fullRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) let signal = combineLatest(maybeFullSize, maybeLargestSize) |> take(1) |> mapToSignal { maybeData, maybeLargestData -> Signal, NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData, .full, true)) } else if maybeLargestData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeLargestData.path), options: []) return .single(Tuple(nil, loadedData, .full, true)) } else { let decodedThumbnailData = photoReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) let fetchedThumbnail: Signal if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { fetchedThumbnail = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) } let fetchedFullSize = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) let anyThumbnail: [Signal<(MediaResourceData, ChatMessagePhotoQuality), NoError>] if tryAdditionalRepresentations { anyThumbnail = photoReference.media.representations.filter({ representation in return representation != largestRepresentation }).map({ representation -> Signal<(MediaResourceData, ChatMessagePhotoQuality), NoError> in return mediaBox.resourceData(representation.resource) |> take(1) |> map { data -> (MediaResourceData, ChatMessagePhotoQuality) in if representation.dimensions.width > 200 || representation.dimensions.height > 200 { return (data, .medium) } else { return (data, .blurred) } } }) } 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 = 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<(Data, ChatMessagePhotoQuality)?, NoError> in for (thumbnail, quality) in thumbnails { if thumbnail.size != 0, let data = try? Data(contentsOf: URL(fileURLWithPath: thumbnail.path), options: []) { return .single((data, quality)) } } return mainThumbnail |> map { data -> (Data, ChatMessagePhotoQuality)? in return data.flatMap { ($0, .blurred) } } } let fullSizeData: Signal, NoError> if autoFetchFullSize && !useMiniThumbnailIfAvailable { fullSizeData = Signal, NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = 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 = 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, thumbnailQuality) = thumbnailData { return fullSizeData |> map { value in return Tuple(thumbnailData, value._0, value._1 ? .full : thumbnailQuality, value._1) } } else { return .single(Tuple(nil, nil, .none, 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 if let decodedThumbnailData = photoReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { return .single(Tuple(decodedThumbnailData, nil, .blurred, false)) } else { return .never() } } public func chatMessageFileDatas(account: Account, userLocation: MediaResourceUserLocation, 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, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), 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, userLocation: MediaResourceUserLocation, 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 || thumbnailRepresentation.dimensions.height > 200) { return Signal { subscriber in let fetchedDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), 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, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), 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, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), 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 fileQualityPreloadData(postbox: Postbox, playlistFile: FileMediaReference, videoFile: FileMediaReference, userLocation: MediaResourceUserLocation, autofetchPlaylist: Bool, isOnce: Bool) -> Signal<(FileMediaReference, Range)?, NoError> { let playlistData: Signal?, NoError> = Signal { subscriber in var fetchDisposable: Disposable? if autofetchPlaylist { fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: userLocation, fileReference: playlistFile, resource: playlistFile.media.resource).start() } let dataDisposable = postbox.mediaBox.resourceData(playlistFile.media.resource).start(next: { data in if !data.complete { if isOnce { subscriber.putNext(nil) subscriber.putCompletion() } return } guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { subscriber.putNext(nil) subscriber.putCompletion() return } guard let playlistString = String(data: data, encoding: .utf8) else { subscriber.putNext(nil) subscriber.putCompletion() return } var durations: [Int] = [] var byteRanges: [Range] = [] let extinfRegex = try! NSRegularExpression(pattern: "EXTINF:(\\d+)", options: []) let byteRangeRegex = try! NSRegularExpression(pattern: "EXT-X-BYTERANGE:(\\d+)@(\\d+)", options: []) let extinfResults = extinfRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) for result in extinfResults { if let durationRange = Range(result.range(at: 1), in: playlistString) { if let duration = Int(String(playlistString[durationRange])) { durations.append(duration) } } } let byteRangeResults = byteRangeRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) for result in byteRangeResults { if let lengthRange = Range(result.range(at: 1), in: playlistString), let upperBoundRange = Range(result.range(at: 2), in: playlistString) { if let length = Int(String(playlistString[lengthRange])), let lowerBound = Int(String(playlistString[upperBoundRange])) { byteRanges.append(lowerBound ..< (lowerBound + length)) } } } if durations.count != byteRanges.count { subscriber.putNext(nil) subscriber.putCompletion() return } var rangeUpperBound: Int64 = 0 for i in 0 ..< durations.count { let byteRange = byteRanges[i] rangeUpperBound = max(rangeUpperBound, Int64(byteRange.upperBound)) if durations[i] != 0 { break } } if rangeUpperBound != 0 { subscriber.putNext(0 ..< rangeUpperBound) subscriber.putCompletion() } else { subscriber.putNext(nil) subscriber.putCompletion() } return }) return ActionDisposable { fetchDisposable?.dispose() dataDisposable.dispose() } } return playlistData |> map { range -> (FileMediaReference, Range)? in guard let range else { return nil } return (videoFile, range) } } private func minimizedHLSQuality(hlsFiles: [(playlist: TelegramMediaFile, video: TelegramMediaFile)]) -> (playlist: TelegramMediaFile, file: TelegramMediaFile)? { let sortedQualities = hlsFiles for (playlist, video) in sortedQualities { guard let dimensions = video.dimensions else { continue } if max(dimensions.width, dimensions.height) >= 600 { return (playlist, video) } } if let (playlist, video) = hlsFiles.first { return (playlist, video) } return nil } private func chatMessageVideoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, fileReference: FileMediaReference, hlsFiles: [(playlist: TelegramMediaFile, video: TelegramMediaFile)] = [], previewSourceFileReference: FileMediaReference?, alternativeFileAndRange: Signal<(TelegramMediaFile, Range), NoError>? = nil, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal?, Bool>, NoError> { if !hlsFiles.isEmpty { var possibleFiles: [TelegramMediaFile] = [fileReference.media] let filteredHlsFiles = hlsFiles.filter { hlsFile in guard let dimensions = hlsFile.video.dimensions else { return false } if !thumbnailSize && hlsFiles.count > 1 { if max(dimensions.width, dimensions.height) < 200 { return false } } return true } for item in filteredHlsFiles { possibleFiles.append(item.video) } var possibleReadyFiles: [Signal] = [] for possibleFile in possibleFiles { if possibleFile.fileId == fileReference.media.fileId { possibleReadyFiles.append( postbox.mediaBox.cachedResourceRepresentation(possibleFile.resource, representation: CachedVideoFirstFrameRepresentation(), complete: false, fetch: false, attemptSynchronously: synchronousLoad) |> take(1) ) } else { possibleReadyFiles.append( postbox.mediaBox.cachedResourceRepresentation(possibleFile.resource, representation: CachedVideoPrefixFirstFrameRepresentation(prefixLength: 0), complete: false, fetch: false, attemptSynchronously: synchronousLoad) |> take(1) ) } } return combineLatest(possibleReadyFiles) |> mapToSignal { possibleReadyFiles -> Signal?, Bool>, NoError> in for possibleReadyFile in possibleReadyFiles { if possibleReadyFile.complete { if let data = try? Data(contentsOf: URL(fileURLWithPath: possibleReadyFile.path), options: .mappedIfSafe) { return .single(Tuple(nil, Tuple(data, possibleReadyFile.path), true)) } } } let previewPrefixes: Signal<[(FileMediaReference, Range)?], NoError> = combineLatest(hlsFiles.map { hlsFile in return fileQualityPreloadData(postbox: postbox, playlistFile: fileReference.withMedia(hlsFile.playlist), videoFile: fileReference.withMedia(hlsFile.video), userLocation: userLocation, autofetchPlaylist: false, isOnce: true) |> take(1) }) let loadSignal = previewPrefixes |> mapToSignal { previewPrefixes -> Signal?, Bool>, NoError> in let possibleReadyPrefixes = previewPrefixes.compactMap { possiblePrefix -> Signal? in guard let possiblePrefix, possiblePrefix.1.lowerBound == 0 else { return nil } return postbox.mediaBox.cachedResourceRepresentation(possiblePrefix.0.media.resource, representation: CachedVideoPrefixFirstFrameRepresentation(prefixLength: Int32(possiblePrefix.1.upperBound)), complete: false, fetch: false, attemptSynchronously: synchronousLoad) |> take(1) } return combineLatest(possibleReadyPrefixes) |> mapToSignal { possibleReadyPrefixes -> Signal?, Bool>, NoError> in for possibleReadyPrefix in possibleReadyPrefixes { if possibleReadyPrefix.complete { if let data = try? Data(contentsOf: URL(fileURLWithPath: possibleReadyPrefix.path), options: .mappedIfSafe) { return .single(Tuple(nil, Tuple(data, possibleReadyPrefix.path), true)) } } } guard let (playlist, video) = minimizedHLSQuality(hlsFiles: hlsFiles) else { return .single(Tuple(nil, nil, true)) } return fileQualityPreloadData(postbox: postbox, playlistFile: fileReference.withMedia(playlist), videoFile: fileReference.withMedia(video), userLocation: userLocation, autofetchPlaylist: true, isOnce: false) |> mapToSignal { preloadData -> Signal?, Bool>, NoError> in guard let preloadData else { return .never() } return Signal { subscriber in let fetchedFilePrefix = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: fileReference.withMedia(video).resourceReference(video.resource), range: (preloadData.1, .default), statsCategory: .image).start() let fetchedFrame = postbox.mediaBox.cachedResourceRepresentation(preloadData.0.media.resource, representation: CachedVideoPrefixFirstFrameRepresentation(prefixLength: Int32(preloadData.1.upperBound)), complete: true, fetch: true, attemptSynchronously: false).start(next: { resourceData in if resourceData.complete { if resourceData.complete { if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: .mappedIfSafe) { subscriber.putNext(Tuple(nil, Tuple(data, resourceData.path), true)) subscriber.putCompletion() } } } }) return ActionDisposable { fetchedFilePrefix.dispose() fetchedFrame.dispose() } } } } } var resultSignal: Signal?, Bool>, NoError> = .complete() if let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { resultSignal = .single(Tuple(decodedThumbnailData, nil, false)) } resultSignal = resultSignal |> then(loadSignal) return resultSignal } } let fullSizeResource = fileReference.media.resource var reducedSizeResource: MediaResource? if let videoThumbnail = fileReference.media.videoThumbnails.first { reducedSizeResource = videoThumbnail.resource } var previewSourceFullSizeResource: MediaResource? if let previewSourceFileReference { previewSourceFullSizeResource = previewSourceFileReference.media.resource } var thumbnailRepresentation: TelegramMediaImageRepresentation? if thumbnailRepresentation == nil { thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(fileReference.media.previewRepresentations) : smallestImageRepresentation(fileReference.media.previewRepresentations) } let thumbnailResource = thumbnailRepresentation?.resource let maybePreviewSourceFullSize: Signal if let previewSourceFullSizeResource { maybePreviewSourceFullSize = postbox.mediaBox.cachedResourceRepresentation(previewSourceFullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false, attemptSynchronously: synchronousLoad) } else { maybePreviewSourceFullSize = .single(MediaResourceData(path: "", offset: 0, size: 0, complete: false)) } 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) var fetchedReducedSize: Signal = .single(MediaResourceData(path: "", offset: 0, size: 0, complete: false)) if let reducedSizeResource = reducedSizeResource { fetchedReducedSize = postbox.mediaBox.cachedResourceRepresentation(reducedSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: true, attemptSynchronously: synchronousLoad) } let signal = combineLatest( maybePreviewSourceFullSize, maybeFullSize ) |> map { maybePreviewSourceFullSize, maybeFullSize -> MediaResourceData in if maybePreviewSourceFullSize.complete { return maybePreviewSourceFullSize } else { return maybeFullSize } } |> take(1) |> mapToSignal { maybeData -> Signal?, Bool>, NoError> in if maybeData.complete && !forceThumbnail { 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 || thumbnailRepresentation.dimensions.height > 200) { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? MediaResourceUserContentType(file: fileReference.media), 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: []) if let data { subscriber.putNext(data) } else { subscriber.putNext(decodedThumbnailData) } }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { thumbnail = .single(decodedThumbnailData) } } else if let thumbnailResource = thumbnailResource { if autoFetchFullSizeThumbnail, let thumbnailRepresentation = thumbnailRepresentation, (thumbnailRepresentation.dimensions.width > 200 || thumbnailRepresentation.dimensions.height > 200) { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? MediaResourceUserContentType(file: fileReference.media), 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: []) if let data { subscriber.putNext(data) } else { subscriber.putNext(nil) } }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } } else { thumbnail = Signal { subscriber in let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? MediaResourceUserContentType(file: fileReference.media), 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) } let reducedSizeDataAndPath = Signal { subscriber in let dataDisposable = fetchedReducedSize.start(next: { next in subscriber.putNext(next) }, completed: { subscriber.putCompletion() }) return ActionDisposable { dataDisposable.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 if synchronousLoad, let thumbnailData = thumbnailData { return .single(Tuple(thumbnailData, nil, false)) |> then( combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath) |> map { fullSize, reducedSize in if !fullSize._1 && reducedSize._1 { return Tuple(thumbnailData, reducedSize._0, false) } return Tuple(thumbnailData, fullSize._0, fullSize._1) } ) } else { return combineLatest(fullSizeDataAndPath, reducedSizeDataAndPath) |> map { fullSize, reducedSize in if !fullSize._1 && reducedSize._1 { return Tuple(thumbnailData, reducedSize._0, false) } return Tuple(thumbnailData, fullSize._0, fullSize._1) } } } } } |> filter({ if onlyFullSize { return $0._1 != nil || $0._2 } else { return true//$0.0 != nil || $0.1 != nil || $0.2 } }) return signal } public func rawMessagePhoto(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference) -> Signal { return chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, photoReference: photoReference, autoFetchFullSize: true) |> map { value -> UIImage? in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._3 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, userLocation: MediaResourceUserLocation, userContentType customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) |> map { _, _, generate in return generate } } public func chatMessagePhoto(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(mediaBox: mediaBox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) |> map { _, _, generate in return generate } } public enum ChatMessagePhotoQuality { case none case blurred case medium case full } public func chatMessagePhotoInternal(photoData: Signal, NoError>, synchronousLoad: Bool = false) -> Signal<(() -> CGSize?, ChatMessagePhotoQuality, (TransformImageArguments) -> DrawingContext?), NoError> { return photoData |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let quality = value._2 let fullSizeComplete = value._3 return ({ return nil }, quality, { 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 } if quality == .blurred && fullSizeImage != nil { thumbnailImage = fullSizeImage fullSizeImage = nil } 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) if 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 if 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 { guard let context = DrawingContext(size: blurredThumbnailImage.size, scale: blurredThumbnailImage.scale, clear: true) else { return nil } 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 } guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) else { return nil } 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? let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) guard let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) else { return } 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 if 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() } 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, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, onlyFullSize: Bool = false, forceThumbnail: 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(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))) { 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, !forceThumbnail { 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, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) let thumbnail = Signal<(Data, Bool)?, NoError> { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in if next.size == 0 { subscriber.putNext(nil) } else if let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) { subscriber.putNext((data, false)) } else { subscriber.putNext(nil) } }, 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, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, onlyFullSize: Bool = false, blurred: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessagePhotoThumbnailDatas(account: account, userLocation: userLocation, photoReference: photoReference, onlyFullSize: onlyFullSize, forceThumbnail: blurred) return signal |> map { value in let thumbnailData: Data? = value._0?.0 let thumbnailIsBlurred: Bool = value._0?.1 ?? false let fullSizeData = value._1 let fullSizeComplete = value._2 return { arguments in guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) else { return nil } 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, !blurred { 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: CGImage? if let thumbnailImage = thumbnailImage { if thumbnailIsBlurred || blurred { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFitted(blurred ? CGSize(width: 50.0, height: 50.0) : CGSize(width: 150.0, height: 150.0)) if 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) if blurred { if !thumbnailIsBlurred { telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) } imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) } blurredThumbnailImage = thumbnailContext.generateImage()?.cgImage } } else { blurredThumbnailImage = thumbnailImage } } 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 { c.interpolationQuality = .low drawImage(context: c, image: blurredThumbnailImage, 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, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, blurred: Bool = false, synchronousLoads: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, previewSourceFileReference: nil, thumbnailSize: true, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred) return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 return { arguments in guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) else { return nil } 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, !blurred { 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 && !blurred { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFitted(blurred ? CGSize(width: 50.0, height: 50.0) : CGSize(width: 150.0, height: 150.0)) if 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) if blurred { imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) } 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, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, ignoreFullSize: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, synchronousLoad: synchronousLoad, forceThumbnail: ignoreFullSize) return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._3 return { arguments in guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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)) if 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)) if 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)) if 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)) if 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))) } } adjustSaturationInContext(context: context, saturation: 1.7) addCorners(context, arguments: arguments) return context } } } public func adjustSaturationInContext(context: DrawingContext, saturation: CGFloat) { var buffer = vImage_Buffer() buffer.data = context.bytes buffer.width = UInt(context.size.width * context.scale) buffer.height = UInt(context.size.height * context.scale) buffer.rowBytes = context.bytesPerRow let divisor: Int32 = 0x1000 let rwgt: CGFloat = 0.3086 let gwgt: CGFloat = 0.6094 let bwgt: CGFloat = 0.0820 let adjustSaturation = saturation let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation let b = (1.0 - adjustSaturation) * rwgt let c = (1.0 - adjustSaturation) * rwgt let d = (1.0 - adjustSaturation) * gwgt let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation let f = (1.0 - adjustSaturation) * gwgt let g = (1.0 - adjustSaturation) * bwgt let h = (1.0 - adjustSaturation) * bwgt let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation let satMatrix: [CGFloat] = [ a, b, c, 0, d, e, f, 0, g, h, i, 0, 0, 0, 0, 1 ] var matrix: [Int16] = satMatrix.map { value in return Int16(value * CGFloat(divisor)) } vImageMatrixMultiply_ARGB8888(&buffer, &buffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) } private func avatarGalleryThumbnailDatas(postbox: Postbox, representations: [ImageRepresentationWithReference], fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, synchronousLoad: Bool) -> Signal, NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = imageRepresentationLargerThan(representations.map({ $0.representation }), size: PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) { let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, 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 fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[smallestIndex].reference, statsCategory: .image) let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .image, 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], synchronousLoad: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = avatarGalleryThumbnailDatas(postbox: account.postbox, representations: representations, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true, synchronousLoad: synchronousLoad) return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._2 return { arguments in guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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)) if 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, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), blurred: Bool = false, synchronousLoad: Bool = false, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let useMiniThumbnailIfAvailable: Bool = fullRepresentationSize.width < 40.0 var updatedFullRepresentationSize = fullRepresentationSize if useMiniThumbnailIfAvailable, let largest = largestImageRepresentation(photoReference.media.representations) { if progressiveImageRepresentation(photoReference.media.representations) == nil { updatedFullRepresentationSize = largest.dimensions.cgSize } } let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: blurred, automaticFetch: automaticFetch) return signal |> map { value in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._3 return { arguments in guard let context = DrawingContext(size: arguments.drawingSize, opaque: arguments.corners.isEmpty && arguments.intrinsicInsets == .zero, clear: true) else { return nil } 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 { if useMiniThumbnailIfAvailable && !blurred { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 90.0, height: 90.0)) if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } if !useMiniThumbnailIfAvailable || blurred { telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) } if blurred { telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) } 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, !blurred { 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, userLocation: .other, userContentType: .other, 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 guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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)) if 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, userLocation: MediaResourceUserLocation, userContentType customUserContentType: MediaResourceUserContentType? = nil, videoReference: FileMediaReference, hlsFiles: [(playlist: TelegramMediaFile, video: TelegramMediaFile)] = [], onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, videoReference: videoReference, hlsFiles: hlsFiles, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor, nilForEmptyResult: nilForEmptyResult, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable) |> map { return $0.1 } } public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, videoReference: FileMediaReference, hlsFiles: [(playlist: TelegramMediaFile, video: TelegramMediaFile)] = [],previewSourceFileReference: FileMediaReference? = nil, imageReference: ImageMediaReference? = nil, alternativeFileAndRange: Signal<(TelegramMediaFile, Range), NoError>? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal: Signal?, Bool>, NoError> if let imageReference = imageReference { signal = chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad, forceThumbnail: blurred) |> map { value -> Tuple3?, Bool> in let thumbnailData = value._0 let fullSizeData = value._1 let fullSizeComplete = value._3 return Tuple(thumbnailData, fullSizeData.flatMap({ Tuple($0, "") }), fullSizeComplete) } } else { signal = chatMessageVideoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, fileReference: videoReference, hlsFiles: hlsFiles, previewSourceFileReference: previewSourceFileReference, alternativeFileAndRange: alternativeFileAndRange, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, forceThumbnail: blurred) } 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 if nilForEmptyResult { if thumbnailData == nil && fullSizeData == nil { return nil } } guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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))) || useMiniThumbnailIfAvailable, !blurred { 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) if 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) if blurred { 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 if 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) let initialThumbnailContextFittingSize = drawingSize.fitted(CGSize(width: 100.0, height: 100.0)) let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) if 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 if 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() } 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, !blurred { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } } if let overlayColor = overlayColor { context.withFlippedContext { c in c.setBlendMode(.normal) c.setFillColor(overlayColor.cgColor) c.fill(arguments.drawingRect) } } addCorners(context, arguments: arguments) return context }) } } public func chatMessagePhotoStatus(context: AccountContext, messageId: MessageId, photoReference: ImageMediaReference, displayAtSize: Int? = nil) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { if let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) { return combineLatest( context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: largestRepresentation.resource), context.account.postbox.mediaBox.resourceRangesStatus(largestRepresentation.resource) ) |> map { status, rangeStatus -> MediaResourceStatus in if rangeStatus.isSuperset(of: RangeSet(range)) { return .Local } switch status { case .Local: return .Local case let .Remote(progress): return .Remote(progress: progress) case let .Fetching(isActive, progress): return .Fetching(isActive: isActive, progress: max(progress, 0.0)) case let .Paused(progress): return .Paused(progress: progress) } } |> distinctUntilChanged } else { return context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: largestRepresentation.resource) } } else { return .never() } } public func standaloneChatMessagePhotoInteractiveFetched(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image, reportResultStatus: true) |> mapToSignal { type -> Signal in return .single(type) } } else { return .never() } } public func chatMessagePhotoInteractiveFetched(context: AccountContext, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, displayAtSize: Int?, storeToDownloadsPeerId: EnginePeer.Id?) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { var fetchRange: (Range, MediaBoxFetchPriority)? if let displayAtSize = displayAtSize, let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) { fetchRange = (range, .default) } return fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(largestRepresentation.resource), range: fetchRange, statsCategory: .image, reportResultStatus: true) |> mapToSignal { type -> Signal in if case .remote = type, let peerId = storeToDownloadsPeerId { return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerId: peerId) |> castError(FetchResourceError.self) |> mapToSignal { _ -> Signal in } |> then(.single(type)) } return .single(type) } |> ignoreValues |> `catch` { _ -> Signal in return .complete() } } 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, userLocation: MediaResourceUserLocation, image: TelegramMediaWebFile) -> Signal { return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, 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, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, resource: MediaResource, automaticFetch: Bool = true) -> 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: { _ in }, completed: { subscriber.putCompletion() })) if automaticFetch { var userContentType: MediaResourceUserContentType = .other if let file = mediaReference.media as? TelegramMediaFile { userContentType = MediaResourceUserContentType(file: file) } else if let _ = mediaReference.media as? TelegramMediaImage { userContentType = .image } disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: mediaReference.resourceReference(resource)).start()) } return disposable } } public func chatWebpageSnippetPhotoData(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference) -> Signal { if let closestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: 120, height: 120)) { 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: { _ in }, completed: { subscriber.putCompletion() })) disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(closestRepresentation.resource)).start()) return disposable } } else { return .never() } } public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true, placeholderColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource, automaticFetch: automaticFetch) 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 } } var blurredImage: UIImage? if fullSizeImage == nil { var immediateThumbnailData: Data? if let file = mediaReference.media as? TelegramMediaFile { immediateThumbnailData = file.immediateThumbnailData } else if let image = mediaReference.media as? TelegramMediaImage { immediateThumbnailData = image.immediateThumbnailData } if let decodedThumbnailData = immediateThumbnailData.flatMap(decodeTinyThumbnail), let imageSource = CGImageSourceCreateWithData(decodedThumbnailData 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)) if 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)) if 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 let fullSizeImage = fullSizeImage ?? (blurredImage?.cgImage) { guard let context = DrawingContext(size: arguments.drawingSize, opaque: arguments.corners.isEmpty && arguments.intrinsicInsets == .zero, clear: true) else { return nil } 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 let emptyColor = arguments.emptyColor { c.setFillColor(emptyColor.cgColor) c.fill(arguments.drawingRect) } 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 { if let emptyColor = arguments.emptyColor { guard let context = DrawingContext(size: arguments.drawingSize, opaque: arguments.corners.isEmpty && arguments.intrinsicInsets == .zero, clear: true) else { return nil } context.withFlippedContext { c in c.setBlendMode(.copy) c.setFillColor(emptyColor.cgColor) c.fill(arguments.drawingRect) } addCorners(context, arguments: arguments) return context } else { return nil } } } } } public func chatWebpageSnippetPhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, placeholderColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatWebpageSnippetPhotoData(account: account, userLocation: userLocation, 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 { guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) else { return nil } 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 if let placeholderColor { guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) else { return nil } context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.corners.topLeft.radius != 0.0 { c.addPath(UIBezierPath(roundedRect: arguments.drawingRect, cornerRadius: arguments.corners.topLeft.radius).cgPath) c.clip() } c.setFillColor(placeholderColor.cgColor) c.fill(arguments.drawingRect) } return context } else { return nil } } } } public func chatMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, videoReference: FileMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return mediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: videoReference, synchronousLoad: synchronousLoad) } private func chatSecretMessageVideoData(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal { let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(thumbnailResource)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, attemptSynchronously: synchronousLoad).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(decodedThumbnailData) } } public func chatSecretMessageVideo(account: Account, userLocation: MediaResourceUserLocation, videoReference: FileMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatSecretMessageVideoData(account: account, userLocation: userLocation, fileReference: videoReference, synchronousLoad: synchronousLoad) return signal |> map { thumbnailData in return { arguments in guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } if arguments.drawingSize.width.isLessThanOrEqualTo(0.0) || arguments.drawingSize.height.isLessThanOrEqualTo(0.0) { return nil } 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: 40.0, height: 40.0)) if 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)) if 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).insetBy(dx: -4.0, dy: -4.0)) } } 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))) } } if arguments.corners.topLeft.radius > 40.0 { } else { 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, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, thumbnail: Bool, fetched: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal, NoError> if thumbnail { signal = chatMessageImageFileThumbnailDatas(account: account, userLocation: userLocation, fileReference: fileReference, autoFetchFullSizeThumbnail: true) } else { signal = chatMessageFileDatas(account: account, userLocation: userLocation, 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() guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } let drawingRect = arguments.drawingRect var fittedSize: CGSize if thumbnail { fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize) } else { fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) } var fullSizeImage: CGImage? var imageOrientation: 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) } } } if fullSizeImage == nil, fileReference.media.mimeType == "image/jxl" { if let data = try? Data(contentsOf: URL(fileURLWithPath: fullSizePath), options: [.mappedIfSafe]), let image = decompressImageFromJPEGXL(data: data) { fullSizeImage = image.cgImage } } } 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) if 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 if 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 preloadedBotIcon(account: Account, fileReference: FileMediaReference) -> Signal { let signal = Signal { subscriber in let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)).start() let dataDisposable = account.postbox.mediaBox.resourceData(fileReference.media.resource, option: .incremental(waitUntilFetchStatus: false)).start(next: { data in if data.complete { subscriber.putNext(true) subscriber.putCompletion() } else { subscriber.putNext(false) } }) return ActionDisposable { fetched.dispose() dataDisposable.dispose() } } return signal } public func instantPageImageFile(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessageFileDatas(account: account, userLocation: userLocation, fileReference: fileReference, progressive: false, fetched: fetched) |> map { value in let fullSizePath = value._1 let fullSizeComplete = value._2 return { arguments in assertNotOnMainThread() guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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 } } } public func svgIconImageFile(account: Account, fileReference: FileMediaReference?, stickToTop: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let data: Signal if let fileReference = fileReference { data = account.postbox.mediaBox.cachedResourceRepresentation(fileReference.media.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true) } else { data = Signal { subscriber in if let url = getAppBundle().url(forResource: "durgerking", withExtension: "placeholder"), let data = try? Data(contentsOf: url, options: .mappedRead) { subscriber.putNext(MediaResourceData(path: url.path, offset: 0, size: Int64(data.count), complete: true)) subscriber.putCompletion() } return EmptyDisposable } } return data |> map { value in let fullSizePath = value.path let fullSizeComplete = value.complete return { arguments in guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) var fullSizeImage: UIImage? let imageOrientation: UIImage.Orientation = .up if fullSizeComplete, let data = try? Data(contentsOf: URL(fileURLWithPath: fullSizePath)) { let renderSize: CGSize if stickToTop { renderSize = .zero } else { renderSize = CGSize(width: 90.0, height: 90.0) } fullSizeImage = renderPreparedImage(data, renderSize, .clear, UIScreenScale, false) if let image = fullSizeImage { fittedSize = image.size.aspectFitted(arguments.boundingSize) } } var 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) if stickToTop { fittedRect.origin.y = drawingRect.size.height - fittedSize.height } context.withFlippedContext { c in if let fullSizeImage = fullSizeImage?.cgImage { 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], immediateThumbnailData: Data?, autoFetchFullSize: Bool = false, attemptSynchronously: Bool = false, skipThumbnail: Bool = false) -> Signal, NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.representation })), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) { let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: attemptSynchronously) 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 = immediateThumbnailData.flatMap(decodeTinyThumbnail) let fetchedThumbnail: Signal if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[smallestIndex].reference) } let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[largestIndex].reference) let thumbnail = Signal { subscriber in if let decodedThumbnailData = decodedThumbnailData { subscriber.putNext(decodedThumbnailData) subscriber.putCompletion() return EmptyDisposable } else { let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: attemptSynchronously).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, attemptSynchronously: attemptSynchronously).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) } } if skipThumbnail { return fullSizeData |> mapToSignal { value -> Signal , NoError> in if value._1 { return .single(Tuple(nil, value._0, value._1)) } else { return .complete() } } } else { return thumbnail |> mapToSignal { thumbnailData in return fullSizeData |> map { value in return Tuple(thumbnailData, value._0, value._1) } } } } } return signal } else { return .never() } } public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepresentationWithReference], immediateThumbnailData: Data?, autoFetchFullSize: Bool = false, attemptSynchronously: Bool = false, skipThumbnail: Bool = false, skipBlurIfLarge: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = avatarGalleryPhotoDatas(account: account, representations: representations, immediateThumbnailData: immediateThumbnailData, autoFetchFullSize: autoFetchFullSize, attemptSynchronously: attemptSynchronously, skipThumbnail: skipThumbnail) 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, !skipThumbnail { let maxThumbnailSide = max(thumbnailImage.width, thumbnailImage.height) if maxThumbnailSide > 200 || (maxThumbnailSide > 120 && maxThumbnailSide < 200 && skipBlurIfLarge) { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { 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) if 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 if 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() } } } } guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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 = .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 chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return account.postbox.mediaBox.resourceData(file.resource) |> map { fullSizeData in return { arguments in guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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) public func albumArtThumbnailData(engine: TelegramEngine, thumbnail: ExternalMusicAlbumArtResource, attemptSynchronously: Bool = false) -> Signal { return engine.resources.custom( id: thumbnail.id.stringRepresentation, fetch: EngineMediaResource.Fetch { return fetchExternalMusicAlbumArtResource(engine: engine, file: thumbnail.file, resource: thumbnail) }, attemptSynchronously: attemptSynchronously ) |> mapToSignal { data in if data.isComplete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: []) return .single(loadedData) } else { return .single(nil) } } |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs == nil && rhs == nil { return true } else { return false } }) } private func albumArtFullSizeDatas(engine: TelegramEngine, file: FileMediaReference?, thumbnail: ExternalMusicAlbumArtResource, fullSize: ExternalMusicAlbumArtResource, autoFetchFullSize: Bool = true) -> Signal, NoError> { return engine.resources.custom( id: fullSize.id.stringRepresentation, fetch: nil, attemptSynchronously: false ) |> take(1) |> mapToSignal { data in if data.isComplete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: []) return .single(Tuple(nil, loadedData, true)) } else { return combineLatest( engine.resources.custom( id: thumbnail.id.stringRepresentation, fetch: EngineMediaResource.Fetch { return fetchExternalMusicAlbumArtResource(engine: engine, file: file, resource: thumbnail) }, attemptSynchronously: false ), engine.resources.custom( id: fullSize.id.stringRepresentation, fetch: autoFetchFullSize ? EngineMediaResource.Fetch { return fetchExternalMusicAlbumArtResource(engine: engine, file: file, resource: fullSize) } : nil, attemptSynchronously: false ) ) |> mapToSignal { thumbnail, fullSize in if fullSize.isComplete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: fullSize.path), options: []) return .single(Tuple(nil, loadedData, true)) } else if thumbnail.isComplete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: thumbnail.path), options: []) return .single(Tuple(loadedData, nil, false)) } else { return .single(Tuple(nil, 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 } }) } 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, engine: TelegramEngine, fileReference: FileMediaReference?, albumArt: SharedMediaPlaybackAlbumArt?, thumbnail: Bool, overlayColor: UIColor? = nil, emptyColor: UIColor? = nil, drawPlaceholderWhenEmpty: Bool = true, attemptSynchronously: Bool = false) -> 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, userLocation: .other, userContentType: .image, reference: fileReference.resourceReference(thumbnailResource)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource, attemptSynchronously: attemptSynchronously).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(engine: engine, thumbnail: albumArt.thumbnailResource, attemptSynchronously: attemptSynchronously) |> map { thumbnailData in return Tuple(thumbnailData, nil, false) } } else { immediateArtworkData = albumArtFullSizeDatas(engine: engine, file: fileReference, 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 guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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)) if let overlayColor = overlayColor { c.setFillColor(overlayColor.cgColor) c.fill(arguments.drawingRect) } } } else { if let emptyColor = emptyColor { context.withFlippedContext { c in let rect = arguments.drawingRect c.setFillColor(emptyColor.cgColor) c.fill(rect) } } else if drawPlaceholderWhenEmpty { context.withFlippedContext { c in drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) } } else { return nil } } 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 { guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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 callDefaultBackground() -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return .single({ arguments in guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } 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 }) } public func solidColorImage(_ color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return .single({ arguments in guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } context.withFlippedContext { c in c.setFillColor(color.withAlphaComponent(1.0).cgColor) c.fill(arguments.drawingRect) } addCorners(context, arguments: arguments) return context }) }