mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Media editor improvements
This commit is contained in:
@@ -105,6 +105,7 @@ swift_library(
|
|||||||
"//submodules/ReactionSelectionNode",
|
"//submodules/ReactionSelectionNode",
|
||||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||||
"//submodules/Camera",
|
"//submodules/Camera",
|
||||||
|
"//submodules/TelegramUI/Components/DustEffect",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@@ -3149,13 +3149,27 @@ public final class DrawingToolsInteraction {
|
|||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if isRectangleImage {
|
if isRectangleImage {
|
||||||
actions.append(ContextMenuAction(content: .text(title: "Cut Out", accessibilityLabel: "Cut Out"), action: { [weak self, weak entityView] in
|
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_CutOut, accessibilityLabel: presentationData.strings.Paint_CutOut), action: { [weak self, weak entityView] in
|
||||||
if let self, let entityView, let entity = entityView.entity as? DrawingStickerEntity, case let .image(image, _) = entity.content {
|
if let self, let entityView, let entity = entityView.entity as? DrawingStickerEntity, case let .image(image, _) = entity.content {
|
||||||
let _ = (cutoutStickerImage(from: image)
|
let _ = (cutoutStickerImage(from: image)
|
||||||
|> deliverOnMainQueue).start(next: { result in
|
|> deliverOnMainQueue).start(next: { [weak entity] result in
|
||||||
if let result {
|
if let result, let entity {
|
||||||
let newEntity = DrawingStickerEntity(content: .image(result, .sticker))
|
let newEntity = DrawingStickerEntity(content: .image(result, .sticker))
|
||||||
self.insertEntity(newEntity)
|
newEntity.referenceDrawingSize = entity.referenceDrawingSize
|
||||||
|
newEntity.scale = entity.scale
|
||||||
|
newEntity.position = entity.position
|
||||||
|
newEntity.rotation = entity.rotation
|
||||||
|
newEntity.mirrored = entity.mirrored
|
||||||
|
let newEntityView = self.entitiesView.add(newEntity)
|
||||||
|
|
||||||
|
if let newEntityView = newEntityView as? DrawingStickerEntityView {
|
||||||
|
newEntityView.playCutoffAnimation()
|
||||||
|
}
|
||||||
|
self.entitiesView.selectEntity(newEntity, animate: false)
|
||||||
|
if let entityView = entityView as? DrawingStickerEntityView {
|
||||||
|
entityView.playDissolveAnimation()
|
||||||
|
self.entitiesView.remove(uuid: entity.uuid, animated: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -3198,6 +3212,11 @@ public final class DrawingToolsInteraction {
|
|||||||
self.insertEntity(DrawingStickerEntity(content: .image(image, isSticker ? .sticker : .rectangle)), scale: 2.5)
|
self.insertEntity(DrawingStickerEntity(content: .image(image, isSticker ? .sticker : .rectangle)), scale: 2.5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
textEntityView.replaceWithAnimatedImage = { [weak self] data, thumbnailImage in
|
||||||
|
if let self {
|
||||||
|
self.insertEntity(DrawingStickerEntity(content: .animatedImage(data, thumbnailImage)), scale: 2.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if self.isVideo {
|
if self.isVideo {
|
||||||
entityView.seek(to: 0.0)
|
entityView.seek(to: 0.0)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import MediaEditor
|
|||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUniversalVideoContent
|
import TelegramUniversalVideoContent
|
||||||
|
import DustEffect
|
||||||
|
|
||||||
private class BlurView: UIVisualEffectView {
|
private class BlurView: UIVisualEffectView {
|
||||||
private func setup() {
|
private func setup() {
|
||||||
@@ -65,6 +66,7 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
let imageNode: TransformImageNode
|
let imageNode: TransformImageNode
|
||||||
var animationNode: DefaultAnimatedStickerNodeImpl?
|
var animationNode: DefaultAnimatedStickerNodeImpl?
|
||||||
var videoNode: UniversalVideoNode?
|
var videoNode: UniversalVideoNode?
|
||||||
|
var animatedImageView: UIImageView?
|
||||||
var cameraPreviewView: UIView?
|
var cameraPreviewView: UIView?
|
||||||
|
|
||||||
let progressDisposable = MetaDisposable()
|
let progressDisposable = MetaDisposable()
|
||||||
@@ -143,6 +145,8 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||||
case let .image(image, _):
|
case let .image(image, _):
|
||||||
return image.size
|
return image.size
|
||||||
|
case let .animatedImage(_, thumbnailImage):
|
||||||
|
return thumbnailImage.size
|
||||||
case let .video(file):
|
case let .video(file):
|
||||||
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||||
case .dualVideoReference:
|
case .dualVideoReference:
|
||||||
@@ -297,6 +301,14 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
self.videoNode = videoNode
|
self.videoNode = videoNode
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
videoNode.play()
|
videoNode.play()
|
||||||
|
} else if case let .animatedImage(data, thumbnailImage) = self.stickerEntity.content {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.image = thumbnailImage
|
||||||
|
imageView.setDrawingAnimatedImage(data: data)
|
||||||
|
self.animatedImageView = imageView
|
||||||
|
self.addSubview(imageView)
|
||||||
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +472,106 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func playDissolveAnimation(completion: @escaping () -> Void = {}) {
|
||||||
|
guard let containerView = self.containerView, case let .image(image, _) = self.stickerEntity.content else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dustEffectLayer = DustEffectLayer()
|
||||||
|
dustEffectLayer.position = containerView.bounds.center
|
||||||
|
dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: containerView.bounds.size)
|
||||||
|
dustEffectLayer.animationSpeed = 2.2
|
||||||
|
dustEffectLayer.becameEmpty = { [weak dustEffectLayer] in
|
||||||
|
dustEffectLayer?.removeFromSuperlayer()
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
containerView.layer.insertSublayer(dustEffectLayer, below: self.layer)
|
||||||
|
|
||||||
|
let itemFrame = self.layer.convert(self.bounds, to: dustEffectLayer)
|
||||||
|
dustEffectLayer.addItem(frame: itemFrame, image: image)
|
||||||
|
|
||||||
|
self.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func playCutoffAnimation() {
|
||||||
|
let values = [self.entity.scale, self.entity.scale * 1.1, self.entity.scale]
|
||||||
|
let keyTimes = [0.0, 0.67, 1.0]
|
||||||
|
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.35, keyPath: "transform.scale")
|
||||||
|
// func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
|
||||||
|
// let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
||||||
|
//
|
||||||
|
// let rgen = { () -> CGFloat in
|
||||||
|
// let accuracy: UInt32 = 1000
|
||||||
|
// let random = arc4random_uniform(accuracy)
|
||||||
|
// return CGFloat(random) / CGFloat(accuracy)
|
||||||
|
// }
|
||||||
|
// let rangeStart: CGFloat = 1 / (1 + randomness / 10)
|
||||||
|
//
|
||||||
|
// let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100)
|
||||||
|
// let points = (0 ..< pointsCount).map { i -> CGPoint in
|
||||||
|
// let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
|
||||||
|
// let angleRandomness: CGFloat = angle * 0.1
|
||||||
|
// let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5)
|
||||||
|
// let pointX = sin(startAngle + CGFloat(i) * randAngle)
|
||||||
|
// let pointY = cos(startAngle + CGFloat(i) * randAngle)
|
||||||
|
// return CGPoint(
|
||||||
|
// x: pointX * randPointOffset,
|
||||||
|
// y: pointY * randPointOffset
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// return points
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func generateNextBlob(for size: CGSize) -> [CGPoint] {
|
||||||
|
// let pointsCount = 8
|
||||||
|
// let minRandomness = 1.0
|
||||||
|
// let maxRandomness = 1.0
|
||||||
|
// let speedLevel = 0.8
|
||||||
|
// let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
|
||||||
|
// return blob(pointsCount: pointsCount, randomness: randomness)
|
||||||
|
// .map {
|
||||||
|
// return CGPoint(
|
||||||
|
// x: $0.x * CGFloat(size.width),
|
||||||
|
// y: $0.y * CGFloat(size.height)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// guard case let .image(image, _) = self.stickerEntity.content else {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// let maskView = UIImageView()
|
||||||
|
// maskView.frame = self.bounds
|
||||||
|
// maskView.image = image
|
||||||
|
// self.mask = maskView
|
||||||
|
//
|
||||||
|
// let blobLayer = CAShapeLayer()
|
||||||
|
// blobLayer.strokeColor = UIColor.red.cgColor
|
||||||
|
// blobLayer.fillColor = UIColor.clear.cgColor
|
||||||
|
// blobLayer.lineWidth = 2.0
|
||||||
|
// blobLayer.shadowRadius = 3.0
|
||||||
|
// blobLayer.shadowOpacity = 0.8
|
||||||
|
// blobLayer.shadowColor = UIColor.white.cgColor
|
||||||
|
// blobLayer.position = CGPoint(
|
||||||
|
// x: CGFloat.random(in: self.bounds.width * 0.33 ..< self.bounds.width * 0.5),
|
||||||
|
// y: self.bounds.height * 0.5
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// let minSide = min(self.bounds.width, self.bounds.height)
|
||||||
|
// let size = CGSize(width: minSide * 0.5, height: minSide * 0.5)
|
||||||
|
// blobLayer.bounds = CGRect(origin: .zero, size: size)
|
||||||
|
//
|
||||||
|
// let points = generateNextBlob(for: size)
|
||||||
|
// blobLayer.path = UIBezierPath.smoothCurve(through: points, length: size.width).cgPath
|
||||||
|
// self.layer.addSublayer(blobLayer)
|
||||||
|
//
|
||||||
|
// blobLayer.animateScale(from: 0.01, to: 3.0, duration: 1.0, removeOnCompletion: false, completion: { _ in
|
||||||
|
// blobLayer.removeFromSuperlayer()
|
||||||
|
// self.mask = nil
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
private var didApplyVisibility = false
|
private var didApplyVisibility = false
|
||||||
public override func layoutSubviews() {
|
public override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
@@ -496,6 +608,10 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
videoNode.updateLayout(size: imageSize, transition: .immediate)
|
videoNode.updateLayout(size: imageSize, transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let animatedImageView = self.animatedImageView {
|
||||||
|
animatedImageView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize)
|
||||||
|
}
|
||||||
|
|
||||||
if let cameraPreviewView = self.cameraPreviewView {
|
if let cameraPreviewView = self.cameraPreviewView {
|
||||||
cameraPreviewView.layer.cornerRadius = imageSize.width / 2.0
|
cameraPreviewView.layer.cornerRadius = imageSize.width / 2.0
|
||||||
cameraPreviewView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize)
|
cameraPreviewView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize)
|
||||||
@@ -557,15 +673,18 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
self.imageNode.transform = animationSourceTransform
|
self.imageNode.transform = animationSourceTransform
|
||||||
self.animationNode?.transform = animationSourceTransform
|
self.animationNode?.transform = animationSourceTransform
|
||||||
self.videoNode?.transform = animationSourceTransform
|
self.videoNode?.transform = animationSourceTransform
|
||||||
|
self.animatedImageView?.layer.transform = animationSourceTransform
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.25, animations: {
|
UIView.animate(withDuration: 0.25, animations: {
|
||||||
self.imageNode.transform = animationTargetTransform
|
self.imageNode.transform = animationTargetTransform
|
||||||
self.animationNode?.transform = animationTargetTransform
|
self.animationNode?.transform = animationTargetTransform
|
||||||
self.videoNode?.transform = animationTargetTransform
|
self.videoNode?.transform = animationTargetTransform
|
||||||
|
self.animatedImageView?.layer.transform = animationTargetTransform
|
||||||
}, completion: { finished in
|
}, completion: { finished in
|
||||||
self.imageNode.transform = staticTransform
|
self.imageNode.transform = staticTransform
|
||||||
self.animationNode?.transform = staticTransform
|
self.animationNode?.transform = staticTransform
|
||||||
self.videoNode?.transform = staticTransform
|
self.videoNode?.transform = staticTransform
|
||||||
|
self.animatedImageView?.layer.transform = staticTransform
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
CATransaction.begin()
|
CATransaction.begin()
|
||||||
@@ -573,6 +692,7 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
self.imageNode.transform = staticTransform
|
self.imageNode.transform = staticTransform
|
||||||
self.animationNode?.transform = staticTransform
|
self.animationNode?.transform = staticTransform
|
||||||
self.videoNode?.transform = staticTransform
|
self.videoNode?.transform = staticTransform
|
||||||
|
self.animatedImageView?.layer.transform = staticTransform
|
||||||
CATransaction.commit()
|
CATransaction.commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1029,3 +1149,111 @@ private final class StickerVideoDecoration: UniversalVideoDecoration {
|
|||||||
public func tap() {
|
public func tap() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension UIBezierPath {
|
||||||
|
static func smoothCurve(
|
||||||
|
through points: [CGPoint],
|
||||||
|
length: CGFloat
|
||||||
|
) -> UIBezierPath {
|
||||||
|
let angle = (CGFloat.pi * 2) / CGFloat(points.count)
|
||||||
|
let smoothness: CGFloat = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
||||||
|
|
||||||
|
var smoothPoints = [SmoothPoint]()
|
||||||
|
for index in (0 ..< points.count) {
|
||||||
|
let prevIndex = index - 1
|
||||||
|
let prev = points[prevIndex >= 0 ? prevIndex : points.count + prevIndex]
|
||||||
|
let curr = points[index]
|
||||||
|
let next = points[(index + 1) % points.count]
|
||||||
|
|
||||||
|
let angle: CGFloat = {
|
||||||
|
let dx = next.x - prev.x
|
||||||
|
let dy = -next.y + prev.y
|
||||||
|
let angle = atan2(dy, dx)
|
||||||
|
if angle < 0 {
|
||||||
|
return abs(angle)
|
||||||
|
} else {
|
||||||
|
return 2 * .pi - angle
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
smoothPoints.append(
|
||||||
|
SmoothPoint(
|
||||||
|
point: curr,
|
||||||
|
inAngle: angle + .pi,
|
||||||
|
inLength: smoothness * distance(from: curr, to: prev),
|
||||||
|
outAngle: angle,
|
||||||
|
outLength: smoothness * distance(from: curr, to: next)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultPath = UIBezierPath()
|
||||||
|
resultPath.move(to: smoothPoints[0].point)
|
||||||
|
for index in (0 ..< smoothPoints.count) {
|
||||||
|
let curr = smoothPoints[index]
|
||||||
|
let next = smoothPoints[(index + 1) % points.count]
|
||||||
|
let currSmoothOut = curr.smoothOut()
|
||||||
|
let nextSmoothIn = next.smoothIn()
|
||||||
|
resultPath.addCurve(to: next.point, controlPoint1: currSmoothOut, controlPoint2: nextSmoothIn)
|
||||||
|
}
|
||||||
|
resultPath.close()
|
||||||
|
return resultPath
|
||||||
|
}
|
||||||
|
|
||||||
|
static private func distance(from fromPoint: CGPoint, to toPoint: CGPoint) -> CGFloat {
|
||||||
|
return sqrt((fromPoint.x - toPoint.x) * (fromPoint.x - toPoint.x) + (fromPoint.y - toPoint.y) * (fromPoint.y - toPoint.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SmoothPoint {
|
||||||
|
let point: CGPoint
|
||||||
|
|
||||||
|
let inAngle: CGFloat
|
||||||
|
let inLength: CGFloat
|
||||||
|
|
||||||
|
let outAngle: CGFloat
|
||||||
|
let outLength: CGFloat
|
||||||
|
|
||||||
|
func smoothIn() -> CGPoint {
|
||||||
|
return smooth(angle: inAngle, length: inLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func smoothOut() -> CGPoint {
|
||||||
|
return smooth(angle: outAngle, length: outLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func smooth(angle: CGFloat, length: CGFloat) -> CGPoint {
|
||||||
|
return CGPoint(
|
||||||
|
x: point.x + length * cos(angle),
|
||||||
|
y: point.y + length * sin(angle)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
extension UIImageView {
|
||||||
|
func setDrawingAnimatedImage(data: Data) {
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
if let animatedImage = UIImage.animatedImageFromData(data: data) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setImage(with: animatedImage)
|
||||||
|
self.startAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setImage(with animatedImage: DrawingAnimatedImage) {
|
||||||
|
if let snapshotView = self.snapshotView(afterScreenUpdates: false) {
|
||||||
|
self.addSubview(snapshotView)
|
||||||
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||||
|
snapshotView.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.image = nil
|
||||||
|
self.animationImages = animatedImage.images
|
||||||
|
self.animationDuration = animatedImage.duration
|
||||||
|
self.animationRepeatCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||||||
|
|
||||||
var textChanged: () -> Void = {}
|
var textChanged: () -> Void = {}
|
||||||
var replaceWithImage: (UIImage, Bool) -> Void = { _, _ in }
|
var replaceWithImage: (UIImage, Bool) -> Void = { _, _ in }
|
||||||
|
var replaceWithAnimatedImage: (Data, UIImage) -> Void = { _, _ in }
|
||||||
|
|
||||||
init(context: AccountContext, entity: DrawingTextEntity) {
|
init(context: AccountContext, entity: DrawingTextEntity) {
|
||||||
self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true)
|
self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true)
|
||||||
@@ -103,8 +104,13 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||||||
var images: [UIImage] = []
|
var images: [UIImage] = []
|
||||||
var isPNG = false
|
var isPNG = false
|
||||||
var isMemoji = false
|
var isMemoji = false
|
||||||
|
var animatedImageData: Data?
|
||||||
for item in pasteboard.items {
|
for item in pasteboard.items {
|
||||||
if let image = item["com.apple.png-sticker"] as? UIImage {
|
print(item.keys)
|
||||||
|
if let data = item["public.heics"] as? Data, let image = item[kUTTypePNG as String] as? UIImage {
|
||||||
|
animatedImageData = data
|
||||||
|
images.append(image)
|
||||||
|
} else if let imageData = item["com.apple.png-sticker"] as? Data, let image = UIImage(data: imageData) {
|
||||||
images.append(image)
|
images.append(image)
|
||||||
isPNG = true
|
isPNG = true
|
||||||
isMemoji = true
|
isMemoji = true
|
||||||
@@ -121,6 +127,12 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let animatedImageData, let image = images.first {
|
||||||
|
self.endEditing(reset: true)
|
||||||
|
self.replaceWithAnimatedImage(animatedImageData, image)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage {
|
if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage {
|
||||||
let maxSide = max(image.size.width, image.size.height)
|
let maxSide = max(image.size.width, image.size.height)
|
||||||
if maxSide.isZero {
|
if maxSide.isZero {
|
||||||
|
|||||||
@@ -544,3 +544,59 @@ extension CATransform3D {
|
|||||||
return (t, r, s)
|
return (t, r, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension UIImage {
|
||||||
|
class func animatedImageFromData(data: Data) -> DrawingAnimatedImage? {
|
||||||
|
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = CGImageSourceGetCount(source)
|
||||||
|
var images = [UIImage]()
|
||||||
|
var duration = 0.0
|
||||||
|
|
||||||
|
for i in 0..<count {
|
||||||
|
if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
|
||||||
|
let image = UIImage(cgImage: cgImage)
|
||||||
|
images.append(image)
|
||||||
|
|
||||||
|
let delaySeconds = UIImage.delayForImageAtIndex(Int(i),
|
||||||
|
source: source)
|
||||||
|
duration += delaySeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DrawingAnimatedImage(images: images, duration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
class func delayForImageAtIndex(_ index: Int, source: CGImageSource!) -> Double {
|
||||||
|
var delay = 0.0
|
||||||
|
|
||||||
|
let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil)
|
||||||
|
let gifPropertiesPointer = UnsafeMutablePointer<UnsafeRawPointer?>.allocate(capacity: 0)
|
||||||
|
if CFDictionaryGetValueIfPresent(cfProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque(), gifPropertiesPointer) == false {
|
||||||
|
return delay
|
||||||
|
}
|
||||||
|
|
||||||
|
let gifProperties:CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self)
|
||||||
|
|
||||||
|
var delayObject: AnyObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()), to: AnyObject.self)
|
||||||
|
if delayObject.doubleValue == 0 {
|
||||||
|
delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()), to: AnyObject.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = delayObject as? Double ?? 0
|
||||||
|
|
||||||
|
return delay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class DrawingAnimatedImage {
|
||||||
|
public let images: [UIImage]
|
||||||
|
public let duration: Double
|
||||||
|
|
||||||
|
init(images: [UIImage], duration: Double) {
|
||||||
|
self.images = images
|
||||||
|
self.duration = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Vision
|
import Vision
|
||||||
import CoreImage
|
import CoreImage
|
||||||
|
import CoreImage.CIFilterBuiltins
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import VideoToolbox
|
import VideoToolbox
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ func cutoutStickerImage(from image: UIImage) -> Signal<UIImage?, NoError> {
|
|||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
|
let ciContext = CIContext(options: nil)
|
||||||
|
let inputImage = CIImage(cgImage: cgImage)
|
||||||
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||||||
let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in
|
let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in
|
||||||
guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else {
|
guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else {
|
||||||
@@ -21,14 +24,21 @@ func cutoutStickerImage(from image: UIImage) -> Signal<UIImage?, NoError> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let instances = instances(atPoint: nil, inObservation: result)
|
let instances = instances(atPoint: nil, inObservation: result)
|
||||||
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler), let image = UIImage(pixelBuffer: mask) {
|
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) {
|
||||||
|
let filter = CIFilter.blendWithMask()
|
||||||
|
filter.inputImage = inputImage
|
||||||
|
filter.backgroundImage = CIImage(color: .clear)
|
||||||
|
filter.maskImage = CIImage(cvPixelBuffer: mask)
|
||||||
|
if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) {
|
||||||
|
let image = UIImage(cgImage: cgImage)
|
||||||
subscriber.putNext(image)
|
subscriber.putNext(image)
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
subscriber.putNext(nil)
|
subscriber.putNext(nil)
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
try? handler.perform([request])
|
try? handler.perform([request])
|
||||||
return ActionDisposable {
|
return ActionDisposable {
|
||||||
request.cancel()
|
request.cancel()
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity {
|
|||||||
case let .image(image, _):
|
case let .image(image, _):
|
||||||
self.file = nil
|
self.file = nil
|
||||||
self.imagePromise.set(.single(image))
|
self.imagePromise.set(.single(image))
|
||||||
|
case .animatedImage:
|
||||||
|
self.file = nil
|
||||||
case .video:
|
case .video:
|
||||||
self.file = nil
|
self.file = nil
|
||||||
case .dualVideoReference:
|
case .dualVideoReference:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
}
|
}
|
||||||
case file(TelegramMediaFile, FileType)
|
case file(TelegramMediaFile, FileType)
|
||||||
case image(UIImage, ImageType)
|
case image(UIImage, ImageType)
|
||||||
|
case animatedImage(Data, UIImage)
|
||||||
case video(TelegramMediaFile)
|
case video(TelegramMediaFile)
|
||||||
case dualVideoReference(Bool)
|
case dualVideoReference(Bool)
|
||||||
|
|
||||||
@@ -46,6 +47,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
case let .animatedImage(lhsData, lhsThumbnailImage):
|
||||||
|
if case let .animatedImage(rhsData, rhsThumbnailImage) = lhs {
|
||||||
|
return lhsData == rhsData && lhsThumbnailImage === rhsThumbnailImage
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
case let .video(lhsFile):
|
case let .video(lhsFile):
|
||||||
if case let .video(rhsFile) = rhs {
|
if case let .video(rhsFile) = rhs {
|
||||||
return lhsFile.fileId == rhsFile.fileId
|
return lhsFile.fileId == rhsFile.fileId
|
||||||
@@ -67,6 +74,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
case reaction
|
case reaction
|
||||||
case reactionStyle
|
case reactionStyle
|
||||||
case imagePath
|
case imagePath
|
||||||
|
case animatedImagePath
|
||||||
case videoFile
|
case videoFile
|
||||||
case isRectangle
|
case isRectangle
|
||||||
case isDualPhoto
|
case isDualPhoto
|
||||||
@@ -112,6 +120,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
switch self.content {
|
switch self.content {
|
||||||
case let .image(image, _):
|
case let .image(image, _):
|
||||||
dimensions = image.size
|
dimensions = image.size
|
||||||
|
case let .animatedImage(_, thumbnailImage):
|
||||||
|
dimensions = thumbnailImage.size
|
||||||
case let .file(file, type):
|
case let .file(file, type):
|
||||||
if case .reaction = type {
|
if case .reaction = type {
|
||||||
dimensions = CGSize(width: 512.0, height: 512.0)
|
dimensions = CGSize(width: 512.0, height: 512.0)
|
||||||
@@ -143,6 +153,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
}
|
}
|
||||||
case .image:
|
case .image:
|
||||||
return false
|
return false
|
||||||
|
case .animatedImage:
|
||||||
|
return true
|
||||||
case .video:
|
case .video:
|
||||||
return true
|
return true
|
||||||
case .dualVideoReference:
|
case .dualVideoReference:
|
||||||
@@ -211,6 +223,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
imageType = .sticker
|
imageType = .sticker
|
||||||
}
|
}
|
||||||
self.content = .image(image, imageType)
|
self.content = .image(image, imageType)
|
||||||
|
} else if let dataPath = try container.decodeIfPresent(String.self, forKey: .animatedImagePath), let data = try? Data(contentsOf: URL(fileURLWithPath: fullEntityMediaPath(dataPath))), let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let thumbnailImage = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) {
|
||||||
|
self.content = .animatedImage(data, thumbnailImage)
|
||||||
} else if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .videoFile) {
|
} else if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .videoFile) {
|
||||||
self.content = .video(file)
|
self.content = .video(file)
|
||||||
} else {
|
} else {
|
||||||
@@ -257,6 +271,20 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case let .animatedImage(data, thumbnailImage):
|
||||||
|
let dataPath = "\(self.uuid).heics"
|
||||||
|
let fullDataPath = fullEntityMediaPath(dataPath)
|
||||||
|
try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true)
|
||||||
|
try? data.write(to: URL(fileURLWithPath: fullDataPath))
|
||||||
|
try container.encodeIfPresent(dataPath, forKey: .animatedImagePath)
|
||||||
|
|
||||||
|
let imagePath = "\(self.uuid).png"
|
||||||
|
let fullImagePath = fullEntityMediaPath(imagePath)
|
||||||
|
if let imageData = thumbnailImage.pngData() {
|
||||||
|
try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true)
|
||||||
|
try? imageData.write(to: URL(fileURLWithPath: fullImagePath))
|
||||||
|
try container.encodeIfPresent(imagePath, forKey: .imagePath)
|
||||||
|
}
|
||||||
case let .video(file):
|
case let .video(file):
|
||||||
try container.encode(file, forKey: .videoFile)
|
try container.encode(file, forKey: .videoFile)
|
||||||
case let .dualVideoReference(isAdditional):
|
case let .dualVideoReference(isAdditional):
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti
|
|||||||
content = .file(file)
|
content = .file(file)
|
||||||
case let .image(image, _):
|
case let .image(image, _):
|
||||||
content = .image(image)
|
content = .image(image)
|
||||||
|
case let .animatedImage(data, _):
|
||||||
|
let _ = data
|
||||||
|
return []
|
||||||
case let .video(file):
|
case let .video(file):
|
||||||
content = .video(file)
|
content = .video(file)
|
||||||
case .dualVideoReference:
|
case .dualVideoReference:
|
||||||
@@ -142,6 +145,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
|||||||
case file(TelegramMediaFile)
|
case file(TelegramMediaFile)
|
||||||
case video(TelegramMediaFile)
|
case video(TelegramMediaFile)
|
||||||
case image(UIImage)
|
case image(UIImage)
|
||||||
|
case animatedImage([UIImage], Double)
|
||||||
|
|
||||||
var file: TelegramMediaFile? {
|
var file: TelegramMediaFile? {
|
||||||
if case let .file(file) = self {
|
if case let .file(file) = self {
|
||||||
@@ -270,6 +274,10 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
|||||||
case let .image(image):
|
case let .image(image):
|
||||||
self.isAnimated = false
|
self.isAnimated = false
|
||||||
self.imagePromise.set(.single(image))
|
self.imagePromise.set(.single(image))
|
||||||
|
case let .animatedImage(images, duration):
|
||||||
|
self.isAnimated = true
|
||||||
|
let _ = images
|
||||||
|
let _ = duration
|
||||||
case .video:
|
case .video:
|
||||||
self.isAnimated = true
|
self.isAnimated = true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user