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 } private let adjustmentToolsKeys: [EditorToolKey] = [ .enhance, .brightness, .contrast, .saturation, .warmth, .fade, .highlights, .shadows, .vignette, .grain, .sharpen ] 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.drawing !== rhs.drawing { return false } if lhs.entities != rhs.entities { return false } // if lhs.toolValues != rhs.toolValues { // return false // } 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 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 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, 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.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 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) 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, 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, 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, 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, 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, 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, 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, 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, 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.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 ) 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 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 } 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) -> (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 { return CGPoint(x: floorToScreenPixels(positionAtIndex(index, step)), y: floorToScreenPixels(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 ((index - 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, forceFullHd: Bool = false, frameRate: Float) -> MediaEditorVideoExport.Configuration { let compressionProperties: [String: Any] let codecType: AVVideoCodecType if hasHEVCHardwareEncoder { codecType = AVVideoCodecType.hevc compressionProperties = [ AVVideoAverageBitRateKey: 3800000, AVVideoProfileLevelKey: kVTProfileLevel_HEVC_Main_AutoLevel ] } else { codecType = AVVideoCodecType.h264 compressionProperties = [ AVVideoAverageBitRateKey: 3800000, 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 ) }