2023-05-15 15:12:51 +04:00

626 lines
20 KiB
Swift

import Foundation
import UIKit
import Display
import TelegramCore
public enum EditorToolKey {
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 class MediaEditorValues {
public let originalDimensions: PixelDimensions
public let cropOffset: CGPoint
public let cropSize: CGSize?
public let cropScale: CGFloat
public let cropRotation: CGFloat
public let cropMirroring: Bool
public let gradientColors: [UIColor]?
public let videoTrimRange: Range<Double>?
public let videoIsMuted: Bool
public let drawing: UIImage?
public let entities: [CodableDrawingEntity]
public let toolValues: [EditorToolKey: Any]
init(
originalDimensions: PixelDimensions,
cropOffset: CGPoint,
cropSize: CGSize?,
cropScale: CGFloat,
cropRotation: CGFloat,
cropMirroring: Bool,
gradientColors: [UIColor]?,
videoTrimRange: Range<Double>?,
videoIsMuted: Bool,
drawing: UIImage?,
entities: [CodableDrawingEntity],
toolValues: [EditorToolKey: Any]
) {
self.originalDimensions = originalDimensions
self.cropOffset = cropOffset
self.cropSize = cropSize
self.cropScale = cropScale
self.cropRotation = cropRotation
self.cropMirroring = cropMirroring
self.gradientColors = gradientColors
self.videoTrimRange = videoTrimRange
self.videoIsMuted = videoIsMuted
self.drawing = drawing
self.entities = entities
self.toolValues = toolValues
}
func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues {
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
}
func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues {
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
}
func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues {
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
}
func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues {
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: drawing, entities: entities, toolValues: self.toolValues)
}
func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues {
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: toolValues)
}
}
public struct TintValue: Equatable {
public static let initial = TintValue(
color: .clear,
intensity: 0.5
)
public let color: UIColor
public let intensity: Float
public init(
color: UIColor,
intensity: Float
) {
self.color = color
self.intensity = 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 {
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: 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 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 {
public struct CurveValue: Equatable {
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 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 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 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)
}