import Foundation import ForgeColor import ForgeGrade public enum ExportError: Error, Equatable { case malformed(String) } /// ASC CDL XML writers/parser (.cdl single, .ccc collection). urn:ASC:CDL:v1.01. public enum CDLExport { private static func f(_ v: Float) -> String { String(format: "%.6f", v) } private static func triple(_ v: SIMD3) -> String { "\(f(v.x)) \(f(v.y)) \(f(v.z))" } static func correctionBlock(_ cdl: CDL, id: String, indent: String) -> String { """ \(indent) \(indent) \(indent) \(triple(cdl.slope)) \(indent) \(triple(cdl.offset)) \(indent) \(triple(cdl.power)) \(indent) \(indent) \(indent) \(f(cdl.saturation)) \(indent) \(indent) """ } /// Single-correction .cdl file. public static func writeCDL(_ cdl: CDL, id: String) -> String { """ \(correctionBlock(cdl, id: id, indent: " ")) """ } /// Multi-clip .ccc collection. public static func writeCCC(_ entries: [(id: String, cdl: CDL)]) -> String { var out = """ """ for e in entries { out += correctionBlock(e.cdl, id: e.id, indent: " ") + "\n" } out += "\n" return out } /// Parse first ColorCorrection from .cdl/.ccc text. Regex-based scanner — /// tolerant of wrappers, strict on numeric content. public static func parseCDL(_ xml: String) throws -> CDL { func extract(_ tag: String) throws -> String { guard let open = xml.range(of: "<\(tag)>"), let close = xml.range(of: ""), open.upperBound <= close.lowerBound else { throw ExportError.malformed("missing <\(tag)>") } return String(xml[open.upperBound.. SIMD3 { let parts = text.split(separator: " ").compactMap { Float($0) } guard parts.count == 3 else { throw ExportError.malformed("bad triple in <\(tag)>") } return SIMD3(parts[0], parts[1], parts[2]) } let slope = try parseTriple(try extract("Slope"), tag: "Slope") let offset = try parseTriple(try extract("Offset"), tag: "Offset") let power = try parseTriple(try extract("Power"), tag: "Power") let satText = (try? extract("Saturation")) ?? "1.0" guard let sat = Float(satText) else { throw ExportError.malformed("bad Saturation") } return CDL(slope: slope, offset: offset, power: power, saturation: sat) } } /// Flattened .cube export from stored grade snapshots. public enum CubeExport { public static func fromSnapshot(_ snapshot: Data, latticeSize: Int, title: String) throws -> String { let stack = try GradeSnapshot.decode(snapshot) let look = Flattener.flatten(stack: stack, latticeSize: latticeSize, splitLeadingCDL: false) guard let lut = look.lut else { throw ExportError.malformed("flatten produced no LUT") } return CubeFile.write(lut, title: title) } }