rainbow-dragon/Sources/ForgeExport/ShotReport.swift

118 lines
5.2 KiB
Swift

import Foundation
import ForgeColor
import ForgeGrade
import ForgeLibrary
/// Producer/post-facing HTML shot report Reel design tokens, still thumbnails
/// (base64-embedded so the file is self-contained/mailable), grade + metadata per shot.
public enum ShotReport {
static func escapeHTML(_ s: String) -> String {
s.replacingOccurrences(of: "&", with: "&")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
}
static func f3(_ v: Float) -> String {
String(format: "%.3f", v)
}
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 stillDataURI(_ shot: Shot) -> String? {
guard let path = shot.stillPath,
let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil }
let mime = path.hasSuffix(".png") ? "image/png" : "image/x-portable-pixmap"
return "data:\(mime);base64,\(data.base64EncodedString())"
}
public static func html(shots: [Shot], projectName: String, dayLabel: String, date: String) -> String {
var rows = ""
for s in shots {
let c = cdl(from: s)
let still: String
if let uri = stillDataURI(s) {
still = "<img class=\"still\" src=\"\(uri)\" alt=\"\(escapeHTML(s.clipName))\">"
} else {
still = "<div class=\"still no-still\">No still</div>"
}
let gradeBlock: String
if let c {
gradeBlock = """
<div class="mono grade">
<span>S \(f3(c.slope.x)) \(f3(c.slope.y)) \(f3(c.slope.z))</span>
<span>O \(f3(c.offset.x)) \(f3(c.offset.y)) \(f3(c.offset.z))</span>
<span>P \(f3(c.power.x)) \(f3(c.power.y)) \(f3(c.power.z))</span>
<span>Sat \(f3(c.saturation))</span>
</div>
"""
} else {
gradeBlock = "<div class=\"grade none\">No grade</div>"
}
rows += """
<div class="shot">
\(still)
<div class="info">
<div class="clip mono">\(escapeHTML(s.clipName))</div>
<div class="sub">\(escapeHTML(s.cameraSlot)) · <span class="mono">\(s.timecodeIn ?? "")\(s.timecodeOut ?? "")</span></div>
<div class="sub mono">EI \(s.exposureIndex.map(String.init) ?? "") · \(s.whiteBalance.map { "\($0)K" } ?? "") · tint \(s.tint.map(String.init) ?? "") · \(s.fps.map { String(format: "%g", $0) } ?? "") fps</div>
</div>
\(gradeBlock)
</div>
"""
}
return """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>\(escapeHTML(projectName))\(escapeHTML(dayLabel)) Shot Report</title>
<style>
:root {
--bg-0: #08090C; --bg-1: #0C0E12; --bg-2: #111318;
--text-1: #F6F4EF; --text-2: #B4B0A6; --text-3: #86837B; --text-4: #63615B;
--accent: #F0A63C; --accent-text: #F6C583;
--hairline: rgba(255,255,255,0.07);
--mono: "SF Mono", ui-monospace, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg-0); color: var(--text-1);
font-family: -apple-system, "Segoe UI", sans-serif; font-size: 13px;
padding: 40px; max-width: 900px; margin: 0 auto; }
.mono { font-family: var(--mono); }
h1 { font-size: 20px; font-weight: 600; }
h1 span { color: var(--accent-text); }
.meta { color: var(--text-3); margin: 6px 0 26px; }
.shot { display: grid; grid-template-columns: 180px 1fr 300px; gap: 18px;
align-items: center; padding: 14px 0; border-bottom: 1px solid var(--hairline); }
.still { width: 180px; height: 101px; border-radius: 5px; object-fit: cover;
background: var(--bg-2); border: 1px solid var(--hairline); }
.no-still { display: flex; align-items: center; justify-content: center;
color: var(--text-4); font-size: 11px; }
.clip { font-size: 14px; font-weight: 600; }
.sub { color: var(--text-3); font-size: 11.5px; margin-top: 3px; }
.grade { display: flex; flex-direction: column; gap: 2px; font-size: 11px;
color: var(--text-2); }
.grade.none { color: var(--text-4); }
.foot { margin-top: 26px; color: var(--text-4); font-size: 11px; }
</style>
</head>
<body>
<h1>\(escapeHTML(projectName)) — <span>\(escapeHTML(dayLabel))</span></h1>
<div class="meta">\(escapeHTML(date)) · \(shots.count) shots · generated by Forge</div>
\(rows)
<div class="foot">All grades captured live at record start. CDL values in ASC order (slope / offset / power / saturation).</div>
</body>
</html>
"""
}
}