mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
496 lines
15 KiB
Swift
496 lines
15 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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
public class MediaEditorValues {
|
|
let originalDimensions: PixelDimensions
|
|
let cropOffset: CGPoint
|
|
let cropSize: CGSize?
|
|
let cropScale: CGFloat
|
|
let cropRotation: CGFloat
|
|
let cropMirroring: Bool
|
|
|
|
let videoTrimRange: Range<Double>?
|
|
|
|
let drawing: UIImage?
|
|
let toolValues: [EditorToolKey: Any]
|
|
|
|
init(originalDimensions: PixelDimensions, cropOffset: CGPoint, cropSize: CGSize?, cropScale: CGFloat, cropRotation: CGFloat, cropMirroring: Bool, videoTrimRange: Range<Double>?, drawing: UIImage?, toolValues: [EditorToolKey: Any]) {
|
|
self.originalDimensions = originalDimensions
|
|
self.cropOffset = cropOffset
|
|
self.cropSize = cropSize
|
|
self.cropScale = cropScale
|
|
self.cropRotation = cropRotation
|
|
self.cropMirroring = cropMirroring
|
|
self.videoTrimRange = videoTrimRange
|
|
self.drawing = drawing
|
|
self.toolValues = 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, videoTrimRange: self.videoTrimRange, drawing: self.drawing, toolValues: toolValues)
|
|
}
|
|
}
|
|
|
|
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] = []
|
|
|
|
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]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.luminance = HistogramBins(values: [], max: 0)
|
|
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)
|
|
}
|