1070 lines
37 KiB
Swift

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<Double>?
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<Double>?,
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<Double>.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<Double>) -> 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
)
}