import Foundation import UIKit import Display import TelegramCore import AVFoundation import VideoToolbox public enum EditorToolKey: Int32, CaseIterable { case enhance case brightness case contrast case saturation case warmth case fade case highlights case shadows case vignette case grain case sharpen case shadowsTint case highlightsTint case blur case curves static let adjustmentToolsKeys: [EditorToolKey] = [ .enhance, .brightness, .contrast, .saturation, .warmth, .fade, .highlights, .shadows, .vignette, .grain, .sharpen ] } public struct VideoPositionChange: Codable, Equatable { private enum CodingKeys: String, CodingKey { case additional case timestamp } public let additional: Bool public let timestamp: Double public init(additional: Bool, timestamp: Double) { self.additional = additional self.timestamp = timestamp } } public final class MediaEditorValues: Codable, Equatable { public static func == (lhs: MediaEditorValues, rhs: MediaEditorValues) -> Bool { if lhs.originalDimensions != rhs.originalDimensions { return false } if lhs.cropOffset != rhs.cropOffset { return false } if lhs.cropSize != rhs.cropSize { return false } if lhs.cropScale != rhs.cropScale { return false } if lhs.cropRotation != rhs.cropRotation { return false } if lhs.cropMirroring != rhs.cropMirroring { return false } if lhs.gradientColors != rhs.gradientColors { return false } if lhs.videoTrimRange != rhs.videoTrimRange { return false } if lhs.videoIsMuted != rhs.videoIsMuted { return false } if lhs.videoIsFullHd != rhs.videoIsFullHd { return false } if lhs.videoIsMirrored != rhs.videoIsMirrored { return false } if lhs.additionalVideoPath != rhs.additionalVideoPath { return false } if lhs.additionalVideoPosition != rhs.additionalVideoPosition { return false } if lhs.additionalVideoScale != rhs.additionalVideoScale { return false } if lhs.additionalVideoRotation != rhs.additionalVideoRotation { return false } if lhs.additionalVideoPositionChanges != rhs.additionalVideoPositionChanges { return false } if lhs.drawing !== rhs.drawing { return false } if lhs.entities != rhs.entities { return false } for key in EditorToolKey.allCases { let lhsToolValue = lhs.toolValues[key] let rhsToolValue = rhs.toolValues[key] if (lhsToolValue == nil) != (rhsToolValue == nil) { return false } if let lhsToolValue = lhsToolValue as? Float, let rhsToolValue = rhsToolValue as? Float { return lhsToolValue != rhsToolValue } if let lhsToolValue = lhsToolValue as? BlurValue, let rhsToolValue = rhsToolValue as? BlurValue { return lhsToolValue != rhsToolValue } if let lhsToolValue = lhsToolValue as? TintValue, let rhsToolValue = rhsToolValue as? TintValue { return lhsToolValue != rhsToolValue } if let lhsToolValue = lhsToolValue as? CurvesValue, let rhsToolValue = rhsToolValue as? CurvesValue { return lhsToolValue != rhsToolValue } } return true } private enum CodingKeys: String, CodingKey { case originalWidth case originalHeight case cropOffset case cropSize case cropScale case cropRotation case cropMirroring case gradientColors case videoTrimRange case videoIsMuted case videoIsFullHd case videoIsMirrored case additionalVideoPath case additionalVideoPosition case additionalVideoScale case additionalVideoRotation case additionalVideoPositionChanges case drawing case entities case toolValues } public let originalDimensions: PixelDimensions public let cropOffset: CGPoint public let cropSize: CGSize? public let cropScale: CGFloat public let cropRotation: CGFloat public let cropMirroring: Bool public let gradientColors: [UIColor]? public let videoTrimRange: Range? public let videoIsMuted: Bool public let videoIsFullHd: Bool public let videoIsMirrored: Bool public let additionalVideoPath: String? public let additionalVideoPosition: CGPoint? public let additionalVideoScale: CGFloat? public let additionalVideoRotation: CGFloat? public let additionalVideoPositionChanges: [VideoPositionChange] public let drawing: UIImage? public let entities: [CodableDrawingEntity] public let toolValues: [EditorToolKey: Any] init( originalDimensions: PixelDimensions, cropOffset: CGPoint, cropSize: CGSize?, cropScale: CGFloat, cropRotation: CGFloat, cropMirroring: Bool, gradientColors: [UIColor]?, videoTrimRange: Range?, videoIsMuted: Bool, videoIsFullHd: Bool, videoIsMirrored: Bool, additionalVideoPath: String?, additionalVideoPosition: CGPoint?, additionalVideoScale: CGFloat?, additionalVideoRotation: CGFloat?, additionalVideoPositionChanges: [VideoPositionChange], 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.videoIsFullHd = videoIsFullHd self.videoIsMirrored = videoIsMirrored self.additionalVideoPath = additionalVideoPath self.additionalVideoPosition = additionalVideoPosition self.additionalVideoScale = additionalVideoScale self.additionalVideoRotation = additionalVideoRotation self.additionalVideoPositionChanges = additionalVideoPositionChanges self.drawing = drawing self.entities = entities self.toolValues = toolValues } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let width = try container.decode(Int32.self, forKey: .originalWidth) let height = try container.decode(Int32.self, forKey: .originalHeight) self.originalDimensions = PixelDimensions(width: width, height: height) self.cropOffset = try container.decode(CGPoint.self, forKey: .cropOffset) self.cropSize = try container.decodeIfPresent(CGSize.self, forKey: .cropSize) self.cropScale = try container.decode(CGFloat.self, forKey: .cropScale) self.cropRotation = try container.decode(CGFloat.self, forKey: .cropRotation) self.cropMirroring = try container.decode(Bool.self, forKey: .cropMirroring) if let gradientColors = try container.decodeIfPresent([DrawingColor].self, forKey: .gradientColors) { self.gradientColors = gradientColors.map { $0.toUIColor() } } else { self.gradientColors = nil } self.videoTrimRange = try container.decodeIfPresent(Range.self, forKey: .videoTrimRange) self.videoIsMuted = try container.decode(Bool.self, forKey: .videoIsMuted) self.videoIsFullHd = try container.decodeIfPresent(Bool.self, forKey: .videoIsFullHd) ?? false self.videoIsMirrored = try container.decodeIfPresent(Bool.self, forKey: .videoIsMirrored) ?? false self.additionalVideoPath = try container.decodeIfPresent(String.self, forKey: .additionalVideoPath) self.additionalVideoPosition = try container.decodeIfPresent(CGPoint.self, forKey: .additionalVideoPosition) self.additionalVideoScale = try container.decodeIfPresent(CGFloat.self, forKey: .additionalVideoScale) self.additionalVideoRotation = try container.decodeIfPresent(CGFloat.self, forKey: .additionalVideoRotation) self.additionalVideoPositionChanges = try container.decodeIfPresent([VideoPositionChange].self, forKey: .additionalVideoPositionChanges) ?? [] if let drawingData = try container.decodeIfPresent(Data.self, forKey: .drawing), let image = UIImage(data: drawingData) { self.drawing = image } else { self.drawing = nil } self.entities = try container.decode([CodableDrawingEntity].self, forKey: .entities) let values = try container.decode([CodableToolValue].self, forKey: .toolValues) var toolValues: [EditorToolKey: Any] = [:] for value in values { let (key, value) = value.keyAndValue toolValues[key] = value } self.toolValues = toolValues } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.originalDimensions.width, forKey: .originalWidth) try container.encode(self.originalDimensions.height, forKey: .originalHeight) try container.encode(self.cropOffset, forKey: .cropOffset) try container.encode(self.cropSize, forKey: .cropSize) try container.encode(self.cropScale, forKey: .cropScale) try container.encode(self.cropRotation, forKey: .cropRotation) try container.encode(self.cropMirroring, forKey: .cropMirroring) if let gradientColors = self.gradientColors { try container.encode(gradientColors.map { DrawingColor(color: $0) }, forKey: .gradientColors) } try container.encodeIfPresent(self.videoTrimRange, forKey: .videoTrimRange) try container.encode(self.videoIsMuted, forKey: .videoIsMuted) try container.encode(self.videoIsFullHd, forKey: .videoIsFullHd) try container.encode(self.videoIsMirrored, forKey: .videoIsMirrored) try container.encodeIfPresent(self.additionalVideoPath, forKey: .additionalVideoPath) try container.encodeIfPresent(self.additionalVideoPosition, forKey: .additionalVideoPosition) try container.encodeIfPresent(self.additionalVideoScale, forKey: .additionalVideoScale) try container.encodeIfPresent(self.additionalVideoRotation, forKey: .additionalVideoRotation) try container.encodeIfPresent(self.additionalVideoPositionChanges, forKey: .additionalVideoPositionChanges) if let drawing = self.drawing, let pngDrawingData = drawing.pngData() { try container.encode(pngDrawingData, forKey: .drawing) } try container.encode(self.entities, forKey: .entities) var values: [CodableToolValue] = [] for (key, value) in self.toolValues { if let toolValue = CodableToolValue(key: key, value: value) { values.append(toolValue) } } try container.encode(values, forKey: .toolValues) } public func makeCopy() -> 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, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.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, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, 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, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, 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, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues) } func withUpdatedVideoIsFullHd(_ videoIsFullHd: 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: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues) } func withUpdatedVideoIsMirrored(_ videoIsMirrored: 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: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues) } func withUpdatedAdditionalVideo(path: String, positionChanges: [VideoPositionChange]) -> 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: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: path, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues) } func withUpdatedAdditionalVideo(position: CGPoint, scale: CGFloat, rotation: CGFloat) -> 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: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues) } func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> 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: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, 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, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, 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, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: toolValues) } public var resultDimensions: PixelDimensions { if self.videoIsFullHd { return PixelDimensions(width: 1080, height: 1920) } else { return PixelDimensions(width: 720, height: 1280) } } public var hasChanges: Bool { if self.cropOffset != .zero { return true } if self.cropScale != 1.0 { return true } if self.cropRotation != 0.0 { return true } if self.cropMirroring { return true } if self.videoTrimRange != nil { return true } if self.drawing != nil { return true } if !self.entities.isEmpty { return true } if !self.toolValues.isEmpty { return true } return false } } public struct TintValue: Equatable, Codable { private enum CodingKeys: String, CodingKey { case color case intensity } public static let initial = TintValue( color: .clear, intensity: 0.5 ) public let color: UIColor public let intensity: Float public init( color: UIColor, intensity: Float ) { self.color = color self.intensity = intensity } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.color = try container.decode(DrawingColor.self, forKey: .color).toUIColor() self.intensity = try container.decode(Float.self, forKey: .intensity) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(DrawingColor(color: self.color), forKey: .color) try container.encode(self.intensity, forKey: .intensity) } public func withUpdatedColor(_ color: UIColor) -> TintValue { return TintValue(color: color, intensity: self.intensity) } public func withUpdatedIntensity(_ intensity: Float) -> TintValue { return TintValue(color: self.color, intensity: intensity) } } public struct BlurValue: Equatable, Codable { private enum CodingKeys: String, CodingKey { case mode case intensity case position case size case falloff case rotation } public static let initial = BlurValue( mode: .off, intensity: 0.5, position: CGPoint(x: 0.5, y: 0.5), size: 0.24, falloff: 0.12, rotation: 0.0 ) public enum Mode: Int32, Equatable { case off case radial case linear case portrait } public let mode: Mode public let intensity: Float public let position: CGPoint public let size: Float public let falloff: Float public let rotation: Float public init( mode: Mode, intensity: Float, position: CGPoint, size: Float, falloff: Float, rotation: Float ) { self.mode = mode self.intensity = intensity self.position = position self.size = size self.falloff = falloff self.rotation = rotation } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.mode = try BlurValue.Mode(rawValue: container.decode(Int32.self, forKey: .mode)) ?? .off self.intensity = try container.decode(Float.self, forKey: .intensity) self.position = try container.decode(CGPoint.self, forKey: .position) self.size = try container.decode(Float.self, forKey: .size) self.falloff = try container.decode(Float.self, forKey: .falloff) self.rotation = try container.decode(Float.self, forKey: .rotation) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.mode.rawValue, forKey: .mode) try container.encode(self.intensity, forKey: .intensity) try container.encode(self.position, forKey: .position) try container.encode(self.size, forKey: .size) try container.encode(self.falloff, forKey: .falloff) try container.encode(self.rotation, forKey: .rotation) } public func withUpdatedMode(_ mode: Mode) -> BlurValue { return BlurValue( mode: mode, intensity: self.intensity, position: self.position, size: self.size, falloff: self.falloff, rotation: self.rotation ) } public func withUpdatedIntensity(_ intensity: Float) -> BlurValue { return BlurValue( mode: self.mode, intensity: intensity, position: self.position, size: self.size, falloff: self.falloff, rotation: self.rotation ) } public func withUpdatedPosition(_ position: CGPoint) -> BlurValue { return BlurValue( mode: self.mode, intensity: self.intensity, position: position, size: self.size, falloff: self.falloff, rotation: self.rotation ) } public func withUpdatedSize(_ size: Float) -> BlurValue { return BlurValue( mode: self.mode, intensity: self.intensity, position: self.position, size: size, falloff: self.falloff, rotation: self.rotation ) } public func withUpdatedFalloff(_ falloff: Float) -> BlurValue { return BlurValue( mode: self.mode, intensity: self.intensity, position: self.position, size: self.size, falloff: falloff, rotation: self.rotation ) } public func withUpdatedRotation(_ rotation: Float) -> BlurValue { return BlurValue( mode: self.mode, intensity: self.intensity, position: self.position, size: self.size, falloff: self.falloff, rotation: rotation ) } } public struct CurvesValue: Equatable, Codable { private enum CodingKeys: String, CodingKey { case all case red case green case blue } public struct CurveValue: Equatable, Codable { private enum CodingKeys: String, CodingKey { case blacks case shadows case midtones case highlights case whites } public static let initial = CurveValue( blacks: 0.0, shadows: 0.25, midtones: 0.5, highlights: 0.75, whites: 1.0 ) public let blacks: Float public let shadows: Float public let midtones: Float public let highlights: Float public let whites: Float lazy var dataPoints: [Float] = { let points: [Float] = [ self.blacks, self.blacks, self.shadows, self.midtones, self.highlights, self.whites, self.whites ] let (_, dataPoints) = curveThroughPoints( count: points.count, valueAtIndex: { index in return points[index] }, positionAtIndex: { index, _ in switch index { case 0: return -0.001 case 1: return 0.0 case 2: return 0.25 case 3: return 0.5 case 4: return 0.75 case 5: return 1.0 default: return 1.001 } }, size: CGSize(width: 1.0, height: 1.0), type: .line, granularity: 100, floor: false ) return dataPoints }() public init( blacks: Float, shadows: Float, midtones: Float, highlights: Float, whites: Float ) { self.blacks = blacks self.shadows = shadows self.midtones = midtones self.highlights = highlights self.whites = whites } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.blacks = try container.decode(Float.self, forKey: .blacks) self.shadows = try container.decode(Float.self, forKey: .shadows) self.midtones = try container.decode(Float.self, forKey: .midtones) self.highlights = try container.decode(Float.self, forKey: .highlights) self.whites = try container.decode(Float.self, forKey: .whites) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.blacks, forKey: .blacks) try container.encode(self.shadows, forKey: .shadows) try container.encode(self.midtones, forKey: .midtones) try container.encode(self.highlights, forKey: .highlights) try container.encode(self.whites, forKey: .whites) } public func withUpdatedBlacks(_ blacks: Float) -> CurveValue { return CurveValue(blacks: blacks, shadows: self.shadows, midtones: self.midtones, highlights: self.highlights, whites: self.whites) } public func withUpdatedShadows(_ shadows: Float) -> CurveValue { return CurveValue(blacks: self.blacks, shadows: shadows, midtones: self.midtones, highlights: self.highlights, whites: self.whites) } public func withUpdatedMidtones(_ midtones: Float) -> CurveValue { return CurveValue(blacks: self.blacks, shadows: self.shadows, midtones: midtones, highlights: self.highlights, whites: self.whites) } public func withUpdatedHighlights(_ highlights: Float) -> CurveValue { return CurveValue(blacks: self.blacks, shadows: self.shadows, midtones: self.midtones, highlights: highlights, whites: self.whites) } public func withUpdatedWhites(_ whites: Float) -> CurveValue { return CurveValue(blacks: self.blacks, shadows: self.shadows, midtones: self.midtones, highlights: self.highlights, whites: whites) } } public static let initial = CurvesValue( all: CurveValue.initial, red: CurveValue.initial, green: CurveValue.initial, blue: CurveValue.initial ) public var all: CurveValue public var red: CurveValue public var green: CurveValue public var blue: CurveValue public init( all: CurveValue, red: CurveValue, green: CurveValue, blue: CurveValue ) { self.all = all self.red = red self.green = green self.blue = blue } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.all = try container.decode(CurveValue.self, forKey: .all) self.red = try container.decode(CurveValue.self, forKey: .red) self.green = try container.decode(CurveValue.self, forKey: .green) self.blue = try container.decode(CurveValue.self, forKey: .blue) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.all, forKey: .all) try container.encode(self.red, forKey: .red) try container.encode(self.green, forKey: .green) try container.encode(self.blue, forKey: .blue) } public func withUpdatedAll(_ all: CurveValue) -> CurvesValue { return CurvesValue(all: all, red: self.red, green: self.green, blue: self.blue) } public func withUpdatedRed(_ red: CurveValue) -> CurvesValue { return CurvesValue(all: self.all, red: red, green: self.green, blue: self.blue) } public func withUpdatedGreen(_ green: CurveValue) -> CurvesValue { return CurvesValue(all: self.all, red: self.red, green: green, blue: self.blue) } public func withUpdatedBlue(_ blue: CurveValue) -> CurvesValue { return CurvesValue(all: self.all, red: self.red, green: self.green, blue: blue) } } private let toolEpsilon: Float = 0.005 public extension MediaEditorValues { var hasAdjustments: Bool { for key in EditorToolKey.adjustmentToolsKeys { if let value = self.toolValues[key] as? Float, abs(value) > toolEpsilon { return true } } return false } var hasTint: Bool { if let tintValue = self.toolValues[.shadowsTint] as? TintValue, tintValue.color != .clear && tintValue.intensity > toolEpsilon { return true } else if let tintValue = self.toolValues[.highlightsTint] as? TintValue, tintValue.color != .clear && tintValue.intensity > toolEpsilon { return true } else { return false } } var hasBlur: Bool { if let blurValue = self.toolValues[.blur] as? BlurValue, blurValue.mode != .off && blurValue.intensity > toolEpsilon { return true } else { return false } } var hasCurves: Bool { if let curvesValue = self.toolValues[.curves] as? CurvesValue, curvesValue != CurvesValue.initial { return true } else { return false } } var requiresComposing: Bool { if self.originalDimensions.width > 0 && abs((Double(self.originalDimensions.height) / Double(self.originalDimensions.width)) - 1.7777778) > 0.001 { return true } if abs(1.0 - self.cropScale) > 0.0 { return true } if self.cropOffset != .zero { return true } if abs(self.cropRotation) > 0.0 { return true } if self.cropMirroring { return true } if self.hasAdjustments { return true } if self.hasTint { return true } if self.hasBlur { return true } if self.hasCurves { return true } if self.drawing != nil { return true } if !self.entities.isEmpty { return true } return false } } public class MediaEditorHistogram: Equatable { public class HistogramBins: Equatable { public static func == (lhs: HistogramBins, rhs: HistogramBins) -> Bool { if lhs.count != rhs.count { return false } if lhs.max != rhs.max { return false } if lhs.values != rhs.values { return false } return true } let values: [UInt32] let max: UInt32 public var count: Int { return self.values.count } init(values: [UInt32], max: UInt32) { self.values = values self.max = max } public func valueAtIndex(_ index: Int, mirrored: Bool = false) -> Float { if index >= 0 && index < values.count, self.max > 0 { let value = Float(self.values[index]) / Float(self.max) return mirrored ? 1.0 - value : value } else { return 0.0 } } } public static func == (lhs: MediaEditorHistogram, rhs: MediaEditorHistogram) -> Bool { if lhs.luminance != rhs.luminance { return false } if lhs.red != rhs.red { return false } if lhs.green != rhs.green { return false } if lhs.blue != rhs.blue { return false } return true } public let luminance: HistogramBins public let red: HistogramBins public let green: HistogramBins public let blue: HistogramBins public init(data: Data) { let count = 256 var maxRed: UInt32 = 0 var redValues: [UInt32] = [] var maxGreen: UInt32 = 0 var greenValues: [UInt32] = [] var maxBlue: UInt32 = 0 var blueValues: [UInt32] = [] var maxLuma: UInt32 = 0 var lumaValues: [UInt32] = [] data.withUnsafeBytes { pointer in if let red = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self) { for i in 0 ..< count { redValues.append(red[i]) if red[i] > maxRed { maxRed = red[i] } } } if let green = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self).advanced(by: count) { for i in 0 ..< count { greenValues.append(green[i]) if green[i] > maxGreen { maxGreen = green[i] } } } if let blue = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self).advanced(by: count * 2) { for i in 0 ..< count { blueValues.append(blue[i]) if blue[i] > maxBlue { maxBlue = blue[i] } } } if let luma = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self).advanced(by: count * 3) { for i in 0 ..< count { lumaValues.append(luma[i]) if luma[i] > maxLuma { maxLuma = luma[i] } } } } self.luminance = HistogramBins(values: lumaValues, max: maxLuma) self.red = HistogramBins(values: redValues, max: maxRed) self.green = HistogramBins(values: greenValues, max: maxGreen) self.blue = HistogramBins(values: blueValues, max: maxBlue) } init( luminance: HistogramBins, red: HistogramBins, green: HistogramBins, blue: HistogramBins ) { self.luminance = luminance self.red = red self.green = green self.blue = blue } } public enum MediaEditorCurveType { case filled case line } public func curveThroughPoints(count: Int, valueAtIndex: (Int) -> Float, positionAtIndex: (Int, CGFloat) -> CGFloat, size: CGSize, type: MediaEditorCurveType, granularity: Int, floor: Bool) -> (UIBezierPath, [Float]) { let path = UIBezierPath() var dataPoints: [Float] = [] let firstValue = valueAtIndex(0) switch type { case .filled: path.move(to: CGPoint(x: -1.0, y: size.height)) path.addLine(to: CGPoint(x: -1.0, y: CGFloat(firstValue) * size.height)) case .line: path.move(to: CGPoint(x: -1.0, y: CGFloat(firstValue) * size.height)) } let step = size.width / CGFloat(count) func pointAtIndex(_ index: Int) -> CGPoint { if floor { return CGPoint(x: floorToScreenPixels(positionAtIndex(index, step)), y: floorToScreenPixels(CGFloat(valueAtIndex(index)) * size.height)) } else { return CGPoint(x: positionAtIndex(index, step), y: CGFloat(valueAtIndex(index)) * size.height) } } for index in 1 ..< count - 2 { let point0 = pointAtIndex(index - 1) let point1 = pointAtIndex(index) let point2 = pointAtIndex(index + 1) let point3 = pointAtIndex(index + 2) for j in 1 ..< granularity { let t = CGFloat(j) * (1.0 / CGFloat(granularity)) let tt = t * t let ttt = tt * t var point = CGPoint( x: 0.5 * (2 * point1.x + (point2.x - point0.x) * t + (2 * point0.x - 5 * point1.x + 4 * point2.x - point3.x) * tt + (3 * point1.x - point0.x - 3 * point2.x + point3.x) * ttt), y: 0.5 * (2 * point1.y + (point2.y - point0.y) * t + (2 * point0.y - 5 * point1.y + 4 * point2.y - point3.y) * tt + (3 * point1.y - point0.y - 3 * point2.y + point3.y) * ttt) ) point.y = max(0.0, min(size.height, point.y)) if point.x > point0.x { path.addLine(to: point) } if ((j - 1) % 2 == 0) { dataPoints.append(Float(point.y)) } } path.addLine(to: point2) } let lastValue = valueAtIndex(count - 1) path.addLine(to: CGPoint(x: size.width + 1.0, y: CGFloat(lastValue) * size.height)) if case .filled = type { path.addLine(to: CGPoint(x: size.width + 1.0, y: size.height)) path.close() } return (path, dataPoints) } public enum CodableToolValue { case float(EditorToolKey, Float) case tint(EditorToolKey, TintValue) case blur(EditorToolKey, BlurValue) case curves(EditorToolKey, CurvesValue) public init?(key: EditorToolKey, value: Any) { if let toolValue = value as? Float { self = .float(key, toolValue) } else if let toolValue = value as? TintValue { self = .tint(key, toolValue) } else if let toolValue = value as? BlurValue { self = .blur(key, toolValue) } else if let toolValue = value as? CurvesValue { self = .curves(key, toolValue) } else { return nil } } public var keyAndValue: (EditorToolKey, Any) { switch self { case let .float(key, value): return (key, value) case let .tint(key, value): return (key, value) case let .blur(key, value): return (key, value) case let .curves(key, value): return (key, value) } } } extension CodableToolValue: Codable { private enum CodingKeys: String, CodingKey { case key case type case value } private enum ToolType: Int, Codable { case float case tint case blur case curves } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(ToolType.self, forKey: .type) let key = EditorToolKey(rawValue: try container.decode(Int32.self, forKey: .key))! switch type { case .float: self = .float(key, try container.decode(Float.self, forKey: .value)) case .tint: self = .tint(key, try container.decode(TintValue.self, forKey: .value)) case .blur: self = .blur(key, try container.decode(BlurValue.self, forKey: .value)) case .curves: self = .curves(key, try container.decode(CurvesValue.self, forKey: .value)) } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case let .float(key, value): try container.encode(key.rawValue, forKey: .key) try container.encode(ToolType.float, forKey: .type) try container.encode(value, forKey: .value) case let .tint(key, value): try container.encode(key.rawValue, forKey: .key) try container.encode(ToolType.tint, forKey: .type) try container.encode(value, forKey: .value) case let .blur(key, value): try container.encode(key.rawValue, forKey: .key) try container.encode(ToolType.blur, forKey: .type) try container.encode(value, forKey: .value) case let .curves(key, value): try container.encode(key.rawValue, forKey: .key) try container.encode(ToolType.curves, forKey: .type) try container.encode(value, forKey: .value) } } } private let hasHEVCHardwareEncoder: Bool = { let spec: [CFString: Any] = [:] var outID: CFString? var properties: CFDictionary? let result = VTCopySupportedPropertyDictionaryForEncoder(width: 1920, height: 1080, codecType: kCMVideoCodecType_HEVC, encoderSpecification: spec as CFDictionary, encoderIDOut: &outID, supportedPropertiesOut: &properties) if result == kVTCouldNotFindVideoEncoderErr { return false } return result == noErr }() public func recommendedVideoExportConfiguration(values: MediaEditorValues, duration: Double, image: Bool = false, forceFullHd: Bool = false, frameRate: Float) -> MediaEditorVideoExport.Configuration { let compressionProperties: [String: Any] let codecType: AVVideoCodecType var bitrate: Int = 3700 if image { bitrate = 5000 } else { if duration < 10 { bitrate = 5800 } else if duration < 20 { bitrate = 5500 } else if duration < 30 { bitrate = 5000 } } if hasHEVCHardwareEncoder { codecType = AVVideoCodecType.hevc compressionProperties = [ AVVideoAverageBitRateKey: bitrate * 1000, AVVideoProfileLevelKey: kVTProfileLevel_HEVC_Main_AutoLevel ] } else { codecType = AVVideoCodecType.h264 compressionProperties = [ AVVideoAverageBitRateKey: bitrate * 1000, AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel, AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC ] } let width: Int let height: Int if values.videoIsFullHd { width = 1080 height = 1920 } else { width = 720 height = 1280 } let videoSettings: [String: Any] = [ AVVideoCodecKey: codecType, AVVideoCompressionPropertiesKey: compressionProperties, AVVideoWidthKey: width, AVVideoHeightKey: height ] let audioSettings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 44100, AVEncoderBitRateKey: 64000, AVNumberOfChannelsKey: 2 ] return MediaEditorVideoExport.Configuration( videoSettings: videoSettings, audioSettings: audioSettings, values: values, frameRate: frameRate ) }