99 lines
3.7 KiB
Swift
99 lines
3.7 KiB
Swift
|
|
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)
|
||
|
|
}
|
||
|
|
}
|