Camera and editor improvements

This commit is contained in:
Ilya Laktyushin 2023-05-15 15:12:51 +04:00
parent 37e7a74e4e
commit a2d7cfba4f
39 changed files with 1877 additions and 1338 deletions

View File

@ -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,

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -5,6 +5,7 @@ import MetalKit
import Display
import SwiftSignalKit
import AppBundle
import MediaEditor
final class DrawingMetalView: MTKView {
let size: CGSize

View File

@ -1,6 +1,7 @@
import Foundation
import UIKit
import Display
import MediaEditor
final class NeonTool: DrawingElement {
class RenderView: UIView, DrawingRenderView {

View File

@ -1,6 +1,7 @@
import Foundation
import UIKit
import Display
import MediaEditor
private let activeWidthFactor: CGFloat = 0.7

View File

@ -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<Void>
let insertSticker: ActionSlot<Void>
let insertText: ActionSlot<Void>
let updateEntityView: ActionSlot<(UUID, Bool)>
let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
let apply: ActionSlot<Void>
let dismiss: ActionSlot<Void>
@ -533,6 +542,8 @@ private final class DrawingScreenComponent: CombinedComponent {
toggleWithPreviousTool: ActionSlot<Void>,
insertSticker: ActionSlot<Void>,
insertText: ActionSlot<Void>,
updateEntityView: ActionSlot<(UUID, Bool)>,
endEditingTextEntityView: ActionSlot<(UUID, Bool)>,
apply: ActionSlot<Void>,
dismiss: ActionSlot<Void>,
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<Void>
private let insertSticker: ActionSlot<Void>
private let insertText: ActionSlot<Void>
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<Void>,
insertSticker: ActionSlot<Void>,
insertText: ActionSlot<Void>,
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<Void>
fileprivate let insertSticker: ActionSlot<Void>
fileprivate let insertText: ActionSlot<Void>
private let updateEntityView: ActionSlot<(UUID, Bool)>
private let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
private let apply: ActionSlot<Void>
private let dismiss: ActionSlot<Void>
@ -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<Void>()
self.insertSticker = ActionSlot<Void>()
self.insertText = ActionSlot<Void>()
self.updateEntityView = ActionSlot<(UUID, Bool)>()
self.endEditingTextEntityView = ActionSlot<(UUID, Bool)>()
self.apply = ActionSlot<Void>()
self.dismiss = ActionSlot<Void>()
@ -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())

View File

@ -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 {

View File

@ -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())

View File

@ -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

View File

@ -1,6 +1,7 @@
import Foundation
import UIKit
import Display
import MediaEditor
final class MarkerTool: DrawingElement {
let uuid: UUID

View File

@ -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) {

View File

@ -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

View File

@ -6,6 +6,7 @@ import ComponentFlow
import LegacyComponents
import AppBundle
import ImageBlur
import MediaEditor
protocol DrawingRenderLayer: CALayer {

View File

@ -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

View File

@ -5,6 +5,7 @@ import ComponentFlow
import LegacyComponents
import TelegramCore
import LottieAnimationComponent
import MediaEditor
enum DrawingTextStyle: Equatable {
case regular

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/DrawingUI:DrawingUI",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/TelegramUI/Components/MediaEditor",
],
visibility = [
"//visibility:public",

View File

@ -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 {

View File

@ -497,9 +497,25 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
})
let dataSignal: Signal<PeersNearbyData?, NoError> = coordinatePromise.get()
|> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs?.latitude == rhs?.latitude && lhs?.longitude == rhs?.longitude
})
|> mapToSignal { coordinate -> Signal<PeersNearbyData?, NoError> 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

View File

@ -14,7 +14,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal<Account
let phoneNumberPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyPhoneNumber))
let phoneDiscoveryPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyAddedByPhone))
let voiceMessagesPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyVoiceMessages))
let bioPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyProfilePhoto))
let bioPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyAbout))
let autoremoveTimeout = account.network.request(Api.functions.account.getAccountTTL())
let globalPrivacySettings = account.network.request(Api.functions.account.getGlobalPrivacySettings())
let messageAutoremoveTimeout = account.network.request(Api.functions.messages.getDefaultHistoryTTL())
@ -253,7 +253,7 @@ public enum UpdateSelectiveAccountPrivacySettingsType {
case .voiceMessages:
return .inputPrivacyKeyVoiceMessages
case .bio:
return .inputPrivacyKeyProfilePhoto
return .inputPrivacyKeyAbout
}
}
}

View File

@ -38,6 +38,7 @@ swift_library(
"//submodules/StickerResources:StickerResources",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/MediaEditor",
],
visibility = [
"//visibility:public",

View File

@ -23,6 +23,7 @@ import DrawingUI
import SolidRoundedButtonComponent
import AnimationCache
import EmojiTextAttachmentView
import MediaEditor
enum AvatarBackground: Equatable {
case gradient([UInt32])

View File

@ -61,6 +61,11 @@ swift_library(
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
"//submodules/TextFormat:TextFormat",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/StickerResources:StickerResources",
"//submodules/YuvConversion:YuvConversion",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,93 @@
import Foundation
public enum CodableDrawingEntity {
case sticker(DrawingStickerEntity)
case text(DrawingTextEntity)
case simpleShape(DrawingSimpleShapeEntity)
case bubble(DrawingBubbleEntity)
case vector(DrawingVectorEntity)
public 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
}
}
public 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
}
}
}
extension CodableDrawingEntity: Codable {
private enum CodingKeys: String, CodingKey {
case type
case entity
}
private enum EntityType: Int, Codable {
case sticker
case text
case simpleShape
case bubble
case vector
}
public 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))
}
}
public 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)
}
}
}

View File

@ -0,0 +1,107 @@
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
}
public enum DrawType: Codable {
case fill
case stroke
}
public let uuid: UUID
public let isAnimated: Bool
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 tailPosition: CGPoint
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(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
}
}

View File

@ -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
}
}

View File

@ -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 }
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()
// }
// }
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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<Int>(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<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
var frameCount: Int?
var frameRate: Int?
var currentFrameIndex: Int?
var totalDuration: Double?
let durationPromise = Promise<Double>()
let queue = Queue()
let disposables = DisposableSet()
var image: CIImage?
var imagePixelBuffer: CVPixelBuffer?
let imagePromise = Promise<UIImage>()
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<AnimatedStickerDirectFrameSource>(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?

View File

@ -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 {

View File

@ -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<Double>?
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<Double>?,
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<Double>?
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<Double>?,
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 {

View File

@ -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) }

View File

@ -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)
}
})
}
}
}