rainbow-dragon/Sources/ForgeExport/CDLExport.swift

99 lines
3.7 KiB
Swift
Raw Normal View History

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<Float>) -> String {
"\(f(v.x)) \(f(v.y)) \(f(v.z))"
}
static func correctionBlock(_ cdl: CDL, id: String, indent: String) -> String {
"""
\(indent)<ColorCorrection id="\(id)">
\(indent) <SOPNode>
\(indent) <Slope>\(triple(cdl.slope))</Slope>
\(indent) <Offset>\(triple(cdl.offset))</Offset>
\(indent) <Power>\(triple(cdl.power))</Power>
\(indent) </SOPNode>
\(indent) <SatNode>
\(indent) <Saturation>\(f(cdl.saturation))</Saturation>
\(indent) </SatNode>
\(indent)</ColorCorrection>
"""
}
/// Single-correction .cdl file.
public static func writeCDL(_ cdl: CDL, id: String) -> String {
"""
<?xml version="1.0" encoding="UTF-8"?>
<ColorDecisionList xmlns="urn:ASC:CDL:v1.01">
<ColorDecision>
\(correctionBlock(cdl, id: id, indent: " "))
</ColorDecision>
</ColorDecisionList>
"""
}
/// Multi-clip .ccc collection.
public static func writeCCC(_ entries: [(id: String, cdl: CDL)]) -> String {
var out = """
<?xml version="1.0" encoding="UTF-8"?>
<ColorCorrectionCollection xmlns="urn:ASC:CDL:v1.01">
"""
for e in entries {
out += correctionBlock(e.cdl, id: e.id, indent: " ") + "\n"
}
out += "</ColorCorrectionCollection>\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: "</\(tag)>"),
open.upperBound <= close.lowerBound else {
throw ExportError.malformed("missing <\(tag)>")
}
return String(xml[open.upperBound..<close.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
}
func parseTriple(_ text: String, tag: String) throws -> SIMD3<Float> {
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)
}
}