import Foundation /// 3x3 RGB->RGB gamut conversion matrix (row-major rows). public struct GamutMatrix: Equatable, Sendable { public var rows: (SIMD3, SIMD3, SIMD3) public init(_ r0: SIMD3, _ r1: SIMD3, _ r2: SIMD3) { rows = (r0, r1, r2) } public static func == (l: GamutMatrix, r: GamutMatrix) -> Bool { l.rows.0 == r.rows.0 && l.rows.1 == r.rows.1 && l.rows.2 == r.rows.2 } public func apply(_ v: SIMD3) -> SIMD3 { SIMD3( (rows.0 * v).sum(), (rows.1 * v).sum(), (rows.2 * v).sum()) } public func inverted() -> GamutMatrix { let (a, b, c) = (rows.0.x, rows.0.y, rows.0.z) let (d, e, f) = (rows.1.x, rows.1.y, rows.1.z) let (g, h, i) = (rows.2.x, rows.2.y, rows.2.z) let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g) let inv = 1 / det return GamutMatrix( SIMD3((e * i - f * h) * inv, (c * h - b * i) * inv, (b * f - c * e) * inv), SIMD3((f * g - d * i) * inv, (a * i - c * g) * inv, (c * d - a * f) * inv), SIMD3((d * h - e * g) * inv, (b * g - a * h) * inv, (a * e - b * d) * inv)) } public static let identity = GamutMatrix(SIMD3(1, 0, 0), SIMD3(0, 1, 0), SIMD3(0, 0, 1)) /// ARRI Wide Gamut 4 -> Rec709 (D65). public static let awg4ToRec709 = GamutMatrix( SIMD3(1.7245, -0.6421, -0.0824), SIMD3(-0.1421, 1.1797, -0.0376), SIMD3(-0.0292, -0.3563, 1.3855)) /// REDWideGamutRGB -> Rec709 (D65). public static let rwgToRec709 = GamutMatrix( SIMD3(1.66225, -0.588228, -0.0740245), SIMD3(-0.124545, 1.13307, -0.00852952), SIMD3(-0.0433403, -0.253201, 1.29654)) /// Sony S-Gamut3.Cine -> Rec709 (D65). public static let sGamut3CineToRec709 = GamutMatrix( SIMD3(1.6410, -0.3245, -0.3165), SIMD3(-0.6698, 1.6175, 0.0522), SIMD3(0.0421, -0.1567, 1.1146)) } /// Camera color space: transfer curve + gamut, with conversion to linear Rec709 working space. public struct ColorSpace: Equatable, Sendable { public var name: String public var transfer: TransferFunction public var gamutToRec709: GamutMatrix public init(name: String, transfer: TransferFunction, gamutToRec709: GamutMatrix) { self.name = name self.transfer = transfer self.gamutToRec709 = gamutToRec709 } /// Camera-log-encoded RGB -> linear Rec709. public func toLinearRec709(_ encoded: SIMD3) -> SIMD3 { let lin = SIMD3(transfer.decode(encoded.x), transfer.decode(encoded.y), transfer.decode(encoded.z)) return gamutToRec709.apply(lin) } /// Linear Rec709 -> camera-log-encoded RGB. public func fromLinearRec709(_ linear709: SIMD3) -> SIMD3 { let lin = gamutToRec709.inverted().apply(linear709) return SIMD3(transfer.encode(lin.x), transfer.encode(lin.y), transfer.encode(lin.z)) } public static let arriLogC4AWG4 = ColorSpace(name: "ARRI LogC4/AWG4", transfer: .logC4, gamutToRec709: .awg4ToRec709) public static let redLog3G10RWG = ColorSpace(name: "RED Log3G10/RWG", transfer: .log3G10, gamutToRec709: .rwgToRec709) public static let sonySLog3SGamut3Cine = ColorSpace(name: "Sony S-Log3/SGamut3.Cine", transfer: .sLog3, gamutToRec709: .sGamut3CineToRec709) public static let rec709 = ColorSpace(name: "Rec709", transfer: .linear, gamutToRec709: .identity) }