rainbow-dragon/Sources/ForgeExport/PostExport.swift

125 lines
4.2 KiB
Swift
Raw Normal View History

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
}
}