import Foundation import ForgeColor import ForgeGrade import ForgeLibrary /// Post-handoff writers: ALE, CSV, CMX3600 EDL with ASC CDL comments. public enum PostExport { private static func f6(_ v: Float) -> String { String(format: "%.6f", v) } /// First CDL node from a shot's grade snapshot, if any. static func cdl(from shot: Shot) -> CDL? { guard let data = shot.gradeSnapshot, let stack = try? GradeSnapshot.decode(data) else { return nil } for node in stack.nodes { if case .cdl(let c) = node.kind, node.isEnabled { return c } } return nil } static func ascSOP(_ c: CDL) -> String { "(\(f6(c.slope.x)) \(f6(c.slope.y)) \(f6(c.slope.z)))" + "(\(f6(c.offset.x)) \(f6(c.offset.y)) \(f6(c.offset.z)))" + "(\(f6(c.power.x)) \(f6(c.power.y)) \(f6(c.power.z)))" } // MARK: ALE public static func writeALE(shots: [Shot], fps: Int) -> String { var out = """ Heading FIELD_DELIM\tTABS VIDEO_FORMAT\t1080 FPS\t\(fps) Column Name\tStart\tEnd\tCamera\tASC_SOP\tASC_SAT\tEI\tWB\tTint\tFPS Data """ for s in shots { let c = cdl(from: s) var row = [String]() row.append(s.clipName) row.append(s.timecodeIn ?? "") row.append(s.timecodeOut ?? "") row.append(s.cameraSlot) row.append(c.map(ascSOP) ?? "") row.append(c.map { f6($0.saturation) } ?? "") row.append(s.exposureIndex.map(String.init) ?? "") row.append(s.whiteBalance.map(String.init) ?? "") row.append(s.tint.map(String.init) ?? "") row.append(s.fps.map { String(format: "%g", $0) } ?? "") out += row.joined(separator: "\t") + "\n" } return out } // MARK: CSV private static func csvEscape(_ s: String) -> String { if s.contains(",") || s.contains("\"") || s.contains("\n") { return "\"" + s.replacingOccurrences(of: "\"", with: "\"\"") + "\"" } return s } public static func writeCSV(shots: [Shot]) -> String { var out = "clip_name,camera_slot,tc_in,tc_out,ei,wb,tint,fps,asc_sop,asc_sat\n" for s in shots { let c = cdl(from: s) var row = [String]() row.append(s.clipName) row.append(s.cameraSlot) row.append(s.timecodeIn ?? "") row.append(s.timecodeOut ?? "") row.append(s.exposureIndex.map(String.init) ?? "") row.append(s.whiteBalance.map(String.init) ?? "") row.append(s.tint.map(String.init) ?? "") row.append(s.fps.map { String(format: "%g", $0) } ?? "") row.append(c.map(ascSOP) ?? "") row.append(c.map { f6($0.saturation) } ?? "") out += row.map(csvEscape).joined(separator: ",") + "\n" } return out } // MARK: EDL (CMX3600 + CDL comments) public static func writeEDL(shots: [Shot], title: String, fps: Int) -> String { var out = "TITLE: \(title)\nFCM: NON-DROP FRAME\n\n" var recordTC = Timecode.parse("01:00:00:00", fps: fps)! var event = 1 for s in shots { guard let srcIn = s.timecodeIn.flatMap({ Timecode.parse($0, fps: fps) }), let srcOut = s.timecodeOut.flatMap({ Timecode.parse($0, fps: fps) }), srcOut.frames > srcIn.frames else { continue } let duration = srcOut - srcIn let recIn = recordTC let recOut = recordTC + duration // Reel column: clip name truncated to CMX-friendly 8+ (modern EDLs allow long names). out += String(format: "%03d %@ V C %@ %@ %@ %@\n", event, s.clipName, srcIn.description, srcOut.description, recIn.description, recOut.description) out += "* FROM CLIP NAME: \(s.clipName)\n" if let c = cdl(from: s) { out += "* ASC_SOP \(ascSOP(c))\n" out += "* ASC_SAT \(f6(c.saturation))\n" } out += "\n" recordTC = recOut event += 1 } return out } }