Spoiler improvements

This commit is contained in:
Ilya Laktyushin 2022-12-18 02:48:34 +04:00
parent c7532e61e0
commit 8cdd2076d4
3 changed files with 219 additions and 17 deletions

View File

@ -10,7 +10,47 @@ import MozjpegBinding
import Accelerate
import ManagedFile
private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
private 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 generateBlurredThumbnail(image: UIImage, adjustSaturation: Bool = false) -> UIImage? {
let thumbnailContextSize = CGSize(width: 32.0, height: 32.0)
guard let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) else {
return nil
@ -24,6 +64,10 @@ private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
}
telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes)
if adjustSaturation {
adjustSaturationInContext(context: thumbnailContext, saturation: 1.7)
}
return thumbnailContext.generateImage()
}
@ -158,10 +202,12 @@ private func loadImage(data: Data) -> UIImage? {
public final class DirectMediaImageCache {
public final class GetMediaResult {
public let image: UIImage?
public let blurredImage: UIImage?
public let loadSignal: Signal<UIImage?, NoError>?
init(image: UIImage?, loadSignal: Signal<UIImage?, NoError>?) {
init(image: UIImage?, blurredImage: UIImage? = nil, loadSignal: Signal<UIImage?, NoError>?) {
self.image = image
self.blurredImage = blurredImage
self.loadSignal = loadSignal
}
}
@ -284,7 +330,7 @@ public final class DirectMediaImageCache {
return self.getProgressiveSize(mediaReference: MediaReference.message(message: MessageReference(message), media: file).abstract, width: width, representations: file.previewRepresentations)
}
private func getImageSynchronous(message: Message, userLocation: MediaResourceUserLocation, media: Media, width: Int, possibleWidths: [Int]) -> GetMediaResult? {
private func getImageSynchronous(message: Message, userLocation: MediaResourceUserLocation, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool) -> GetMediaResult? {
var immediateThumbnailData: Data?
var resource: (resource: MediaResourceReference, size: Int64)?
if let image = media as? TelegramMediaImage {
@ -298,39 +344,50 @@ public final class DirectMediaImageCache {
guard let resource = resource else {
return nil
}
var resultImage: UIImage?
var blurredImage: UIImage?
for otherWidth in possibleWidths.reversed() {
if otherWidth == width {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth)))), let image = loadImage(data: data) {
return GetMediaResult(image: image, loadSignal: nil)
if blurredImage == nil, includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) {
blurredImage = blurredImageValue
}
return GetMediaResult(image: image, blurredImage: blurredImage, loadSignal: nil)
}
} else {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth)))), let image = loadImage(data: data) {
blurredImage = image
resultImage = image
}
}
}
if blurredImage == nil {
if resultImage == nil {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) {
blurredImage = image
resultImage = image
} else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) {
if let blurredImageValue = generateBlurredThumbnail(image: image) {
resultImage = blurredImageValue
}
if includeBlurred, let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) {
blurredImage = blurredImageValue
}
}
}
if blurredImage == nil, includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) {
blurredImage = blurredImageValue
}
return GetMediaResult(image: blurredImage, loadSignal: self.getLoadSignal(width: width, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size))
return GetMediaResult(image: resultImage, blurredImage: blurredImage, loadSignal: self.getLoadSignal(width: width, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size))
}
public func getImage(message: Message, media: Media, width: Int, possibleWidths: [Int], synchronous: Bool) -> GetMediaResult? {
public func getImage(message: Message, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool = false, synchronous: Bool) -> GetMediaResult? {
if synchronous {
return self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths)
return self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths, includeBlurred: includeBlurred)
} else {
return GetMediaResult(image: nil, loadSignal: Signal { subscriber in
let result = self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths)
return GetMediaResult(image: nil, blurredImage: nil, loadSignal: Signal { subscriber in
let result = self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths, includeBlurred: includeBlurred)
guard let result = result else {
subscriber.putNext(nil)
subscriber.putCompletion()

View File

@ -6,6 +6,89 @@ import Display
import AppBundle
import LegacyComponents
public class MediaDustLayer: CALayer {
private var emitter: CAEmitterCell?
private var emitterLayer: CAEmitterLayer?
private var size: CGSize?
override public init() {
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupEmitterLayerIfNeeded() {
guard self.emitterLayer == nil else {
return
}
let emitter = CAEmitterCell()
emitter.color = UIColor(rgb: 0xffffff, alpha: 0.0).cgColor
emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage
emitter.contentsScale = 1.8
emitter.emissionRange = .pi * 2.0
emitter.lifetime = 8.0
emitter.scale = 0.5
emitter.velocityRange = 0.0
emitter.name = "dustCell"
emitter.alphaRange = 1.0
emitter.setValue("point", forKey: "particleType")
emitter.setValue(1.0, forKey: "mass")
emitter.setValue(0.01, forKey: "massRange")
self.emitter = emitter
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive")
let scaleBehavior = createEmitterBehavior(type: "valueOverLife")
scaleBehavior.setValue("scale", forKey: "keyPath")
scaleBehavior.setValue([0.0, 0.5], forKey: "values")
scaleBehavior.setValue([0.0, 0.05], forKey: "locations")
let behaviors = [alphaBehavior, scaleBehavior]
let emitterLayer = CAEmitterLayer()
emitterLayer.masksToBounds = true
emitterLayer.allowsGroupOpacity = true
emitterLayer.lifetime = 1
emitterLayer.emitterCells = [emitter]
emitterLayer.seed = arc4random()
emitterLayer.emitterShape = .rectangle
emitterLayer.setValue(behaviors, forKey: "emitterBehaviors")
self.addSublayer(emitterLayer)
self.emitterLayer = emitterLayer
}
private func updateEmitter() {
guard let size = self.size else {
return
}
self.setupEmitterLayerIfNeeded()
self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size)
self.emitterLayer?.emitterSize = size
self.emitterLayer?.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
let square = Float(size.width * size.height)
Queue.mainQueue().async {
self.emitter?.birthRate = min(100000.0, square * 0.02)
}
}
public func updateLayout(size: CGSize) {
self.size = size
self.updateEmitter()
}
}
public class MediaDustNode: ASDisplayNode {
private var currentParams: (size: CGSize, color: UIColor)?
private var animColor: CGColor?

View File

@ -24,6 +24,7 @@ import TelegramUIPreferences
import CheckNode
import AppBundle
import ChatControllerInteraction
import InvisibleInkDustNode
private final class FrameSequenceThumbnailNode: ASDisplayNode {
private let context: AccountContext
@ -776,9 +777,11 @@ private protocol ItemLayer: SparseItemGridLayer {
var disposable: Disposable? { get set }
var hasContents: Bool { get set }
func setSpoilerContents(_ contents: Any?)
func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat)
func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool)
func updateHasSpoiler(hasSpoiler: Bool)
func bind(item: VisualMediaItem)
func unbind()
@ -789,6 +792,7 @@ private final class GenericItemLayer: CALayer, ItemLayer {
var durationLayer: DurationLayer?
var minFactor: CGFloat = 1.0
var selectionLayer: GridMessageSelectionLayer?
var dustLayer: MediaDustLayer?
var disposable: Disposable?
var hasContents: Bool = false
@ -816,6 +820,12 @@ private final class GenericItemLayer: CALayer, ItemLayer {
self.contents = image.cgImage
}
}
func setSpoilerContents(_ contents: Any?) {
if let image = contents as? UIImage {
self.dustLayer?.contents = image.cgImage
}
}
override func action(forKey event: String) -> CAAction? {
return nullAction
@ -873,6 +883,24 @@ private final class GenericItemLayer: CALayer, ItemLayer {
}
}
}
func updateHasSpoiler(hasSpoiler: Bool) {
if hasSpoiler {
if let _ = self.dustLayer {
} else {
let dustLayer = MediaDustLayer()
self.dustLayer = dustLayer
self.addSublayer(dustLayer)
if !self.bounds.isEmpty {
dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
dustLayer.updateLayout(size: self.bounds.size)
}
}
} else if let dustLayer = self.dustLayer {
self.dustLayer = nil
dustLayer.removeFromSuperlayer()
}
}
func unbind() {
self.item = nil
@ -894,6 +922,7 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
var durationLayer: DurationLayer?
var minFactor: CGFloat = 1.0
var selectionLayer: GridMessageSelectionLayer?
var dustLayer: MediaDustLayer?
var disposable: Disposable?
var hasContents: Bool = false
@ -935,6 +964,12 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
}
}
}
func setSpoilerContents(_ contents: Any?) {
if let image = contents as? UIImage {
self.dustLayer?.contents = image.cgImage
}
}
func bind(item: VisualMediaItem) {
self.item = item
@ -988,6 +1023,24 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
}
}
}
func updateHasSpoiler(hasSpoiler: Bool) {
if hasSpoiler {
if let _ = self.dustLayer {
} else {
let dustLayer = MediaDustLayer()
self.dustLayer = dustLayer
self.addSublayer(dustLayer)
if !self.bounds.isEmpty {
dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
dustLayer.updateLayout(size: self.bounds.size)
}
}
} else if let dustLayer = self.dustLayer {
self.dustLayer = nil
dustLayer.removeFromSuperlayer()
}
}
func unbind() {
self.item = nil
@ -1194,6 +1247,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
var onBeginFastScrollingImpl: (() -> Void)?
var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)?
var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)?
var revealedSpoilerMessageIds = Set<MessageId>()
private var shimmerImages: [CGFloat: UIImage] = [:]
@ -1395,7 +1450,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
}
let message = item.message
let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id)
layer.updateHasSpoiler(hasSpoiler: hasSpoiler)
var selectedMedia: Media?
for media in message.media {
if let image = media as? TelegramMediaImage {
@ -1408,7 +1465,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
}
if let selectedMedia = selectedMedia {
if let result = directMediaImageCache.getImage(message: message, media: selectedMedia, width: imageWidthSpec, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, synchronous: synchronous == .full) {
if let result = directMediaImageCache.getImage(message: message, media: selectedMedia, width: imageWidthSpec, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, includeBlurred: hasSpoiler, synchronous: synchronous == .full) {
if let image = result.image {
layer.setContents(image)
switch synchronous {
@ -1423,6 +1480,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
layer.hasContents = true
}
}
if let image = result.blurredImage {
layer.setSpoilerContents(image)
}
if let loadSignal = result.loadSignal {
layer.disposable?.dispose()
let startTimestamp = CFAbsoluteTimeGetCurrent()
@ -1494,7 +1554,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
} else {
layer.updateSelection(theme: self.checkNodeTheme, isSelected: nil, animated: false)
}
layer.bind(item: item)
}
}
@ -1664,7 +1724,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, contentType: ContentType, captureProtected: Bool) {
self.context = context
self.peerId = peerId
@ -2316,6 +2376,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
if let item = itemLayer.item {
if self.itemInteraction.hiddenMedia[item.message.id] != nil {
itemLayer.isHidden = true
itemLayer.updateHasSpoiler(hasSpoiler: false)
self.itemGridBinding.revealedSpoilerMessageIds.insert(item.message.id)
} else {
itemLayer.isHidden = false
}