diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift index b2763b2533..fe314eec12 100644 --- a/submodules/DrawingUI/Sources/ColorPickerScreen.swift +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -13,6 +13,7 @@ import BlurredBackgroundComponent import SegmentedControlNode import MultilineTextComponent import HexColor +import MediaEditor private let palleteColors: [UInt32] = [ 0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000, diff --git a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift index 1713d16546..756afbdc43 100644 --- a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift @@ -2,119 +2,7 @@ import Foundation import UIKit import Display import AccountContext - -public final class DrawingBubbleEntity: DrawingEntity, Codable { - private enum CodingKeys: String, CodingKey { - case uuid - case drawType - case color - case lineWidth - case referenceDrawingSize - case position - case size - case rotation - case tailPosition - case renderImage - } - - enum DrawType: Codable { - case fill - case stroke - } - - public let uuid: UUID - public let isAnimated: Bool - - var drawType: DrawType - public var color: DrawingColor - public var lineWidth: CGFloat - - var referenceDrawingSize: CGSize - public var position: CGPoint - public var size: CGSize - public var rotation: CGFloat - var tailPosition: CGPoint - - public var center: CGPoint { - return self.position - } - - public var scale: CGFloat = 1.0 - - public var renderImage: UIImage? - - public var isMedia: Bool { - return false - } - - init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { - self.uuid = UUID() - self.isAnimated = false - - self.drawType = drawType - self.color = color - self.lineWidth = lineWidth - - self.referenceDrawingSize = .zero - self.position = .zero - self.size = CGSize(width: 1.0, height: 1.0) - self.rotation = 0.0 - self.tailPosition = CGPoint(x: 0.16, y: 0.18) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.uuid = try container.decode(UUID.self, forKey: .uuid) - self.isAnimated = false - self.drawType = try container.decode(DrawType.self, forKey: .drawType) - self.color = try container.decode(DrawingColor.self, forKey: .color) - self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth) - self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) - self.position = try container.decode(CGPoint.self, forKey: .position) - self.size = try container.decode(CGSize.self, forKey: .size) - self.rotation = try container.decode(CGFloat.self, forKey: .rotation) - self.tailPosition = try container.decode(CGPoint.self, forKey: .tailPosition) - if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { - self.renderImage = UIImage(data: renderImageData) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.uuid, forKey: .uuid) - try container.encode(self.drawType, forKey: .drawType) - try container.encode(self.color, forKey: .color) - try container.encode(self.lineWidth, forKey: .lineWidth) - try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) - try container.encode(self.position, forKey: .position) - try container.encode(self.size, forKey: .size) - try container.encode(self.rotation, forKey: .rotation) - try container.encode(self.tailPosition, forKey: .tailPosition) - if let renderImage, let data = renderImage.pngData() { - try container.encode(data, forKey: .renderImage) - } - } - - public func duplicate() -> DrawingEntity { - let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) - newEntity.referenceDrawingSize = self.referenceDrawingSize - newEntity.position = self.position - newEntity.size = self.size - newEntity.rotation = self.rotation - return newEntity - } - - public weak var currentEntityView: DrawingEntityView? - public func makeView(context: AccountContext) -> DrawingEntityView { - let entityView = DrawingBubbleEntityView(context: context, entity: self) - self.currentEntityView = entityView - return entityView - } - - public func prepareForRender() { - self.renderImage = (self.currentEntityView as? DrawingBubbleEntityView)?.getRenderImage() - } -} +import MediaEditor final class DrawingBubbleEntityView: DrawingEntityView { private var bubbleEntity: DrawingBubbleEntity { diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index fe9cab12f7..6e59843601 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -3,66 +3,7 @@ import UIKit import Display import LegacyComponents import AccountContext - - -public protocol DrawingEntity: AnyObject { - var uuid: UUID { get } - var isAnimated: Bool { get } - var center: CGPoint { get } - - var isMedia: Bool { get } - - var lineWidth: CGFloat { get set } - var color: DrawingColor { get set } - - var scale: CGFloat { get set } - - func duplicate() -> DrawingEntity - - var currentEntityView: DrawingEntityView? { get } - func makeView(context: AccountContext) -> DrawingEntityView - - func prepareForRender() -} - -enum CodableDrawingEntity { - case sticker(DrawingStickerEntity) - case text(DrawingTextEntity) - case simpleShape(DrawingSimpleShapeEntity) - case bubble(DrawingBubbleEntity) - case vector(DrawingVectorEntity) - - init?(entity: DrawingEntity) { - if let entity = entity as? DrawingStickerEntity { - self = .sticker(entity) - } else if let entity = entity as? DrawingTextEntity { - self = .text(entity) - } else if let entity = entity as? DrawingSimpleShapeEntity { - self = .simpleShape(entity) - } else if let entity = entity as? DrawingBubbleEntity { - self = .bubble(entity) - } else if let entity = entity as? DrawingVectorEntity { - self = .vector(entity) - } else { - return nil - } - } - - var entity: DrawingEntity { - switch self { - case let .sticker(entity): - return entity - case let .text(entity): - return entity - case let .simpleShape(entity): - return entity - case let .bubble(entity): - return entity - case let .vector(entity): - return entity - } - } -} +import MediaEditor public func decodeDrawingEntities(data: Data) -> [DrawingEntity] { if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) { @@ -71,56 +12,37 @@ public func decodeDrawingEntities(data: Data) -> [DrawingEntity] { return [] } -extension CodableDrawingEntity: Codable { - private enum CodingKeys: String, CodingKey { - case type - case entity +private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> DrawingEntityView? { + if let entity = entity as? DrawingBubbleEntity { + return DrawingBubbleEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingSimpleShapeEntity { + return DrawingSimpleShapeEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingStickerEntity { + return DrawingStickerEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingTextEntity { + return DrawingTextEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingVectorEntity { + return DrawingVectorEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingMediaEntity { + return DrawingMediaEntityView(context: context, entity: entity) + } else { + return nil } +} - private enum EntityType: Int, Codable { - case sticker - case text - case simpleShape - case bubble - case vector +private func prepareForRendering(entityView: DrawingEntityView) { + if let entityView = entityView as? DrawingBubbleEntityView { + entityView.entity.renderImage = entityView.getRenderImage() } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(EntityType.self, forKey: .type) - switch type { - case .sticker: - self = .sticker(try container.decode(DrawingStickerEntity.self, forKey: .entity)) - case .text: - self = .text(try container.decode(DrawingTextEntity.self, forKey: .entity)) - case .simpleShape: - self = .simpleShape(try container.decode(DrawingSimpleShapeEntity.self, forKey: .entity)) - case .bubble: - self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity)) - case .vector: - self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity)) - } + if let entityView = entityView as? DrawingSimpleShapeEntityView { + entityView.entity.renderImage = entityView.getRenderImage() } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case let .sticker(payload): - try container.encode(EntityType.sticker, forKey: .type) - try container.encode(payload, forKey: .entity) - case let .text(payload): - try container.encode(EntityType.text, forKey: .type) - try container.encode(payload, forKey: .entity) - case let .simpleShape(payload): - try container.encode(EntityType.simpleShape, forKey: .type) - try container.encode(payload, forKey: .entity) - case let .bubble(payload): - try container.encode(EntityType.bubble, forKey: .type) - try container.encode(payload, forKey: .entity) - case let .vector(payload): - try container.encode(EntityType.vector, forKey: .type) - try container.encode(payload, forKey: .entity) - } + if let entityView = entityView as? DrawingTextEntityView { + entityView.entity.renderImage = entityView.getRenderImage() + entityView.entity.renderSubEntities = entityView.getRenderSubEntities() + } + if let entityView = entityView as? DrawingVectorEntityView { + entityView.entity.renderImage = entityView.getRenderImage() } } @@ -227,13 +149,17 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } } - public static func encodeEntities(_ entities: [DrawingEntity]) -> Data? { + public static func encodeEntities(_ entities: [DrawingEntity], entitiesView: DrawingEntitiesView? = nil) -> Data? { let entities = entities guard !entities.isEmpty else { return nil } - for entity in entities { - entity.prepareForRender() + if let entitiesView { + for entity in entities { + if let entityView = entitiesView.getView(for: entity.uuid) { + prepareForRendering(entityView: entityView) + } + } } let codableEntities = entities.compactMap({ CodableDrawingEntity(entity: $0) }) if let data = try? JSONEncoder().encode(codableEntities) { @@ -244,7 +170,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } var entitiesData: Data? { - return DrawingEntitiesView.encodeEntities(self.entities) + return DrawingEntitiesView.encodeEntities(self.entities, entitiesView: self) } var hasChanges: Bool { @@ -351,7 +277,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { @discardableResult public func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView { - let view = entity.makeView(context: self.context) + guard let view = makeEntityView(context: self.context, entity: entity) else { + fatalError() + } view.containerView = self view.onSnapToXAxis = { [weak self, weak view] snappedToX in @@ -420,7 +348,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { let newEntity = entity.duplicate() self.prepareNewEntity(newEntity, setup: false, relativeTo: entity) - let view = newEntity.makeView(context: self.context) + guard let view = makeEntityView(context: self.context, entity: entity) else { + fatalError() + } view.containerView = self view.update() self.addSubview(view) diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift index 935b64e88a..209c4997a9 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -7,152 +7,6 @@ import AccountContext import MediaEditor import Photos -public final class DrawingMediaEntity: DrawingEntity, Codable { - public enum Content { - case image(UIImage, PixelDimensions) - case video(String, PixelDimensions) - case asset(PHAsset) - - var dimensions: PixelDimensions { - switch self { - case let .image(_, dimensions), let .video(_, dimensions): - return dimensions - case let .asset(asset): - return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) - } - } - } - - private enum CodingKeys: String, CodingKey { - case uuid - case image - case videoPath - case assetId - case size - case width - case height - case referenceDrawingSize - case position - case scale - case rotation - case mirrored - } - - public let uuid: UUID - public let content: Content - public let size: CGSize - - public var referenceDrawingSize: CGSize - public var position: CGPoint - public var scale: CGFloat - public var rotation: CGFloat - public var mirrored: Bool - - public var color: DrawingColor = DrawingColor.clear - public var lineWidth: CGFloat = 0.0 - - public var center: CGPoint { - return self.position - } - - public var baseSize: CGSize { - return self.size - } - - public var isAnimated: Bool { - switch self.content { - case .image: - return false - case .video: - return true - case let .asset(asset): - return asset.mediaType == .video - } - } - - public var isMedia: Bool { - return true - } - - public init(content: Content, size: CGSize) { - self.uuid = UUID() - self.content = content - self.size = size - - self.referenceDrawingSize = .zero - self.position = CGPoint() - self.scale = 1.0 - self.rotation = 0.0 - self.mirrored = false - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.uuid = try container.decode(UUID.self, forKey: .uuid) - self.size = try container.decode(CGSize.self, forKey: .size) - let width = try container.decode(Int32.self, forKey: .width) - let height = try container.decode(Int32.self, forKey: .height) - if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) { - self.content = .video(videoPath, PixelDimensions(width: width, height: height)) - } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { - self.content = .image(image, PixelDimensions(width: width, height: height)) - } else if let _ = try container.decodeIfPresent(String.self, forKey: .assetId) { - fatalError() - //self.content = .asset() - } else { - fatalError() - } - self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) - self.position = try container.decode(CGPoint.self, forKey: .position) - self.scale = try container.decode(CGFloat.self, forKey: .scale) - self.rotation = try container.decode(CGFloat.self, forKey: .rotation) - self.mirrored = try container.decode(Bool.self, forKey: .mirrored) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.uuid, forKey: .uuid) - switch self.content { - case let .video(videoPath, dimensions): - try container.encode(videoPath, forKey: .videoPath) - try container.encode(dimensions.width, forKey: .width) - try container.encode(dimensions.height, forKey: .height) - case let .image(image, dimensions): - try container.encodeIfPresent(image.jpegData(compressionQuality: 0.9), forKey: .image) - try container.encode(dimensions.width, forKey: .width) - try container.encode(dimensions.height, forKey: .height) - case let .asset(asset): - try container.encode(asset.localIdentifier, forKey: .assetId) - } - try container.encode(self.size, forKey: .size) - try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) - try container.encode(self.position, forKey: .position) - try container.encode(self.scale, forKey: .scale) - try container.encode(self.rotation, forKey: .rotation) - try container.encode(self.mirrored, forKey: .mirrored) - } - - public func duplicate() -> DrawingEntity { - let newEntity = DrawingMediaEntity(content: self.content, size: self.size) - newEntity.referenceDrawingSize = self.referenceDrawingSize - newEntity.position = self.position - newEntity.scale = self.scale - newEntity.rotation = self.rotation - newEntity.mirrored = self.mirrored - return newEntity - } - - public weak var currentEntityView: DrawingEntityView? - public func makeView(context: AccountContext) -> DrawingEntityView { - let entityView = DrawingMediaEntityView(context: context, entity: self) - self.currentEntityView = entityView - return entityView - } - - public func prepareForRender() { - } -} - public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMediaView { private var mediaEntity: DrawingMediaEntity { return self.entity as! DrawingMediaEntity diff --git a/submodules/DrawingUI/Sources/DrawingMetalView.swift b/submodules/DrawingUI/Sources/DrawingMetalView.swift index 34c7e6742c..2ad7288a25 100644 --- a/submodules/DrawingUI/Sources/DrawingMetalView.swift +++ b/submodules/DrawingUI/Sources/DrawingMetalView.swift @@ -5,6 +5,7 @@ import MetalKit import Display import SwiftSignalKit import AppBundle +import MediaEditor final class DrawingMetalView: MTKView { let size: CGSize diff --git a/submodules/DrawingUI/Sources/DrawingNeonTool.swift b/submodules/DrawingUI/Sources/DrawingNeonTool.swift index 001b035bcb..1eafd6f41f 100644 --- a/submodules/DrawingUI/Sources/DrawingNeonTool.swift +++ b/submodules/DrawingUI/Sources/DrawingNeonTool.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import MediaEditor final class NeonTool: DrawingElement { class RenderView: UIView, DrawingRenderView { diff --git a/submodules/DrawingUI/Sources/DrawingPenTool.swift b/submodules/DrawingUI/Sources/DrawingPenTool.swift index a132bb6b64..5fabb02c1c 100644 --- a/submodules/DrawingUI/Sources/DrawingPenTool.swift +++ b/submodules/DrawingUI/Sources/DrawingPenTool.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import MediaEditor private let activeWidthFactor: CGFloat = 0.7 diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 9b4b294fc9..d791ee91e2 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -21,6 +21,13 @@ import ChatEntityKeyboardInputNode import EntityKeyboard import TelegramUIPreferences import FastBlur +import MediaEditor + +public struct DrawingResultData { + public let data: Data? + public let drawingImage: UIImage? + public let entities: [CodableDrawingEntity] +} enum DrawingToolState: Equatable, Codable { private enum CodingKeys: String, CodingKey { @@ -502,6 +509,8 @@ private final class DrawingScreenComponent: CombinedComponent { let toggleWithPreviousTool: ActionSlot let insertSticker: ActionSlot let insertText: ActionSlot + let updateEntityView: ActionSlot<(UUID, Bool)> + let endEditingTextEntityView: ActionSlot<(UUID, Bool)> let apply: ActionSlot let dismiss: ActionSlot @@ -533,6 +542,8 @@ private final class DrawingScreenComponent: CombinedComponent { toggleWithPreviousTool: ActionSlot, insertSticker: ActionSlot, insertText: ActionSlot, + updateEntityView: ActionSlot<(UUID, Bool)>, + endEditingTextEntityView: ActionSlot<(UUID, Bool)>, apply: ActionSlot, dismiss: ActionSlot, presentColorPicker: @escaping (DrawingColor) -> Void, @@ -562,6 +573,8 @@ private final class DrawingScreenComponent: CombinedComponent { self.toggleWithPreviousTool = toggleWithPreviousTool self.insertSticker = insertSticker self.insertText = insertText + self.updateEntityView = updateEntityView + self.endEditingTextEntityView = endEditingTextEntityView self.apply = apply self.dismiss = dismiss self.presentColorPicker = presentColorPicker @@ -637,6 +650,8 @@ private final class DrawingScreenComponent: CombinedComponent { private let toggleWithPreviousTool: ActionSlot private let insertSticker: ActionSlot private let insertText: ActionSlot + private let updateEntityView: ActionSlot<(UUID, Bool)> + private let endEditingTextEntityView: ActionSlot<(UUID, Bool)> private let present: (ViewController) -> Void var currentMode: Mode @@ -661,6 +676,8 @@ private final class DrawingScreenComponent: CombinedComponent { toggleWithPreviousTool: ActionSlot, insertSticker: ActionSlot, insertText: ActionSlot, + updateEntityView: ActionSlot<(UUID, Bool)>, + endEditingTextEntityView: ActionSlot<(UUID, Bool)>, present: @escaping (ViewController) -> Void) { self.context = context @@ -673,6 +690,8 @@ private final class DrawingScreenComponent: CombinedComponent { self.toggleWithPreviousTool = toggleWithPreviousTool self.insertSticker = insertSticker self.insertText = insertText + self.updateEntityView = updateEntityView + self.endEditingTextEntityView = endEditingTextEntityView self.present = present self.currentMode = .drawing @@ -808,7 +827,7 @@ private final class DrawingScreenComponent: CombinedComponent { self.currentColor = color if let selectedEntity = self.selectedEntity { selectedEntity.color = color - selectedEntity.currentEntityView?.update() + self.updateEntityView.invoke((selectedEntity.uuid, false)) } else { self.drawingState = self.drawingState.withUpdatedColor(color) self.updateToolState.invoke(self.drawingState.currentToolState) @@ -850,7 +869,7 @@ private final class DrawingScreenComponent: CombinedComponent { } else { selectedEntity.lineWidth = size } - selectedEntity.currentEntityView?.update() + self.updateEntityView.invoke((selectedEntity.uuid, false)) } else { self.drawingState = self.drawingState.withUpdatedSize(size) self.updateToolState.invoke(self.drawingState.currentToolState) @@ -996,7 +1015,7 @@ private final class DrawingScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, existingStickerPickerInputData: self.existingStickerPickerInputData, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, insertSticker: self.insertSticker, insertText: self.insertText, present: self.present) + return State(context: self.context, existingStickerPickerInputData: self.existingStickerPickerInputData, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, insertSticker: self.insertSticker, insertText: self.insertText, updateEntityView: self.updateEntityView, endEditingTextEntityView: self.endEditingTextEntityView, present: self.present) } static var body: Body { @@ -1070,6 +1089,9 @@ private final class DrawingScreenComponent: CombinedComponent { let dismissFastColorPicker = component.dismissFastColorPicker let presentFontPicker = component.presentFontPicker + let updateEntityView = component.updateEntityView + let endEditingTextEntityView = component.endEditingTextEntityView + component.updateState.connect { [weak state] updatedState in state?.updateDrawingState(updatedState) } @@ -1162,9 +1184,7 @@ private final class DrawingScreenComponent: CombinedComponent { nextStyle = .regular } textEntity.style = nextStyle - if let entityView = textEntity.currentEntityView { - entityView.update() - } + updateEntityView.invoke((textEntity.uuid, false)) state?.updated(transition: .easeInOut(duration: 0.2)) }, toggleAnimation: { [weak state, weak textEntity] in @@ -1183,9 +1203,7 @@ private final class DrawingScreenComponent: CombinedComponent { nextAnimation = .none } textEntity.animation = nextAnimation - if let entityView = textEntity.currentEntityView { - entityView.update() - } + updateEntityView.invoke((textEntity.uuid, false)) state?.updated(transition: .easeInOut(duration: 0.2)) }, toggleAlignment: { [weak state, weak textEntity] in @@ -1202,9 +1220,7 @@ private final class DrawingScreenComponent: CombinedComponent { nextAlignment = .left } textEntity.alignment = nextAlignment - if let entityView = textEntity.currentEntityView { - entityView.update() - } + updateEntityView.invoke((textEntity.uuid, false)) state?.updated(transition: .easeInOut(duration: 0.2)) }, presentFontPicker: { @@ -1549,14 +1565,14 @@ private final class DrawingScreenComponent: CombinedComponent { } else { entity.drawType = .fill } - entity.currentEntityView?.update() + updateEntityView.invoke((entity.uuid, false)) } else if let entity = state.selectedEntity as? DrawingBubbleEntity { if case .fill = entity.drawType { entity.drawType = .stroke } else { entity.drawType = .fill } - entity.currentEntityView?.update() + updateEntityView.invoke((entity.uuid, false)) } else if let entity = state.selectedEntity as? DrawingVectorEntity { if case .oneSidedArrow = entity.type { entity.type = .twoSidedArrow @@ -1565,7 +1581,7 @@ private final class DrawingScreenComponent: CombinedComponent { } else { entity.type = .oneSidedArrow } - entity.currentEntityView?.update() + updateEntityView.invoke((entity.uuid, false)) } state.updated(transition: .easeInOut(duration: 0.2)) } @@ -1594,10 +1610,10 @@ private final class DrawingScreenComponent: CombinedComponent { var updatedTailPosition = entity.tailPosition updatedTailPosition.x = 1.0 - updatedTailPosition.x entity.tailPosition = updatedTailPosition - entity.currentEntityView?.update() + updateEntityView.invoke((entity.uuid, false)) } else if let entity = state.selectedEntity as? DrawingStickerEntity { entity.mirrored = !entity.mirrored - entity.currentEntityView?.update(animated: true) + updateEntityView.invoke((entity.uuid, true)) } state.updated(transition: .easeInOut(duration: 0.2)) } @@ -1616,9 +1632,9 @@ private final class DrawingScreenComponent: CombinedComponent { var sizeSliderVisible = false var isEditingText = false var sizeValue: CGFloat? - if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = textEntity.currentEntityView as? DrawingTextEntityView { + if let textEntity = state.selectedEntity as? DrawingTextEntity, !"".isEmpty {//} let entityView = textEntity.currentEntityView as? DrawingTextEntityView { sizeSliderVisible = true - isEditingText = entityView.isEditing + isEditingText = false//entityView.isEditing sizeValue = textEntity.fontSize } else { if state.selectedEntity == nil || !(state.selectedEntity is DrawingStickerEntity) { @@ -1755,8 +1771,8 @@ private final class DrawingScreenComponent: CombinedComponent { Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white) ), action: { [weak state] in - if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView { - entityView.endEditing(reset: true) + if let entity = state?.selectedEntity as? DrawingTextEntity { + endEditingTextEntityView.invoke((entity.uuid, true)) } } ), @@ -1775,8 +1791,8 @@ private final class DrawingScreenComponent: CombinedComponent { Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white) ), action: { [weak state] in - if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView { - entityView.endEditing() + if let entity = state?.selectedEntity as? DrawingTextEntity { + endEditingTextEntityView.invoke((entity.uuid, false)) } } ), @@ -2041,6 +2057,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private let toggleWithPreviousTool: ActionSlot fileprivate let insertSticker: ActionSlot fileprivate let insertText: ActionSlot + private let updateEntityView: ActionSlot<(UUID, Bool)> + private let endEditingTextEntityView: ActionSlot<(UUID, Bool)> private let apply: ActionSlot private let dismiss: ActionSlot @@ -2274,6 +2292,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } } } + self.updateEntityView.connect { [weak self] uuid, animated in + if let strongSelf = self, let entitiesView = strongSelf._entitiesView { + entitiesView.getView(for: uuid)?.update(animated: animated) + } + } + self.endEditingTextEntityView.connect { [weak self] uuid, reset in + if let strongSelf = self, let entitiesView = strongSelf._entitiesView { + if let textEntityView = entitiesView.getView(for: uuid) as? DrawingTextEntityView { + textEntityView.endEditing(reset: reset) + } + } + } } return self._entitiesView! } @@ -2312,6 +2342,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.toggleWithPreviousTool = ActionSlot() self.insertSticker = ActionSlot() self.insertText = ActionSlot() + self.updateEntityView = ActionSlot<(UUID, Bool)>() + self.endEditingTextEntityView = ActionSlot<(UUID, Bool)>() self.apply = ActionSlot() self.dismiss = ActionSlot() @@ -2737,6 +2769,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U toggleWithPreviousTool: self.toggleWithPreviousTool, insertSticker: self.insertSticker, insertText: self.insertText, + updateEntityView: self.updateEntityView, + endEditingTextEntityView: self.endEditingTextEntityView, apply: self.apply, dismiss: self.dismiss, presentColorPicker: { [weak self] initialColor in @@ -3042,6 +3076,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.drawingView.addInteraction(dropInteraction) } + public func generateDrawingResultData() -> DrawingResultData? { + if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty { + return nil + } + + let drawingImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = self.drawingView.drawingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + }, opaque: false, scale: 1.0) + + let _ = self.entitiesView.entitiesData + let codableEntities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) }) + return DrawingResultData(data: self.drawingView.drawingData, drawingImage: drawingImage, entities: codableEntities) + } + public func generateResultData() -> TGPaintingData? { if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty { return nil @@ -3102,7 +3154,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U stickers.append(coder.makeData()) } else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities { for sticker in subEntities { - if case let .file(file) = sticker.content { + if let sticker = sticker as? DrawingStickerEntity, case let .file(file) = sticker.content { let coder = PostboxEncoder() coder.encodeRootObject(file) stickers.append(coder.makeData()) diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift index f543905058..8bf8a45e5e 100644 --- a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift @@ -2,125 +2,7 @@ import Foundation import UIKit import Display import AccountContext - -public final class DrawingSimpleShapeEntity: DrawingEntity, Codable { - private enum CodingKeys: String, CodingKey { - case uuid - case shapeType - case drawType - case color - case lineWidth - case referenceDrawingSize - case position - case size - case rotation - case renderImage - } - - public enum ShapeType: Codable { - case rectangle - case ellipse - case star - } - - public enum DrawType: Codable { - case fill - case stroke - } - - public let uuid: UUID - public let isAnimated: Bool - - var shapeType: ShapeType - var drawType: DrawType - public var color: DrawingColor - public var lineWidth: CGFloat - - var referenceDrawingSize: CGSize - public var position: CGPoint - public var size: CGSize - public var rotation: CGFloat - - public var center: CGPoint { - return self.position - } - - public var scale: CGFloat = 1.0 - - public var renderImage: UIImage? - - public var isMedia: Bool { - return false - } - - init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { - self.uuid = UUID() - self.isAnimated = false - - self.shapeType = shapeType - self.drawType = drawType - self.color = color - self.lineWidth = lineWidth - - self.referenceDrawingSize = .zero - self.position = .zero - self.size = CGSize(width: 1.0, height: 1.0) - self.rotation = 0.0 - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.uuid = try container.decode(UUID.self, forKey: .uuid) - self.isAnimated = false - self.shapeType = try container.decode(ShapeType.self, forKey: .shapeType) - self.drawType = try container.decode(DrawType.self, forKey: .drawType) - self.color = try container.decode(DrawingColor.self, forKey: .color) - self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth) - self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) - self.position = try container.decode(CGPoint.self, forKey: .position) - self.size = try container.decode(CGSize.self, forKey: .size) - self.rotation = try container.decode(CGFloat.self, forKey: .rotation) - if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { - self.renderImage = UIImage(data: renderImageData) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.uuid, forKey: .uuid) - try container.encode(self.shapeType, forKey: .shapeType) - try container.encode(self.drawType, forKey: .drawType) - try container.encode(self.color, forKey: .color) - try container.encode(self.lineWidth, forKey: .lineWidth) - try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) - try container.encode(self.position, forKey: .position) - try container.encode(self.size, forKey: .size) - try container.encode(self.rotation, forKey: .rotation) - if let renderImage, let data = renderImage.pngData() { - try container.encode(data, forKey: .renderImage) - } - } - - public func duplicate() -> DrawingEntity { - let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) - newEntity.referenceDrawingSize = self.referenceDrawingSize - newEntity.position = self.position - newEntity.size = self.size - newEntity.rotation = self.rotation - return newEntity - } - - public weak var currentEntityView: DrawingEntityView? - public func makeView(context: AccountContext) -> DrawingEntityView { - let entityView = DrawingSimpleShapeEntityView(context: context, entity: self) - self.currentEntityView = entityView - return entityView - } - - public func prepareForRender() { - self.renderImage = (self.currentEntityView as? DrawingSimpleShapeEntityView)?.getRenderImage() - } -} +import MediaEditor final class DrawingSimpleShapeEntityView: DrawingEntityView { private var shapeEntity: DrawingSimpleShapeEntity { diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index 0297d7e258..ae4e248e84 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -7,121 +7,7 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import StickerResources import AccountContext - -public final class DrawingStickerEntity: DrawingEntity, Codable { - public enum Content { - case file(TelegramMediaFile) - case image(UIImage) - } - private enum CodingKeys: String, CodingKey { - case uuid - case file - case image - case referenceDrawingSize - case position - case scale - case rotation - case mirrored - } - - public let uuid: UUID - public let content: Content - - public var referenceDrawingSize: CGSize - public var position: CGPoint - public var scale: CGFloat - public var rotation: CGFloat - public var mirrored: Bool - - public var color: DrawingColor = DrawingColor.clear - public var lineWidth: CGFloat = 0.0 - - public var center: CGPoint { - return self.position - } - - public var baseSize: CGSize { - let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.2) - return CGSize(width: size, height: size) - } - - public var isAnimated: Bool { - switch self.content { - case let .file(file): - return file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" - case .image: - return false - } - } - - public var isMedia: Bool { - return false - } - - public init(content: Content) { - self.uuid = UUID() - self.content = content - - self.referenceDrawingSize = .zero - self.position = CGPoint() - self.scale = 1.0 - self.rotation = 0.0 - self.mirrored = false - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.uuid = try container.decode(UUID.self, forKey: .uuid) - if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) { - self.content = .file(file) - } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { - self.content = .image(image) - } else { - fatalError() - } - self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) - self.position = try container.decode(CGPoint.self, forKey: .position) - self.scale = try container.decode(CGFloat.self, forKey: .scale) - self.rotation = try container.decode(CGFloat.self, forKey: .rotation) - self.mirrored = try container.decode(Bool.self, forKey: .mirrored) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.uuid, forKey: .uuid) - switch self.content { - case let .file(file): - try container.encode(file, forKey: .file) - case let .image(image): - try container.encodeIfPresent(image.pngData(), forKey: .image) - } - try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) - try container.encode(self.position, forKey: .position) - try container.encode(self.scale, forKey: .scale) - try container.encode(self.rotation, forKey: .rotation) - try container.encode(self.mirrored, forKey: .mirrored) - } - - public func duplicate() -> DrawingEntity { - let newEntity = DrawingStickerEntity(content: self.content) - newEntity.referenceDrawingSize = self.referenceDrawingSize - newEntity.position = self.position - newEntity.scale = self.scale - newEntity.rotation = self.rotation - newEntity.mirrored = self.mirrored - return newEntity - } - - public weak var currentEntityView: DrawingEntityView? - public func makeView(context: AccountContext) -> DrawingEntityView { - let entityView = DrawingStickerEntityView(context: context, entity: self) - self.currentEntityView = entityView - return entityView - } - - public func prepareForRender() { - } -} +import MediaEditor final class DrawingStickerEntityView: DrawingEntityView { private var stickerEntity: DrawingStickerEntity { @@ -274,7 +160,8 @@ final class DrawingStickerEntityView: DrawingEntityView { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") - self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + let pathPrefix = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) |> deliverOn(Queue.concurrentDefaultQueue())).start()) diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index 6a1da8e795..1dc511135b 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -5,286 +5,17 @@ import SwiftSignalKit import AccountContext import TextFormat import EmojiTextAttachmentView +import MediaEditor -public final class DrawingTextEntity: DrawingEntity, Codable { - final class CustomEmojiAttribute: Codable { - private enum CodingKeys: String, CodingKey { - case attribute - case rangeOrigin - case rangeLength - } - let attribute: ChatTextInputTextCustomEmojiAttribute - let range: NSRange - - init(attribute: ChatTextInputTextCustomEmojiAttribute, range: NSRange) { - self.attribute = attribute - self.range = range - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.attribute = try container.decode(ChatTextInputTextCustomEmojiAttribute.self, forKey: .attribute) - - let rangeOrigin = try container.decode(Int.self, forKey: .rangeOrigin) - let rangeLength = try container.decode(Int.self, forKey: .rangeLength) - self.range = NSMakeRange(rangeOrigin, rangeLength) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.attribute, forKey: .attribute) - try container.encode(self.range.location, forKey: .rangeOrigin) - try container.encode(self.range.length, forKey: .rangeLength) - } - } - - private enum CodingKeys: String, CodingKey { - case uuid - case text - case textAttributes - case style - case animation - case font - case alignment - case fontSize - case color - case referenceDrawingSize - case position - case width - case scale - case rotation - case renderImage - case renderSubEntities - case renderAnimationFrames - } - - enum Style: Codable { - case regular - case filled - case semi - case stroke - } - - enum Animation: Codable { - case none - case typing - case wiggle - case zoomIn - } - - enum Font: Codable { - case sanFrancisco - case other(String, String) - } - - enum Alignment: Codable { - case left - case center - case right - - var alignment: NSTextAlignment { - switch self { - case .left: - return .left - case .center: - return .center - case .right: - return .right - } - } - } - - public var uuid: UUID - public var isAnimated: Bool { - if self.animation != .none { - return true - } - var isAnimated = false - self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in - if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { - isAnimated = true - } - }) - return isAnimated - } - - var text: NSAttributedString - var style: Style - var animation: Animation - var font: Font - var alignment: Alignment - var fontSize: CGFloat - public var color: DrawingColor - public var lineWidth: CGFloat = 0.0 - - var referenceDrawingSize: CGSize - public var position: CGPoint - var width: CGFloat - public var scale: CGFloat - public var rotation: CGFloat - - public var center: CGPoint { - return self.position - } - - public var renderImage: UIImage? - public var renderSubEntities: [DrawingStickerEntity]? - - public var isMedia: Bool { - return false - } - - public class AnimationFrame: Codable { - private enum CodingKeys: String, CodingKey { - case timestamp - case duration - case image - } - - public let timestamp: Double - public let duration: Double - public let image: UIImage - - public init(timestamp: Double, duration: Double, image: UIImage) { - self.timestamp = timestamp - self.duration = duration - self.image = image - } - - required public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.timestamp = try container.decode(Double.self, forKey: .timestamp) - self.duration = try container.decode(Double.self, forKey: .duration) - if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .image) { - self.image = UIImage(data: renderImageData)! - } else { - fatalError() - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(self.timestamp, forKey: .timestamp) - try container.encode(self.duration, forKey: .duration) - if let data = self.image.pngData() { - try container.encode(data, forKey: .image) - } - } - } - public var renderAnimationFrames: [AnimationFrame]? - - init(text: NSAttributedString, style: Style, animation: Animation, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) { - self.uuid = UUID() - - self.text = text - self.style = style - self.animation = animation - self.font = font - self.alignment = alignment - self.fontSize = fontSize - self.color = color - - self.referenceDrawingSize = .zero - self.position = .zero - self.width = 100.0 - self.scale = 1.0 - self.rotation = 0.0 - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.uuid = try container.decode(UUID.self, forKey: .uuid) - let text = try container.decode(String.self, forKey: .text) - - let attributedString = NSMutableAttributedString(string: text) - let textAttributes = try container.decode([CustomEmojiAttribute].self, forKey: .textAttributes) - for attribute in textAttributes { - attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: attribute.attribute, range: attribute.range) - } - self.text = attributedString - - self.style = try container.decode(Style.self, forKey: .style) - self.animation = try container.decode(Animation.self, forKey: .animation) - self.font = try container.decode(Font.self, forKey: .font) - self.alignment = try container.decode(Alignment.self, forKey: .alignment) - self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize) - self.color = try container.decode(DrawingColor.self, forKey: .color) - self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) - self.position = try container.decode(CGPoint.self, forKey: .position) - self.width = try container.decode(CGFloat.self, forKey: .width) - self.scale = try container.decode(CGFloat.self, forKey: .scale) - self.rotation = try container.decode(CGFloat.self, forKey: .rotation) - if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { - self.renderImage = UIImage(data: renderImageData) - } - if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) { - self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity } - } - self.renderAnimationFrames = try container.decodeIfPresent([AnimationFrame].self, forKey: .renderAnimationFrames) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.uuid, forKey: .uuid) - try container.encode(self.text.string, forKey: .text) - - var textAttributes: [CustomEmojiAttribute] = [] - self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in - if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { - textAttributes.append(CustomEmojiAttribute(attribute: value, range: range)) - } - }) - try container.encode(textAttributes, forKey: .textAttributes) - - try container.encode(self.style, forKey: .style) - try container.encode(self.animation, forKey: .animation) - try container.encode(self.font, forKey: .font) - try container.encode(self.alignment, forKey: .alignment) - try container.encode(self.fontSize, forKey: .fontSize) - try container.encode(self.color, forKey: .color) - try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) - try container.encode(self.position, forKey: .position) - try container.encode(self.width, forKey: .width) - try container.encode(self.scale, forKey: .scale) - try container.encode(self.rotation, forKey: .rotation) - if let renderImage, let data = renderImage.pngData() { - try container.encode(data, forKey: .renderImage) - } - if let renderSubEntities = self.renderSubEntities { - let codableEntities: [CodableDrawingEntity] = renderSubEntities.map { .sticker($0) } - try container.encode(codableEntities, forKey: .renderSubEntities) - } - if let renderAnimationFrames = self.renderAnimationFrames { - try container.encode(renderAnimationFrames, forKey: .renderAnimationFrames) - } - } - - public func duplicate() -> DrawingEntity { - let newEntity = DrawingTextEntity(text: self.text, style: self.style, animation: self.animation, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color) - newEntity.referenceDrawingSize = self.referenceDrawingSize - newEntity.position = self.position - newEntity.width = self.width - newEntity.scale = self.scale - newEntity.rotation = self.rotation - return newEntity - } - - public weak var currentEntityView: DrawingEntityView? - public func makeView(context: AccountContext) -> DrawingEntityView { - let entityView = DrawingTextEntityView(context: context, entity: self) - self.currentEntityView = entityView - return entityView - } - - public func prepareForRender() { - self.renderImage = (self.currentEntityView as? DrawingTextEntityView)?.getRenderImage() - self.renderSubEntities = (self.currentEntityView as? DrawingTextEntityView)?.getRenderSubEntities() - - if case .none = self.animation { - self.renderAnimationFrames = nil - } else { - self.renderAnimationFrames = (self.currentEntityView as? DrawingTextEntityView)?.getRenderAnimationFrames() +extension DrawingTextEntity.Alignment { + var alignment: NSTextAlignment { + switch self { + case .left: + return .left + case .center: + return .center + case .right: + return .right } } } @@ -892,7 +623,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { return image } - func getRenderSubEntities() -> [DrawingStickerEntity] { + func getRenderSubEntities() -> [DrawingEntity] { let textSize = self.textView.bounds.size let textPosition = self.textEntity.position let scale = self.textEntity.scale @@ -900,7 +631,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0) - var entities: [DrawingStickerEntity] = [] + var entities: [DrawingEntity] = [] for (emojiRect, emojiAttribute) in self.emojiRects { guard let file = emojiAttribute.file else { continue diff --git a/submodules/DrawingUI/Sources/DrawingTools.swift b/submodules/DrawingUI/Sources/DrawingTools.swift index 078ad69a15..eba0a7e23d 100644 --- a/submodules/DrawingUI/Sources/DrawingTools.swift +++ b/submodules/DrawingUI/Sources/DrawingTools.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import MediaEditor final class MarkerTool: DrawingElement { let uuid: UUID diff --git a/submodules/DrawingUI/Sources/DrawingUtils.swift b/submodules/DrawingUI/Sources/DrawingUtils.swift index 3570f6cbea..acc40e71ef 100644 --- a/submodules/DrawingUI/Sources/DrawingUtils.swift +++ b/submodules/DrawingUI/Sources/DrawingUtils.swift @@ -2,164 +2,7 @@ import Foundation import UIKit import QuartzCore import simd - -public struct DrawingColor: Equatable, Codable { - private enum CodingKeys: String, CodingKey { - case red - case green - case blue - case alpha - case position - } - - public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) - - public var red: CGFloat - public var green: CGFloat - public var blue: CGFloat - public var alpha: CGFloat - - public var position: CGPoint? - - var isClear: Bool { - return self.red.isZero && self.green.isZero && self.blue.isZero && self.alpha.isZero - } - - public init( - red: CGFloat, - green: CGFloat, - blue: CGFloat, - alpha: CGFloat = 1.0, - position: CGPoint? = nil - ) { - self.red = red - self.green = green - self.blue = blue - self.alpha = alpha - self.position = position - } - - public init(color: UIColor) { - var red: CGFloat = 0.0 - var green: CGFloat = 0.0 - var blue: CGFloat = 0.0 - var alpha: CGFloat = 1.0 - if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { - self.init(red: red, green: green, blue: blue, alpha: alpha) - } else if color.getWhite(&red, alpha: &alpha) { - self.init(red: red, green: red, blue: red, alpha: alpha) - } else { - self.init(red: 0.0, green: 0.0, blue: 0.0) - } - } - - public init(rgb: UInt32) { - self.init(color: UIColor(rgb: rgb)) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.red = try container.decode(CGFloat.self, forKey: .red) - self.green = try container.decode(CGFloat.self, forKey: .green) - self.blue = try container.decode(CGFloat.self, forKey: .blue) - self.alpha = try container.decode(CGFloat.self, forKey: .alpha) - self.position = try container.decodeIfPresent(CGPoint.self, forKey: .position) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.red, forKey: .red) - try container.encode(self.green, forKey: .green) - try container.encode(self.blue, forKey: .blue) - try container.encode(self.alpha, forKey: .alpha) - try container.encodeIfPresent(self.position, forKey: .position) - } - - func withUpdatedRed(_ red: CGFloat) -> DrawingColor { - return DrawingColor( - red: red, - green: self.green, - blue: self.blue, - alpha: self.alpha - ) - } - - func withUpdatedGreen(_ green: CGFloat) -> DrawingColor { - return DrawingColor( - red: self.red, - green: green, - blue: self.blue, - alpha: self.alpha - ) - } - - func withUpdatedBlue(_ blue: CGFloat) -> DrawingColor { - return DrawingColor( - red: self.red, - green: self.green, - blue: blue, - alpha: self.alpha - ) - } - - func withUpdatedAlpha(_ alpha: CGFloat) -> DrawingColor { - return DrawingColor( - red: self.red, - green: self.green, - blue: self.blue, - alpha: alpha, - position: self.position - ) - } - - func withUpdatedPosition(_ position: CGPoint) -> DrawingColor { - return DrawingColor( - red: self.red, - green: self.green, - blue: self.blue, - alpha: self.alpha, - position: position - ) - } - - func toUIColor() -> UIColor { - return UIColor( - red: self.red, - green: self.green, - blue: self.blue, - alpha: self.alpha - ) - } - - func toCGColor() -> CGColor { - return self.toUIColor().cgColor - } - - func toFloat4() -> vector_float4 { - return [ - simd_float1(self.red), - simd_float1(self.green), - simd_float1(self.blue), - simd_float1(self.alpha) - ] - } - - public static func ==(lhs: DrawingColor, rhs: DrawingColor) -> Bool { - if lhs.red != rhs.red { - return false - } - if lhs.green != rhs.green { - return false - } - if lhs.blue != rhs.blue { - return false - } - if lhs.alpha != rhs.alpha { - return false - } - return true - } -} +import MediaEditor extension UIBezierPath { convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) { diff --git a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift index 6fc2c6da0d..a17a7daa42 100644 --- a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift @@ -2,133 +2,7 @@ import Foundation import UIKit import Display import AccountContext - -public final class DrawingVectorEntity: DrawingEntity, Codable { - private enum CodingKeys: String, CodingKey { - case uuid - case type - case color - case lineWidth - case drawingSize - case referenceDrawingSize - case start - case mid - case end - case renderImage - } - - public enum VectorType: Codable { - case line - case oneSidedArrow - case twoSidedArrow - } - - public let uuid: UUID - public let isAnimated: Bool - - var type: VectorType - public var color: DrawingColor - public var lineWidth: CGFloat - - public var drawingSize: CGSize - var referenceDrawingSize: CGSize - var start: CGPoint - var mid: (CGFloat, CGFloat) - var end: CGPoint - - var _cachedMidPoint: (start: CGPoint, end: CGPoint, midLength: CGFloat, midHeight: CGFloat, midPoint: CGPoint)? - var midPoint: CGPoint { - if let (start, end, midLength, midHeight, midPoint) = self._cachedMidPoint, start == self.start, end == self.end, midLength == self.mid.0, midHeight == self.mid.1 { - return midPoint - } else { - let midPoint = midPointPositionFor(start: self.start, end: self.end, length: self.mid.0, height: self.mid.1) - self._cachedMidPoint = (self.start, self.end, self.mid.0, self.mid.1, midPoint) - return midPoint - } - } - - public var center: CGPoint { - return self.start - } - - public var scale: CGFloat = 1.0 - - public var renderImage: UIImage? - - public var isMedia: Bool { - return false - } - - init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) { - self.uuid = UUID() - self.isAnimated = false - - self.type = type - self.color = color - self.lineWidth = lineWidth - - self.drawingSize = .zero - self.referenceDrawingSize = .zero - self.start = CGPoint() - self.mid = (0.5, 0.0) - self.end = CGPoint() - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.uuid = try container.decode(UUID.self, forKey: .uuid) - self.isAnimated = false - self.type = try container.decode(VectorType.self, forKey: .type) - self.color = try container.decode(DrawingColor.self, forKey: .color) - self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth) - self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize) - self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) - self.start = try container.decode(CGPoint.self, forKey: .start) - let mid = try container.decode(CGPoint.self, forKey: .mid) - self.mid = (mid.x, mid.y) - self.end = try container.decode(CGPoint.self, forKey: .end) - if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { - self.renderImage = UIImage(data: renderImageData) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.uuid, forKey: .uuid) - try container.encode(self.type, forKey: .type) - try container.encode(self.color, forKey: .color) - try container.encode(self.lineWidth, forKey: .lineWidth) - try container.encode(self.drawingSize, forKey: .drawingSize) - try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) - try container.encode(self.start, forKey: .start) - try container.encode(CGPoint(x: self.mid.0, y: self.mid.1), forKey: .mid) - try container.encode(self.end, forKey: .end) - if let renderImage, let data = renderImage.pngData() { - try container.encode(data, forKey: .renderImage) - } - } - - public func duplicate() -> DrawingEntity { - let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth) - newEntity.drawingSize = self.drawingSize - newEntity.referenceDrawingSize = self.referenceDrawingSize - newEntity.start = self.start - newEntity.mid = self.mid - newEntity.end = self.end - return newEntity - } - - public weak var currentEntityView: DrawingEntityView? - public func makeView(context: AccountContext) -> DrawingEntityView { - let entityView = DrawingVectorEntityView(context: context, entity: self) - self.currentEntityView = entityView - return entityView - } - - public func prepareForRender() { - self.renderImage = (self.currentEntityView as? DrawingVectorEntityView)?.getRenderImage() - } -} +import MediaEditor final class DrawingVectorEntityView: DrawingEntityView { private var vectorEntity: DrawingVectorEntity { @@ -166,7 +40,7 @@ final class DrawingVectorEntityView: DrawingEntityView { self.shapeLayer.path = CGPath.curve( start: self.vectorEntity.start, end: self.vectorEntity.end, - mid: self.vectorEntity.midPoint, + mid: self.midPoint, lineWidth: lineWidth, arrowSize: self.vectorEntity.type == .line ? nil : CGSize(width: lineWidth * 1.5, height: lineWidth * 3.0), twoSided: self.vectorEntity.type == .twoSidedArrow @@ -198,7 +72,7 @@ final class DrawingVectorEntityView: DrawingEntityView { let expandedPath = CGPath.curve( start: self.vectorEntity.start, end: self.vectorEntity.end, - mid: self.vectorEntity.midPoint, + mid: self.midPoint, lineWidth: self.maxLineWidth * 0.8, arrowSize: nil, twoSided: false @@ -227,6 +101,18 @@ final class DrawingVectorEntityView: DrawingEntityView { UIGraphicsEndImageContext() return image } + + var _cachedMidPoint: (start: CGPoint, end: CGPoint, midLength: CGFloat, midHeight: CGFloat, midPoint: CGPoint)? + var midPoint: CGPoint { + let entity = self.vectorEntity + if let (start, end, midLength, midHeight, midPoint) = self._cachedMidPoint, start == entity.start, end == entity.end, midLength == entity.mid.0, midHeight == entity.mid.1 { + return midPoint + } else { + let midPoint = midPointPositionFor(start: entity.start, end: entity.end, length: entity.mid.0, height: entity.mid.1) + self._cachedMidPoint = (entity.start, entity.end, entity.mid.0, entity.mid.1, midPoint) + return midPoint + } + } } private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, height: CGFloat) -> CGPoint { @@ -295,7 +181,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe private var currentHandle: CALayer? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - guard let entityView = self.entityView, let entity = entityView.entity as? DrawingVectorEntity else { + guard let entityView = self.entityView as? DrawingVectorEntityView, let entity = entityView.entity as? DrawingVectorEntity else { return } let location = gestureRecognizer.location(in: self) @@ -325,7 +211,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe updatedEnd.x += delta.x updatedEnd.y += delta.y } else if self.currentHandle === self.midHandle { - var updatedMidPoint = entity.midPoint + var updatedMidPoint = entityView.midPoint updatedMidPoint.x += delta.x updatedMidPoint.y += delta.y @@ -356,7 +242,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe entity.start = updatedStart entity.mid = updatedMid entity.end = updatedEnd - entityView.update() + entityView.update(animated: false) gestureRecognizer.setTranslation(.zero, in: entityView) case .ended: @@ -391,7 +277,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe self.startHandle.lineWidth = lineWidth self.midHandle.path = handlePath - self.midHandle.position = entity.midPoint + self.midHandle.position = entityView.midPoint self.midHandle.bounds = bounds self.midHandle.lineWidth = lineWidth diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift index 5cb9fdcec1..2fc79b5798 100644 --- a/submodules/DrawingUI/Sources/DrawingView.swift +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -6,6 +6,7 @@ import ComponentFlow import LegacyComponents import AppBundle import ImageBlur +import MediaEditor protocol DrawingRenderLayer: CALayer { diff --git a/submodules/DrawingUI/Sources/EyedropperView.swift b/submodules/DrawingUI/Sources/EyedropperView.swift index 90181b3d32..e38470da8c 100644 --- a/submodules/DrawingUI/Sources/EyedropperView.swift +++ b/submodules/DrawingUI/Sources/EyedropperView.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import SwiftSignalKit +import MediaEditor private let size = CGSize(width: 148.0, height: 148.0) private let outerWidth: CGFloat = 12.0 diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift index 420eb67cdf..e20b53ac63 100644 --- a/submodules/DrawingUI/Sources/TextSettingsComponent.swift +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -5,6 +5,7 @@ import ComponentFlow import LegacyComponents import TelegramCore import LottieAnimationComponent +import MediaEditor enum DrawingTextStyle: Equatable { case regular diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index 65aae17021..9e743545d8 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/AttachmentUI:AttachmentUI", "//submodules/DrawingUI:DrawingUI", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", + "//submodules/TelegramUI/Components/MediaEditor", ], visibility = [ "//visibility:public", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 1ab174a644..e1b3520201 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -10,6 +10,7 @@ import YuvConversion import StickerResources import DrawingUI import SolidRoundedButtonNode +import MediaEditor protocol LegacyPaintEntity { var position: CGPoint { get } @@ -432,7 +433,9 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender renderEntities.append(LegacyPaintTextEntity(entity: text)) if let renderSubEntities = text.renderSubEntities, let account { for entity in renderSubEntities { - renderEntities.append(LegacyPaintStickerEntity(account: account, entity: entity)) + if let entity = entity as? DrawingStickerEntity { + renderEntities.append(LegacyPaintStickerEntity(account: account, entity: entity)) + } } } } else if let simpleShape = entity as? DrawingSimpleShapeEntity { diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index 2411edffdb..50ab1a7250 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -497,9 +497,25 @@ public func peersNearbyController(context: AccountContext) -> ViewController { }) let dataSignal: Signal = coordinatePromise.get() + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs?.latitude == rhs?.latitude && lhs?.longitude == rhs?.longitude + }) |> mapToSignal { coordinate -> Signal in guard let coordinate = coordinate else { - return .single(nil) + let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: nil) + return peersNearbyContext.get() + |> map { peersNearby -> PeersNearbyData in + var isVisible = false + if let peersNearby { + for peer in peersNearby { + if case .selfPeer = peer { + isVisible = true + break + } + } + } + return PeersNearbyData(latitude: 0.0, longitude: 0.0, address: nil, visible: isVisible, accountPeerId: context.account.peerId, users: [], groups: [], channels: []) + } } return Signal { subscriber in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index 3562eb148e..29061e3983 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift @@ -14,7 +14,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal DrawingEntity { + let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.size = self.size + newEntity.rotation = self.rotation + return newEntity + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingColor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingColor.swift new file mode 100644 index 0000000000..fee05eac02 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingColor.swift @@ -0,0 +1,161 @@ +import Foundation +import UIKit +import simd + +public struct DrawingColor: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case red + case green + case blue + case alpha + case position + } + + public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + + public var red: CGFloat + public var green: CGFloat + public var blue: CGFloat + public var alpha: CGFloat + + public var position: CGPoint? + + public var isClear: Bool { + return self.red.isZero && self.green.isZero && self.blue.isZero && self.alpha.isZero + } + + public init( + red: CGFloat, + green: CGFloat, + blue: CGFloat, + alpha: CGFloat = 1.0, + position: CGPoint? = nil + ) { + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + self.position = position + } + + public init(color: UIColor) { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 1.0 + if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { + self.init(red: red, green: green, blue: blue, alpha: alpha) + } else if color.getWhite(&red, alpha: &alpha) { + self.init(red: red, green: red, blue: red, alpha: alpha) + } else { + self.init(red: 0.0, green: 0.0, blue: 0.0) + } + } + + public init(rgb: UInt32) { + self.init(color: UIColor(rgb: rgb)) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.red = try container.decode(CGFloat.self, forKey: .red) + self.green = try container.decode(CGFloat.self, forKey: .green) + self.blue = try container.decode(CGFloat.self, forKey: .blue) + self.alpha = try container.decode(CGFloat.self, forKey: .alpha) + self.position = try container.decodeIfPresent(CGPoint.self, forKey: .position) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.red, forKey: .red) + try container.encode(self.green, forKey: .green) + try container.encode(self.blue, forKey: .blue) + try container.encode(self.alpha, forKey: .alpha) + try container.encodeIfPresent(self.position, forKey: .position) + } + + public func withUpdatedRed(_ red: CGFloat) -> DrawingColor { + return DrawingColor( + red: red, + green: self.green, + blue: self.blue, + alpha: self.alpha + ) + } + + public func withUpdatedGreen(_ green: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: green, + blue: self.blue, + alpha: self.alpha + ) + } + + public func withUpdatedBlue(_ blue: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: blue, + alpha: self.alpha + ) + } + + public func withUpdatedAlpha(_ alpha: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: alpha, + position: self.position + ) + } + + public func withUpdatedPosition(_ position: CGPoint) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: self.alpha, + position: position + ) + } + + public func toUIColor() -> UIColor { + return UIColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: self.alpha + ) + } + + public func toCGColor() -> CGColor { + return self.toUIColor().cgColor + } + + public func toFloat4() -> vector_float4 { + return [ + simd_float1(self.red), + simd_float1(self.green), + simd_float1(self.blue), + simd_float1(self.alpha) + ] + } + + public static func ==(lhs: DrawingColor, rhs: DrawingColor) -> Bool { + if lhs.red != rhs.red { + return false + } + if lhs.green != rhs.green { + return false + } + if lhs.blue != rhs.blue { + return false + } + if lhs.alpha != rhs.alpha { + return false + } + return true + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift new file mode 100644 index 0000000000..5d79c01486 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift @@ -0,0 +1,20 @@ +import Foundation +import UIKit + +public protocol DrawingEntity: AnyObject { + var uuid: UUID { get } + var isAnimated: Bool { get } + var center: CGPoint { get } + + var isMedia: Bool { get } + + var lineWidth: CGFloat { get set } + var color: DrawingColor { get set } + + var scale: CGFloat { get set } + + func duplicate() -> DrawingEntity + + var renderImage: UIImage? { get set } + var renderSubEntities: [DrawingEntity]? { get set } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift new file mode 100644 index 0000000000..1164dd1093 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift @@ -0,0 +1,146 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import AccountContext +import Photos + +public final class DrawingMediaEntity: DrawingEntity, Codable { + public enum Content { + case image(UIImage, PixelDimensions) + case video(String, PixelDimensions) + case asset(PHAsset) + + var dimensions: PixelDimensions { + switch self { + case let .image(_, dimensions), let .video(_, dimensions): + return dimensions + case let .asset(asset): + return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) + } + } + } + + private enum CodingKeys: String, CodingKey { + case uuid + case image + case videoPath + case assetId + case size + case width + case height + case referenceDrawingSize + case position + case scale + case rotation + case mirrored + } + + public let uuid: UUID + public let content: Content + public let size: CGSize + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var scale: CGFloat + public var rotation: CGFloat + public var mirrored: Bool + + public var color: DrawingColor = DrawingColor.clear + public var lineWidth: CGFloat = 0.0 + + public var center: CGPoint { + return self.position + } + + public var baseSize: CGSize { + return self.size + } + + public var isAnimated: Bool { + switch self.content { + case .image: + return false + case .video: + return true + case let .asset(asset): + return asset.mediaType == .video + } + } + + public var isMedia: Bool { + return true + } + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingEntity]? + + public init(content: Content, size: CGSize) { + self.uuid = UUID() + self.content = content + self.size = size + + self.referenceDrawingSize = .zero + self.position = CGPoint() + self.scale = 1.0 + self.rotation = 0.0 + self.mirrored = false + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.size = try container.decode(CGSize.self, forKey: .size) + let width = try container.decode(Int32.self, forKey: .width) + let height = try container.decode(Int32.self, forKey: .height) + if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) { + self.content = .video(videoPath, PixelDimensions(width: width, height: height)) + } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { + self.content = .image(image, PixelDimensions(width: width, height: height)) + } else if let _ = try container.decodeIfPresent(String.self, forKey: .assetId) { + fatalError() + //self.content = .asset() + } else { + fatalError() + } + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + self.mirrored = try container.decode(Bool.self, forKey: .mirrored) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + switch self.content { + case let .video(videoPath, dimensions): + try container.encode(videoPath, forKey: .videoPath) + try container.encode(dimensions.width, forKey: .width) + try container.encode(dimensions.height, forKey: .height) + case let .image(image, dimensions): + try container.encodeIfPresent(image.jpegData(compressionQuality: 0.9), forKey: .image) + try container.encode(dimensions.width, forKey: .width) + try container.encode(dimensions.height, forKey: .height) + case let .asset(asset): + try container.encode(asset.localIdentifier, forKey: .assetId) + } + try container.encode(self.size, forKey: .size) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + try container.encode(self.mirrored, forKey: .mirrored) + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingMediaEntity(content: self.content, size: self.size) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.scale = self.scale + newEntity.rotation = self.rotation + newEntity.mirrored = self.mirrored + return newEntity + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift new file mode 100644 index 0000000000..151d56b19e --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift @@ -0,0 +1,113 @@ +import Foundation +import UIKit +import Display +import AccountContext + +public final class DrawingSimpleShapeEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case shapeType + case drawType + case color + case lineWidth + case referenceDrawingSize + case position + case size + case rotation + case renderImage + } + + public enum ShapeType: Codable { + case rectangle + case ellipse + case star + } + + public enum DrawType: Codable { + case fill + case stroke + } + + public let uuid: UUID + public let isAnimated: Bool + + public var shapeType: ShapeType + public var drawType: DrawType + public var color: DrawingColor + public var lineWidth: CGFloat + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var size: CGSize + public var rotation: CGFloat + + public var center: CGPoint { + return self.position + } + + public var scale: CGFloat = 1.0 + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingEntity]? + + public var isMedia: Bool { + return false + } + + public init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.isAnimated = false + + self.shapeType = shapeType + self.drawType = drawType + self.color = color + self.lineWidth = lineWidth + + self.referenceDrawingSize = .zero + self.position = .zero + self.size = CGSize(width: 1.0, height: 1.0) + self.rotation = 0.0 + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.isAnimated = false + self.shapeType = try container.decode(ShapeType.self, forKey: .shapeType) + self.drawType = try container.decode(DrawType.self, forKey: .drawType) + self.color = try container.decode(DrawingColor.self, forKey: .color) + self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth) + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.size = try container.decode(CGSize.self, forKey: .size) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.shapeType, forKey: .shapeType) + try container.encode(self.drawType, forKey: .drawType) + try container.encode(self.color, forKey: .color) + try container.encode(self.lineWidth, forKey: .lineWidth) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.size, forKey: .size) + try container.encode(self.rotation, forKey: .rotation) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.size = self.size + newEntity.rotation = self.rotation + return newEntity + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift new file mode 100644 index 0000000000..3d74eb7dc5 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -0,0 +1,113 @@ +import Foundation +import UIKit +import Display +import AccountContext +import TelegramCore + +public final class DrawingStickerEntity: DrawingEntity, Codable { + public enum Content { + case file(TelegramMediaFile) + case image(UIImage) + } + private enum CodingKeys: String, CodingKey { + case uuid + case file + case image + case referenceDrawingSize + case position + case scale + case rotation + case mirrored + } + + public let uuid: UUID + public let content: Content + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var scale: CGFloat + public var rotation: CGFloat + public var mirrored: Bool + + public var color: DrawingColor = DrawingColor.clear + public var lineWidth: CGFloat = 0.0 + + public var center: CGPoint { + return self.position + } + + public var baseSize: CGSize { + let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.2) + return CGSize(width: size, height: size) + } + + public var isAnimated: Bool { + switch self.content { + case let .file(file): + return file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" + case .image: + return false + } + } + + public var isMedia: Bool { + return false + } + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingEntity]? + + public init(content: Content) { + self.uuid = UUID() + self.content = content + + self.referenceDrawingSize = .zero + self.position = CGPoint() + self.scale = 1.0 + self.rotation = 0.0 + self.mirrored = false + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) { + self.content = .file(file) + } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { + self.content = .image(image) + } else { + fatalError() + } + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + self.mirrored = try container.decode(Bool.self, forKey: .mirrored) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + switch self.content { + case let .file(file): + try container.encode(file, forKey: .file) + case let .image(image): + try container.encodeIfPresent(image.pngData(), forKey: .image) + } + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + try container.encode(self.mirrored, forKey: .mirrored) + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingStickerEntity(content: self.content) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.scale = self.scale + newEntity.rotation = self.rotation + newEntity.mirrored = self.mirrored + return newEntity + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift new file mode 100644 index 0000000000..db4c0a783e --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift @@ -0,0 +1,277 @@ +import Foundation +import UIKit +import Display +import AccountContext +import TextFormat + +public final class DrawingTextEntity: DrawingEntity, Codable { + final class CustomEmojiAttribute: Codable { + private enum CodingKeys: String, CodingKey { + case attribute + case rangeOrigin + case rangeLength + } + let attribute: ChatTextInputTextCustomEmojiAttribute + let range: NSRange + + init(attribute: ChatTextInputTextCustomEmojiAttribute, range: NSRange) { + self.attribute = attribute + self.range = range + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.attribute = try container.decode(ChatTextInputTextCustomEmojiAttribute.self, forKey: .attribute) + + let rangeOrigin = try container.decode(Int.self, forKey: .rangeOrigin) + let rangeLength = try container.decode(Int.self, forKey: .rangeLength) + self.range = NSMakeRange(rangeOrigin, rangeLength) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.attribute, forKey: .attribute) + try container.encode(self.range.location, forKey: .rangeOrigin) + try container.encode(self.range.length, forKey: .rangeLength) + } + } + + private enum CodingKeys: String, CodingKey { + case uuid + case text + case textAttributes + case style + case animation + case font + case alignment + case fontSize + case color + case referenceDrawingSize + case position + case width + case scale + case rotation + case renderImage + case renderSubEntities + case renderAnimationFrames + } + + public enum Style: Codable { + case regular + case filled + case semi + case stroke + } + + public enum Animation: Codable { + case none + case typing + case wiggle + case zoomIn + } + + public enum Font: Codable { + case sanFrancisco + case other(String, String) + } + + public enum Alignment: Codable { + case left + case center + case right + } + + public var uuid: UUID + public var isAnimated: Bool { + if self.animation != .none { + return true + } + var isAnimated = false + self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { + isAnimated = true + } + }) + return isAnimated + } + + public var text: NSAttributedString + public var style: Style + public var animation: Animation + public var font: Font + public var alignment: Alignment + public var fontSize: CGFloat + public var color: DrawingColor + public var lineWidth: CGFloat = 0.0 + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var width: CGFloat + public var scale: CGFloat + public var rotation: CGFloat + + public var center: CGPoint { + return self.position + } + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingEntity]? + + public var isMedia: Bool { + return false + } + + public class AnimationFrame: Codable { + private enum CodingKeys: String, CodingKey { + case timestamp + case duration + case image + } + + public let timestamp: Double + public let duration: Double + public let image: UIImage + + public init(timestamp: Double, duration: Double, image: UIImage) { + self.timestamp = timestamp + self.duration = duration + self.image = image + } + + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.timestamp = try container.decode(Double.self, forKey: .timestamp) + self.duration = try container.decode(Double.self, forKey: .duration) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .image) { + self.image = UIImage(data: renderImageData)! + } else { + fatalError() + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.timestamp, forKey: .timestamp) + try container.encode(self.duration, forKey: .duration) + if let data = self.image.pngData() { + try container.encode(data, forKey: .image) + } + } + } + public var renderAnimationFrames: [AnimationFrame]? + + public init(text: NSAttributedString, style: Style, animation: Animation, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) { + self.uuid = UUID() + + self.text = text + self.style = style + self.animation = animation + self.font = font + self.alignment = alignment + self.fontSize = fontSize + self.color = color + + self.referenceDrawingSize = .zero + self.position = .zero + self.width = 100.0 + self.scale = 1.0 + self.rotation = 0.0 + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + let text = try container.decode(String.self, forKey: .text) + + let attributedString = NSMutableAttributedString(string: text) + let textAttributes = try container.decode([CustomEmojiAttribute].self, forKey: .textAttributes) + for attribute in textAttributes { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: attribute.attribute, range: attribute.range) + } + self.text = attributedString + + self.style = try container.decode(Style.self, forKey: .style) + self.animation = try container.decode(Animation.self, forKey: .animation) + self.font = try container.decode(Font.self, forKey: .font) + self.alignment = try container.decode(Alignment.self, forKey: .alignment) + self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize) + self.color = try container.decode(DrawingColor.self, forKey: .color) + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.width = try container.decode(CGFloat.self, forKey: .width) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) { + self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity } + } + self.renderAnimationFrames = try container.decodeIfPresent([AnimationFrame].self, forKey: .renderAnimationFrames) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.text.string, forKey: .text) + + var textAttributes: [CustomEmojiAttribute] = [] + self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in + if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { + textAttributes.append(CustomEmojiAttribute(attribute: value, range: range)) + } + }) + try container.encode(textAttributes, forKey: .textAttributes) + + try container.encode(self.style, forKey: .style) + try container.encode(self.animation, forKey: .animation) + try container.encode(self.font, forKey: .font) + try container.encode(self.alignment, forKey: .alignment) + try container.encode(self.fontSize, forKey: .fontSize) + try container.encode(self.color, forKey: .color) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.width, forKey: .width) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + if let renderSubEntities = self.renderSubEntities { + let codableEntities: [CodableDrawingEntity] = renderSubEntities.compactMap { CodableDrawingEntity(entity: $0) } + try container.encode(codableEntities, forKey: .renderSubEntities) + } + if let renderAnimationFrames = self.renderAnimationFrames { + try container.encode(renderAnimationFrames, forKey: .renderAnimationFrames) + } + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingTextEntity(text: self.text, style: self.style, animation: self.animation, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.width = self.width + newEntity.scale = self.scale + newEntity.rotation = self.rotation + return newEntity + } + +// public weak var currentEntityView: DrawingEntityView? +// public func makeView(context: AccountContext) -> DrawingEntityView { +// let entityView = DrawingTextEntityView(context: context, entity: self) +// self.currentEntityView = entityView +// return entityView +// } +// +// public func prepareForRender() { +// self.renderImage = (self.currentEntityView as? DrawingTextEntityView)?.getRenderImage() +// self.renderSubEntities = (self.currentEntityView as? DrawingTextEntityView)?.getRenderSubEntities() +// +// if case .none = self.animation { +// self.renderAnimationFrames = nil +// } else { +// self.renderAnimationFrames = (self.currentEntityView as? DrawingTextEntityView)?.getRenderAnimationFrames() +// } +// } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift new file mode 100644 index 0000000000..798c3cd22c --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift @@ -0,0 +1,110 @@ +import Foundation +import UIKit +import Display +import AccountContext + +public final class DrawingVectorEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case type + case color + case lineWidth + case drawingSize + case referenceDrawingSize + case start + case mid + case end + case renderImage + } + + public enum VectorType: Codable { + case line + case oneSidedArrow + case twoSidedArrow + } + + public let uuid: UUID + public let isAnimated: Bool + + public var type: VectorType + public var color: DrawingColor + public var lineWidth: CGFloat + + public var drawingSize: CGSize + public var referenceDrawingSize: CGSize + public var start: CGPoint + public var mid: (CGFloat, CGFloat) + public var end: CGPoint + + public var center: CGPoint { + return self.start + } + + public var scale: CGFloat = 1.0 + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingEntity]? + + public var isMedia: Bool { + return false + } + + public init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.isAnimated = false + + self.type = type + self.color = color + self.lineWidth = lineWidth + + self.drawingSize = .zero + self.referenceDrawingSize = .zero + self.start = CGPoint() + self.mid = (0.5, 0.0) + self.end = CGPoint() + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.isAnimated = false + self.type = try container.decode(VectorType.self, forKey: .type) + self.color = try container.decode(DrawingColor.self, forKey: .color) + self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth) + self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize) + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.start = try container.decode(CGPoint.self, forKey: .start) + let mid = try container.decode(CGPoint.self, forKey: .mid) + self.mid = (mid.x, mid.y) + self.end = try container.decode(CGPoint.self, forKey: .end) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.type, forKey: .type) + try container.encode(self.color, forKey: .color) + try container.encode(self.lineWidth, forKey: .lineWidth) + try container.encode(self.drawingSize, forKey: .drawingSize) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.start, forKey: .start) + try container.encode(CGPoint(x: self.mid.0, y: self.mid.1), forKey: .mid) + try container.encode(self.end, forKey: .end) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth) + newEntity.drawingSize = self.drawingSize + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.start = self.start + newEntity.mid = self.mid + newEntity.end = self.end + return newEntity + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 54852701d6..7b4392a677 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -89,6 +89,7 @@ public final class MediaEditor { videoTrimRange: nil, videoIsMuted: false, drawing: nil, + entities: [], toolValues: [:] ) } @@ -267,6 +268,10 @@ public final class MediaEditor { self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted) } + public func setDrawingAndEntities(data: Data?, image: UIImage?, entities: [CodableDrawingEntity]) { + self.values = self.values.withUpdatedDrawingAndEntities(drawing: image, entities: entities) + } + public func setGradientColors(_ gradientColors: [UIColor]) { self.values = self.values.withUpdatedGradientColors(gradientColors: gradientColors) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index 221794ce91..d17bed5105 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -4,6 +4,13 @@ import UIKit import CoreImage import Metal import Display +import SwiftSignalKit +import TelegramCore +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import YuvConversion +import StickerResources +import AccountContext final class MediaEditorComposer { let device: MTLDevice? @@ -17,15 +24,14 @@ final class MediaEditorComposer { private let renderer = MediaEditorRenderer() private let renderChain = MediaEditorRenderChain() - private var gradientImage: CIImage - - let semaphore = DispatchSemaphore(value: 1) + private let gradientImage: CIImage + private let drawingImage: CIImage? + private var entities: [MediaEditorComposerEntity] - init(values: MediaEditorValues, dimensions: CGSize) { + init(context: AccountContext, values: MediaEditorValues, dimensions: CGSize) { self.values = values self.dimensions = dimensions - self.renderer.externalSemaphore = self.semaphore self.renderer.addRenderChain(self.renderChain) self.renderer.addRenderPass(ComposerRenderPass()) @@ -36,6 +42,14 @@ final class MediaEditorComposer { self.gradientImage = CIImage(color: .black) } + if let drawing = values.drawing, let drawingImage = CIImage(image: drawing) { + self.drawingImage = drawingImage.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) + } else { + self.drawingImage = nil + } + + self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(context: context, entity: $0) } + self.device = MTLCreateSystemDefaultDevice() if let device = self.device { self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : NSNull()]) @@ -49,10 +63,13 @@ final class MediaEditorComposer { self.renderChain.update(values: self.values) } - func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?) -> CVPixelBuffer? { + func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?, completion: @escaping (CVPixelBuffer?) -> Void) { guard let textureCache = self.textureCache, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else { - return nil + completion(nil) + return } + let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + let width = CVPixelBufferGetWidth(imageBuffer) let height = CVPixelBufferGetHeight(imageBuffer) let format: MTLPixelFormat = .bgra8Unorm @@ -62,7 +79,6 @@ final class MediaEditorComposer { if status == kCVReturnSuccess { texture = CVMetalTextureGetTexture(textureRef!) } - if let texture { self.renderer.consumeTexture(texture, rotation: .rotate90Degrees) self.renderer.renderFrame() @@ -73,45 +89,59 @@ final class MediaEditorComposer { var pixelBuffer: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) - if let composition = processImage(inputImage: ciImage), let pixelBuffer { - self.ciContext?.render(composition, to: pixelBuffer) - - return pixelBuffer - } else { - return nil + if let pixelBuffer { + processImage(inputImage: ciImage, time: time, completion: { compositedImage in + if let compositedImage { + self.ciContext?.render(compositedImage, to: pixelBuffer) + completion(pixelBuffer) + } else { + completion(nil) + } + }) + return } - } else { - return nil } - } else { - return nil } + completion(nil) + return } - func processImage(inputImage: CIImage) -> CIImage? { - return makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: self.gradientImage, dimensions: self.dimensions, values: self.values) + func processImage(inputImage: CIImage, time: CMTime, completion: @escaping (CIImage?) -> Void) { + return makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: completion) } } -public func makeEditorImageComposition(inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues) -> UIImage? { +public func makeEditorImageComposition(context: AccountContext, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) { let inputImage = CIImage(image: inputImage)! let gradientImage: CIImage + var drawingImage: CIImage? if let gradientColors = values.gradientColors { let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])! gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } else { gradientImage = CIImage(color: .black) } - if let ciImage = makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, dimensions: dimensions, values: values) { - let context = CIContext(options: [.workingColorSpace : NSNull()]) - if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) { - return UIImage(cgImage: cgImage) - } + + if let drawing = values.drawing, let image = CIImage(image: drawing) { + drawingImage = image.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } - return nil + + let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(context: context, entity: $0) } + makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in + if let ciImage { + let context = CIContext(options: [.workingColorSpace : NSNull()]) + if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) { + Queue.mainQueue().async { + completion(UIImage(cgImage: cgImage)) + } + return + } + } + completion(nil) + }) } -private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: CIImage, dimensions: CGSize, values: MediaEditorValues) -> CIImage? { +private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, completion: @escaping (CIImage?) -> Void) { var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) resultImage = gradientImage.composited(over: resultImage) @@ -121,32 +151,377 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: cropTransform = cropTransform.rotated(by: -values.cropRotation) cropTransform = cropTransform.scaledBy(x: values.cropScale, y: values.cropScale) mediaImage = mediaImage.transformed(by: cropTransform) - resultImage = mediaImage.composited(over: resultImage) - - return resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0)) + + if let drawingImage { + resultImage = drawingImage.composited(over: resultImage) + } + + let frameRate: Float = 60.0 + + let entitiesCount = Atomic(value: 1) + let entitiesImages = Atomic<[(CIImage, Int)]>(value: []) + let maybeFinalize = { + let count = entitiesCount.modify { current -> Int in + return current - 1 + } + if count == 0 { + let sortedImages = entitiesImages.with({ $0 }).sorted(by: { $0.1 < $1.1 }).map({ $0.0 }) + for image in sortedImages { + resultImage = image.composited(over: resultImage) + } + + resultImage = resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0)) + completion(resultImage) + } + } + var i = 0 + for entity in entities { + let _ = entitiesCount.modify { current -> Int in + return current + 1 + } + let index = i + entity.image(for: time, frameRate: frameRate, completion: { image in + if var image = image { + var transform = CGAffineTransform(translationX: -image.extent.midX, y: -image.extent.midY) + image = image.transformed(by: transform) + + var scale = entity.scale * 1.0 + if let baseSize = entity.baseSize { + scale *= baseSize.width / image.extent.size.width + } + + transform = CGAffineTransform(translationX: entity.position.x, y: dimensions.height - entity.position.y) + transform = transform.rotated(by: CGFloat.pi * 2.0 - entity.rotation) + transform = transform.scaledBy(x: scale, y: scale) + if entity.mirrored { + transform = transform.scaledBy(x: -1.0, y: 1.0) + } + + image = image.transformed(by: transform) + let _ = entitiesImages.modify { current in + var updated = current + updated.append((image, index)) + return updated + } + } + maybeFinalize() + }) + i += 1 + } + maybeFinalize() } -extension CMSampleBuffer { - func newSampleBufferWithReplacedImageBuffer(_ imageBuffer: CVImageBuffer) -> CMSampleBuffer? { - guard let _ = CMSampleBufferGetImageBuffer(self) else { - return nil +private func composerEntityForDrawingEntity(context: AccountContext, entity: DrawingEntity) -> MediaEditorComposerEntity? { + if let entity = entity as? DrawingStickerEntity { + let content: MediaEditorComposerStickerEntity.Content + switch entity.content { + case let .file(file): + content = .file(file) + case let .image(image): + content = .image(image) } - var timingInfo = CMSampleTimingInfo() - guard CMSampleBufferGetSampleTimingInfo(self, at: 0, timingInfoOut: &timingInfo) == 0 else { - return nil + return MediaEditorComposerStickerEntity(context: context, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored) + } else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage) { + if let entity = entity as? DrawingBubbleEntity { + return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false) + } else if let entity = entity as? DrawingSimpleShapeEntity { + return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false) + } else if let entity = entity as? DrawingVectorEntity { + return MediaEditorComposerStaticEntity(image: image, position: CGPoint(x: entity.drawingSize.width * 0.5, y: entity.drawingSize.height * 0.5), scale: 1.0, rotation: 0.0, baseSize: entity.drawingSize, mirrored: false) + } else if let entity = entity as? DrawingTextEntity { + return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: nil, mirrored: false) } - var outputSampleBuffer: CMSampleBuffer? - var newFormatDescription: CMFormatDescription? - CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: imageBuffer, formatDescriptionOut: &newFormatDescription) - guard let formatDescription = newFormatDescription else { - return nil - } - CMSampleBufferCreateReadyWithImageBuffer(allocator: nil, imageBuffer: imageBuffer, formatDescription: formatDescription, sampleTiming: &timingInfo, sampleBufferOut: &outputSampleBuffer) - return outputSampleBuffer + } + return nil +} + +private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity { + let image: CIImage + let position: CGPoint + let scale: CGFloat + let rotation: CGFloat + let baseSize: CGSize? + let mirrored: Bool + + init(image: CIImage, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize?, mirrored: Bool) { + self.image = image + self.position = position + self.scale = scale + self.rotation = rotation + self.baseSize = baseSize + self.mirrored = mirrored + } + + func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) { + completion(self.image) } } +private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { + public enum Content { + case file(TelegramMediaFile) + case image(UIImage) + + var file: TelegramMediaFile? { + if case let .file(file) = self { + return file + } + return nil + } + } + + let content: Content + let position: CGPoint + let scale: CGFloat + let rotation: CGFloat + let baseSize: CGSize? + let mirrored: Bool + + var isAnimated: Bool + var source: AnimatedStickerNodeSource? + var frameSource = Promise?>() + + var frameCount: Int? + var frameRate: Int? + var currentFrameIndex: Int? + var totalDuration: Double? + let durationPromise = Promise() + + let queue = Queue() + let disposables = DisposableSet() + + var image: CIImage? + var imagePixelBuffer: CVPixelBuffer? + let imagePromise = Promise() + + init(context: AccountContext, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool) { + self.content = content + self.position = position + self.scale = scale + self.rotation = rotation + self.baseSize = baseSize + self.mirrored = mirrored + + switch content { + case let .file(file): + if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { + self.isAnimated = true + self.source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") + let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + if let source = self.source { + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384)) + self.disposables.add((source.directDataPath(attemptSynchronously: true) + |> deliverOn(self.queue)).start(next: { [weak self] path in + if let strongSelf = self, let path { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + let queue = strongSelf.queue + let frameSource = QueueLocalObject(queue: queue, generate: { + return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)! + //return AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})! + }) + frameSource.syncWith { frameSource in + strongSelf.frameCount = frameSource.frameCount + strongSelf.frameRate = frameSource.frameRate + + let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate) + strongSelf.totalDuration = duration + strongSelf.durationPromise.set(.single(duration)) + } + + strongSelf.frameSource.set(.single(frameSource)) + } + } + })) + } + } else { + self.isAnimated = false + self.disposables.add((chatMessageSticker(account: context.account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false) + |> deliverOn(self.queue)).start(next: { [weak self] generator in + if let strongSelf = self { + let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets())) + let image = context?.generateImage() + if let image = image { + strongSelf.imagePromise.set(.single(image)) + } + } + })) + } + case let .image(image): + self.isAnimated = false + self.imagePromise.set(.single(image)) + } + } + + deinit { + self.disposables.dispose() + } + + var tested = false + func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) { + if self.isAnimated { + let currentTime = CMTimeGetSeconds(time) + + var tintColor: UIColor? + if let file = self.content.file, file.isCustomTemplateEmoji { + tintColor = .white + } + +// let start = CACurrentMediaTime() + self.disposables.add((self.frameSource.get() + |> take(1) + |> deliverOn(self.queue)).start(next: { [weak self] frameSource in + guard let strongSelf = self else { + completion(nil) + return + } + + guard let frameSource, let duration = strongSelf.totalDuration, let frameCount = strongSelf.frameCount else { + completion(nil) + return + } + +// if !strongSelf.tested { +// frameSource.syncWith { frameSource in +// for _ in 0 ..< 60 * 3 { +// let _ = frameSource.takeFrame(draw: true) +// } +// } +// strongSelf.tested = true +// print("180 frames in \(CACurrentMediaTime() - start)") +// } + + let relativeTime = currentTime - floor(currentTime / duration) * duration + var t = relativeTime / duration + t = max(0.0, t) + t = min(1.0, t) + + let startFrame: Double = 0 + let endFrame = Double(frameCount) + + let frameOffset = Int(Double(startFrame) * (1.0 - t) + Double(endFrame - 1) * t) + let lowerBound: Int = 0 + let upperBound = frameCount - 1 + let frameIndex = max(lowerBound, min(upperBound, frameOffset)) + + let currentFrameIndex = strongSelf.currentFrameIndex + if currentFrameIndex != frameIndex { + let previousFrameIndex = currentFrameIndex + strongSelf.currentFrameIndex = frameIndex + + var delta = 1 + if let previousFrameIndex = previousFrameIndex { + delta = max(1, frameIndex - previousFrameIndex) + } + + //print("skipping: \(delta) frames") + + + var frame: AnimatedStickerFrame? + frameSource.syncWith { frameSource in + for i in 0 ..< delta { + frame = frameSource.takeFrame(draw: i == delta - 1) + } + } + if let frame { + //print("has frame: \(CACurrentMediaTime() - start)") + + var imagePixelBuffer: CVPixelBuffer? + if let pixelBuffer = strongSelf.imagePixelBuffer { + imagePixelBuffer = pixelBuffer + } else { + let ioSurfaceProperties = NSMutableDictionary() + let options = NSMutableDictionary() + options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString) + + var pixelBuffer: CVPixelBuffer? + CVPixelBufferCreate( + kCFAllocatorDefault, + frame.width, + frame.height, + kCVPixelFormatType_32BGRA, + options, + &pixelBuffer + ) + + imagePixelBuffer = pixelBuffer + strongSelf.imagePixelBuffer = pixelBuffer + } + + if let imagePixelBuffer { + let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor) + //print("image loaded in: \(CACurrentMediaTime() - start)") + strongSelf.image = image + } + completion(strongSelf.image) + } else { + completion(nil) + } + } else { + completion(strongSelf.image) + } + })) + } else { + var image: CIImage? + if let cachedImage = self.image { + image = cachedImage + completion(image) + } else { + let _ = (self.imagePromise.get() + |> take(1) + |> deliverOn(self.queue)).start(next: { [weak self] image in + if let strongSelf = self { + strongSelf.image = CIImage(image: image) + completion(strongSelf.image) + } + }) + } + } + } +} + +protocol MediaEditorComposerEntity { + var position: CGPoint { get } + var scale: CGFloat { get } + var rotation: CGFloat { get } + var baseSize: CGSize? { get } + var mirrored: Bool { get } + + func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) +} + +private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? { + //let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31) + //assert(bytesPerRow == calculatedBytesPerRow) + + + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + let dest = CVPixelBufferGetBaseAddress(pixelBuffer) + + switch type { + case .yuva: + data.withUnsafeBytes { buffer -> Void in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return + } + decodeYUVAToRGBA(bytes, dest, Int32(width), Int32(height), Int32(bytesPerRow)) + } + case .argb: + data.withUnsafeBytes { buffer -> Void in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return + } + memcpy(dest, bytes, data.count) + } + case .dct: + break + } + + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + + return CIImage(cvPixelBuffer: pixelBuffer) +} final class ComposerRenderPass: DefaultRenderPass { fileprivate var cachedTexture: MTLTexture? diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 094ffe95bd..4cb0e5c7fe 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -65,9 +65,7 @@ final class MediaEditorRenderer: TextureConsumer { private var library: MTLLibrary? var finalTexture: MTLTexture? - - var externalSemaphore: DispatchSemaphore? - + public init() { } @@ -175,7 +173,6 @@ final class MediaEditorRenderer: TextureConsumer { commandBuffer.addCompletedHandler { [weak self] _ in self?.semaphore.signal() - self?.externalSemaphore?.signal() } if let _ = self.renderTarget { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 1b92b82b31..e2e7c13724 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -34,6 +34,72 @@ private let adjustmentToolsKeys: [EditorToolKey] = [ .sharpen ] +public class MediaEditorValues { + public let originalDimensions: PixelDimensions + public let cropOffset: CGPoint + public let cropSize: CGSize? + public let cropScale: CGFloat + public let cropRotation: CGFloat + public let cropMirroring: Bool + + public let gradientColors: [UIColor]? + + public let videoTrimRange: Range? + public let videoIsMuted: Bool + + public let drawing: UIImage? + public let entities: [CodableDrawingEntity] + public let toolValues: [EditorToolKey: Any] + + init( + originalDimensions: PixelDimensions, + cropOffset: CGPoint, + cropSize: CGSize?, + cropScale: CGFloat, + cropRotation: CGFloat, + cropMirroring: Bool, + gradientColors: [UIColor]?, + videoTrimRange: Range?, + videoIsMuted: Bool, + drawing: UIImage?, + entities: [CodableDrawingEntity], + toolValues: [EditorToolKey: Any] + ) { + self.originalDimensions = originalDimensions + self.cropOffset = cropOffset + self.cropSize = cropSize + self.cropScale = cropScale + self.cropRotation = cropRotation + self.cropMirroring = cropMirroring + self.gradientColors = gradientColors + self.videoTrimRange = videoTrimRange + self.videoIsMuted = videoIsMuted + self.drawing = drawing + self.entities = entities + self.toolValues = toolValues + } + + func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues) + } + + func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues { + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues) + } + + func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues { + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues) + } + + func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues { + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: drawing, entities: entities, toolValues: self.toolValues) + } + + func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues { + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: toolValues) + } +} + public struct TintValue: Equatable { public static let initial = TintValue( color: .clear, @@ -300,64 +366,6 @@ public struct CurvesValue: Equatable { } } -public class MediaEditorValues { - public let originalDimensions: PixelDimensions - public let cropOffset: CGPoint - public let cropSize: CGSize? - public let cropScale: CGFloat - public let cropRotation: CGFloat - public let cropMirroring: Bool - - public let gradientColors: [UIColor]? - - public let videoTrimRange: Range? - public let videoIsMuted: Bool - - public let drawing: UIImage? - public let toolValues: [EditorToolKey: Any] - - init( - originalDimensions: PixelDimensions, - cropOffset: CGPoint, - cropSize: CGSize?, - cropScale: CGFloat, - cropRotation: CGFloat, - cropMirroring: Bool, - gradientColors: [UIColor]?, - videoTrimRange: Range?, - videoIsMuted: Bool, - drawing: UIImage?, - toolValues: [EditorToolKey: Any] - ) { - self.originalDimensions = originalDimensions - self.cropOffset = cropOffset - self.cropSize = cropSize - self.cropScale = cropScale - self.cropRotation = cropRotation - self.cropMirroring = cropMirroring - self.gradientColors = gradientColors - self.videoTrimRange = videoTrimRange - self.videoIsMuted = videoIsMuted - self.drawing = drawing - self.toolValues = toolValues - } - - func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, toolValues: self.toolValues) - } - - func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, toolValues: self.toolValues) - } - - func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, drawing: self.drawing, toolValues: self.toolValues) - } - - func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, toolValues: toolValues) - } -} private let toolEpsilon: Float = 0.005 public extension MediaEditorValues { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 8539fe9b0f..7ec4b9fe29 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -2,6 +2,7 @@ import Foundation import AVFoundation import MetalKit import SwiftSignalKit +import AccountContext enum ExportWriterStatus { case unknown @@ -239,6 +240,7 @@ public final class MediaEditorVideoExport { public private(set) var internalStatus: Status = .idle + private let context: AccountContext private let subject: Subject private let configuration: Configuration private let outputPath: String @@ -262,7 +264,10 @@ public final class MediaEditorVideoExport { private var startTimestamp = CACurrentMediaTime() - public init(subject: Subject, configuration: Configuration, outputPath: String) { + private let semaphore = DispatchSemaphore(value: 0) + + public init(context: AccountContext, subject: Subject, configuration: Configuration, outputPath: String) { + self.context = context self.subject = subject self.configuration = configuration self.outputPath = outputPath @@ -284,7 +289,7 @@ public final class MediaEditorVideoExport { } if self.configuration.values.requiresComposing { - self.composer = MediaEditorComposer(values: self.configuration.values, dimensions: self.configuration.dimensions) + self.composer = MediaEditorComposer(context: self.context, values: self.configuration.values, dimensions: self.configuration.dimensions) } self.setupVideoInput() } @@ -336,7 +341,7 @@ public final class MediaEditorVideoExport { } let audioTracks = asset.tracks(withMediaType: .audio) - if audioTracks.count > 0 { + if audioTracks.count > 0, !self.configuration.values.videoIsMuted { let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil) audioOutput.alwaysCopiesSampleData = false if reader.canAdd(audioOutput) { @@ -420,28 +425,40 @@ public final class MediaEditorVideoExport { return false } + var appendFailed = false while writer.isReadyForMoreVideoData { - let _ = self.composer?.semaphore.wait(timeout: .distantFuture) - + if appendFailed { + return false + } if reader.status != .reading || writer.status != .writing { writer.markVideoAsFinished() return false } self.pauseDispatchGroup.wait() if let buffer = output.copyNextSampleBuffer() { - if let pixelBuffer = self.composer?.processSampleBuffer(buffer, pool: writer.pixelBufferPool) { + if let composer = self.composer { let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer) - if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) { - writer.markVideoAsFinished() - return false - } + composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, completion: { pixelBuffer in + if let pixelBuffer { + if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) { + writer.markVideoAsFinished() + appendFailed = true + } + } else { + if !writer.appendVideoBuffer(buffer) { + writer.markVideoAsFinished() + appendFailed = true + } + } + self.semaphore.signal() + }) + self.semaphore.wait() } else { if !writer.appendVideoBuffer(buffer) { writer.markVideoAsFinished() return false } } - // let progress = (CMSampleBufferGetPresentationTimeStamp(buffer) - self.configuration.timeRange.start).seconds/self.duration.seconds // if self.videoOutput === output { // self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: progress) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 566a137234..a05a4d0bb5 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1044,6 +1044,12 @@ public final class MediaEditorScreen: ViewController { self?.drawingView.isUserInteractionEnabled = false self?.animateInFromTool() + if let result = controller?.generateDrawingResultData() { + self?.mediaEditor?.setDrawingAndEntities(data: result.data, image: result.drawingImage, entities: result.entities) + } else { + self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: []) + } + selectionContainerView?.removeFromSuperview() } self.controller?.present(controller, in: .current) @@ -1217,13 +1223,15 @@ public final class MediaEditorScreen: ViewController { }) } else { if let image = mediaEditor.resultImage { - if let resultImage = makeEditorImageComposition(inputImage: image, dimensions: storyDimensions, values: mediaEditor.values) { - self.completion(.image(resultImage, nil), { [weak self] in - self?.node.animateOut(completion: { [weak self] in - self?.dismiss() + makeEditorImageComposition(context: self.context, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in + if let resultImage { + self.completion(.image(resultImage, nil), { [weak self] in + self?.node.animateOut(completion: { [weak self] in + self?.dismiss() + }) }) - }) - } + } + }) } } } @@ -1267,7 +1275,7 @@ public final class MediaEditorScreen: ViewController { let configuration = recommendedExportConfiguration(mediaEditor: mediaEditor) let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4" - let export = MediaEditorVideoExport(subject: exportSubject, configuration: configuration, outputPath: outputPath) + let export = MediaEditorVideoExport(context: self.context, subject: exportSubject, configuration: configuration, outputPath: outputPath) self.export = export export.startExport() @@ -1283,12 +1291,13 @@ public final class MediaEditorScreen: ViewController { }) } else { if let image = mediaEditor.resultImage { - let resultImage = makeEditorImageComposition(inputImage: image, dimensions: storyDimensions, values: mediaEditor.values) - if let data = resultImage?.jpegData(compressionQuality: 0.8) { - let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg" - try? data.write(to: URL(fileURLWithPath: outputPath)) - saveToPhotos(outputPath, false) - } + makeEditorImageComposition(context: self.context, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in + if let data = resultImage?.jpegData(compressionQuality: 0.8) { + let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg" + try? data.write(to: URL(fileURLWithPath: outputPath)) + saveToPhotos(outputPath, false) + } + }) } } }