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: "<") .replacingOccurrences(of: ">", with: ">") } 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 = "\"\(escapeHTML(s.clipName))\"" } else { still = "
No still
" } let gradeBlock: String if let c { gradeBlock = """
S \(f3(c.slope.x)) \(f3(c.slope.y)) \(f3(c.slope.z)) O \(f3(c.offset.x)) \(f3(c.offset.y)) \(f3(c.offset.z)) P \(f3(c.power.x)) \(f3(c.power.y)) \(f3(c.power.z)) Sat \(f3(c.saturation))
""" } else { gradeBlock = "
No grade
" } rows += """
\(still)
\(escapeHTML(s.clipName))
\(escapeHTML(s.cameraSlot)) · \(s.timecodeIn ?? "—") → \(s.timecodeOut ?? "—")
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
\(gradeBlock)
""" } return """ \(escapeHTML(projectName)) — \(escapeHTML(dayLabel)) Shot Report

\(escapeHTML(projectName)) — \(escapeHTML(dayLabel))

\(escapeHTML(date)) · \(shots.count) shots · generated by Forge
\(rows)
All grades captured live at record start. CDL values in ASC order (slope / offset / power / saturation).
""" } }