mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
3431 lines
192 KiB
Swift
3431 lines
192 KiB
Swift
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<Int64>? {
|
|
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<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, 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<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, 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<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, 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<FetchResourceSourceType, FetchResourceError>
|
|
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<Data?, NoError> { 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<Tuple2<Data?, Bool>, NoError>
|
|
|
|
if autoFetchFullSize && !useMiniThumbnailIfAvailable {
|
|
fullSizeData = Signal<Tuple2<Data?, Bool>, 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<Data?, Bool> 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<Tuple3<Data?, String?, Bool>, NoError> {
|
|
let thumbnailResource = fetched ? nil : smallestImageRepresentation(fileReference.media.previewRepresentations)?.resource
|
|
let fullSizeResource = fileReference.media.resource
|
|
|
|
let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource, pathExtension: pathExtension)
|
|
let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail)
|
|
|
|
let signal = maybeFullSize
|
|
|> take(1)
|
|
|> mapToSignal { maybeData -> Signal<Tuple3<Data?, String?, Bool>, NoError> in
|
|
if maybeData.complete {
|
|
return .single(Tuple(nil, maybeData.path, true))
|
|
} else {
|
|
let fetchedThumbnail: Signal<FetchResourceSourceType, FetchResourceError>
|
|
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<Data?, NoError>
|
|
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<String?, Bool> in
|
|
return Tuple(next.size == 0 ? nil : next.path, next.complete)
|
|
}
|
|
|
|
return thumbnail
|
|
|> mapToSignal { thumbnailData in
|
|
return fullSizeDataAndPath
|
|
|> map { value -> Tuple3<Data?, String?, Bool> in
|
|
return Tuple3<Data?, String?, Bool>(thumbnailData, value._0, value._1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|> filter({ $0._0 != nil || $0._1 != nil })
|
|
|
|
return signal
|
|
}
|
|
|
|
private let thumbnailGenerationMimeTypes: Set<String> = 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<Tuple3<Data?, String?, Bool>, 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<FetchResourceSourceType, FetchResourceError> = 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<Tuple3<Data?, String?, Bool>, NoError> in
|
|
if maybeData.complete {
|
|
return .single(Tuple(nil, maybeData.path, true))
|
|
} else {
|
|
let fetchedThumbnail: Signal<FetchResourceSourceType, FetchResourceError>
|
|
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<Data?, NoError>
|
|
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 -> Tuple2<String?, Bool>in
|
|
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<Int64>)?, NoError> {
|
|
let playlistData: Signal<Range<Int64>?, 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<Int>] = []
|
|
|
|
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<Int64>)? 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<Int64>), NoError>? = nil, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal<Tuple3<Data?, Tuple2<Data, String>?, 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<MediaResourceData, NoError>] = []
|
|
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<Tuple3<Data?, Tuple2<Data, String>?, 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<Int64>)?], 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<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError> in
|
|
let possibleReadyPrefixes = previewPrefixes.compactMap { possiblePrefix -> Signal<MediaResourceData, NoError>? 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<Tuple3<Data?, Tuple2<Data, String>?, 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<Tuple3<Data?, Tuple2<Data, String>?, 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<Tuple3<Data?, Tuple2<Data, String>?, 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<MediaResourceData, NoError>
|
|
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<MediaResourceData, NoError> = .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<Tuple3<Data?, Tuple2<Data, String>?, 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<Data?, NoError>
|
|
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<MediaResourceData, NoError> { 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<Tuple2<Data, String>?, 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<MediaResourceData, NoError> { subscriber in
|
|
let dataDisposable = fetchedReducedSize.start(next: { next in
|
|
subscriber.putNext(next)
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
return ActionDisposable {
|
|
dataDisposable.dispose()
|
|
}
|
|
}
|
|
|> map { next -> Tuple2<Tuple2<Data, String>?, 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<UIImage?, NoError> {
|
|
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<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, 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<Tuple3<(Data, Bool)?, Data?, Bool>, NoError> {
|
|
let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0)
|
|
if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(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<Tuple3<(Data, Bool)?, Data?, Bool>, 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<Tuple2<Data?, Bool>, NoError> = fetchedFullSize
|
|
|> map { next -> Tuple2<Data?, Bool> 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<Tuple3<Data?, Data?, Bool>, 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<Tuple3<Data?, Data?, Bool>, 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<Data?, NoError> { 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<Tuple2<Data?, Bool>, NoError>
|
|
|
|
if autoFetchFullSize {
|
|
fullSizeData = Signal<Tuple2<Data?, Bool>, 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<Data?, Bool> 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<MediaResourceData, NoError> { 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<Int64>), 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<Tuple3<Data?, Tuple2<Data, String>?, 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<Data?, Tuple2<Data, String>?, 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<MediaResourceStatus, NoError> {
|
|
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<Int64>(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<FetchResourceSourceType, FetchResourceError> {
|
|
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<FetchResourceSourceType, FetchResourceError> in
|
|
return .single(type)
|
|
}
|
|
} else {
|
|
return .never()
|
|
}
|
|
}
|
|
|
|
public func chatMessagePhotoInteractiveFetched(context: AccountContext, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, displayAtSize: Int?, storeToDownloadsPeerId: EnginePeer.Id?) -> Signal<Never, NoError> {
|
|
if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) {
|
|
var fetchRange: (Range<Int64>, 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<FetchResourceSourceType, FetchResourceError> in
|
|
if case .remote = type, let peerId = storeToDownloadsPeerId {
|
|
return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerId: peerId)
|
|
|> castError(FetchResourceError.self)
|
|
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
|
}
|
|
|> then(.single(type))
|
|
}
|
|
return .single(type)
|
|
}
|
|
|> ignoreValues
|
|
|> `catch` { _ -> Signal<Never, NoError> 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<FetchResourceSourceType, FetchResourceError> {
|
|
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<Data?, NoError> {
|
|
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<Data?, NoError> {
|
|
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<Data?, NoError> {
|
|
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<Data?, NoError> { 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<Tuple3<Data?, String?, Bool>, 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<Bool, NoError> {
|
|
let signal = Signal<Bool, NoError> { 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<MediaResourceData, NoError>
|
|
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<Tuple3<Data?, Data?, Bool>, 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<Tuple3<Data?, Data?, Bool>, 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<FetchResourceSourceType, FetchResourceError>
|
|
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<Data?, NoError> { 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<Tuple2<Data?, Bool>, NoError>
|
|
|
|
if autoFetchFullSize {
|
|
fullSizeData = Signal<Tuple2<Data?, Bool>, 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<Data?, Bool> in
|
|
return Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)
|
|
}
|
|
}
|
|
|
|
if skipThumbnail {
|
|
return fullSizeData |> mapToSignal { value -> Signal <Tuple3<Data?, Data?, Bool>, 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<UIImage?>(value: nil)
|
|
|
|
public func albumArtThumbnailData(engine: TelegramEngine, thumbnail: ExternalMusicAlbumArtResource, attemptSynchronously: Bool = false) -> Signal<Data?, NoError> {
|
|
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<Tuple3<Data?, Data?, Bool>, 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<Data?, NoError> = .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<Tuple3<Data?, Data?, Bool>, 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<Data?, NoError> { 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<MediaResourceData, NoError> { 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
|
|
})
|
|
}
|