Files
lightcraft/crates/engine/src/cmd/lut_profiles.rs
T

167 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Imported profiles: `.cube` 3D LUTs used as creative profiles (Amount 0–200 % like the
//! built-in ones). A library on disk keeps a copy of each file in its `Profiles/` folder; the
//! list lives in the library's prefs and the LUTs are registered with the pipeline when the
//! library opens.
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use super::{CommandSpec, always, bad, cmd, str_param};
use crate::{Result, Session};
/// One imported LUT profile.
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct LutProfile {
/// `lut:<slug>`.
pub id: String,
pub name: String,
pub group: String,
/// The library's copy of the `.cube` file (empty without a library on disk).
pub file: String,
}
fn profiles_dir(s: &Session) -> Option<PathBuf> {
s.library.as_ref().filter(|l| l.on_disk).map(|l| l.dir.join("Profiles"))
}
/// Register every imported profile's LUT with the pipeline (missing files are skipped).
pub fn register_all(s: &Session) {
for p in &s.lut_profiles {
if let Some(l) = std::fs::read_to_string(&p.file).ok().and_then(|t| lightcraft_pipeline::lut::Lut3d::parse_cube(&t).ok()) {
lightcraft_pipeline::lut::register(&p.id, l);
}
}
}
/// `.cube` files in `paths` (folders searched, zip bundles opened), and the files that couldn't
/// be read with the reason (`[name, reason]`).
fn cube_files(paths: &[String]) -> (Vec<(String, String, Option<String>)>, Vec<Value>) {
// (name, text, group)
let mut out = Vec::new();
let mut failed = Vec::new();
let stem = |f: &Path| f.file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
for p in paths {
let path = Path::new(p);
if path.is_dir() {
let cube = |f: &Path| f.extension().is_some_and(|x| x.eq_ignore_ascii_case("cube"));
for ep in crate::walk::files_in(path, None, crate::walk::Limits::default(), cube).files {
match std::fs::read_to_string(&ep) {
Ok(t) => {
let group = ep.parent().and_then(|d| d.file_name()).map(|n| n.to_string_lossy().to_string());
out.push((stem(&ep), t, group));
}
Err(e) => failed.push(json!([stem(&ep), e.to_string()])),
}
}
} else if path.extension().is_some_and(|x| x.eq_ignore_ascii_case("zip")) {
let b = match std::fs::read(path) {
Ok(b) => b,
Err(e) => {
failed.push(json!([stem(path), e.to_string()]));
continue;
}
};
for (inner, data) in crate::preset_import::read_zip(&b).unwrap_or_default() {
if inner.to_ascii_lowercase().ends_with(".cube") {
let ip = Path::new(&inner);
let group = ip
.parent()
.and_then(|d| d.file_name())
.map(|n| n.to_string_lossy().to_string())
.or_else(|| path.file_stem().map(|n| n.to_string_lossy().to_string()));
out.push((
ip.file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(),
String::from_utf8_lossy(&data).to_string(),
group,
));
}
}
} else {
match std::fs::read_to_string(path) {
Ok(t) => out.push((stem(path), t, None)),
Err(e) => failed.push(json!([stem(path), e.to_string()])),
}
}
}
(out, failed)
}
fn import(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "profile.import";
let paths: Vec<String> =
p.get("paths").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default();
if paths.is_empty() {
return Err(bad(C, "no paths"));
}
let dir = profiles_dir(s);
if let Some(d) = &dir {
std::fs::create_dir_all(d).map_err(|e| bad(C, format!("{}: {e}", d.display())))?;
}
let (files, mut failed) = cube_files(&paths);
let mut imported = Vec::new();
for (name, text, group) in files {
let lut = match lightcraft_pipeline::lut::Lut3d::parse_cube(&text) {
Ok(l) => l,
Err(e) => {
failed.push(json!([name, e]));
continue;
}
};
let display = lut.title.clone().filter(|t| !t.trim().is_empty()).unwrap_or_else(|| name.clone());
let group = super::super::preset_import::group_from_dir(&group.unwrap_or_default()).unwrap_or_else(|| "Imported".into());
let slug = crate::presets::slug(&format!("{group}-{display}"));
let mut id = format!("lut:{slug}");
let mut n = 2;
while s.lut_profiles.iter().any(|x| x.id == id) {
id = format!("lut:{slug}-{n}");
n += 1;
}
let file = match &dir {
Some(d) => {
let f = d.join(format!("{}.cube", id.trim_start_matches("lut:")));
std::fs::write(&f, &text).map_err(|e| bad(C, format!("{}: {e}", f.display())))?;
f.to_string_lossy().to_string()
}
// no library on disk: the registered LUT is all this session needs (a copy would only
// be read back when a library opens, and that takes its own list from its prefs)
None => String::new(),
};
lightcraft_pipeline::lut::register(&id, lut);
s.lut_profiles.push(LutProfile { id: id.clone(), name: display.clone(), group: group.clone(), file });
imported.push(json!({"id": id, "name": display, "group": group}));
}
s.save_prefs()?;
// presets that name these profiles use them now
crate::profile_refs::refresh(s);
Ok(json!({"imported": imported, "failed": failed}))
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"profile.import",
"Import Profiles",
[],
None,
"{paths: [.cube file, folder or .zip]} — 3D LUTs become creative profiles (Amount 0–200 %; rendered on the CPU) in the profile browser's groups (folder / zip name, else Imported) → {imported: [{id, name, group}], failed}",
always,
import
),
cmd!("profile.deleteImported", "Delete Imported Profile", [], None, "{id: lut:…}", always, |s, p| {
let id = str_param(p, "id").ok_or_else(|| bad("profile.deleteImported", "missing id"))?.to_string();
let Some(i) = s.lut_profiles.iter().position(|x| x.id == id) else {
return Err(bad("profile.deleteImported", format!("no imported profile `{id}`")));
};
let gone = s.lut_profiles.remove(i);
if profiles_dir(s).is_some_and(|d| Path::new(&gone.file).starts_with(d)) {
let _ = std::fs::remove_file(&gone.file);
}
lightcraft_pipeline::lut::unregister(&id);
s.save_prefs()?;
crate::profile_refs::refresh(s);
Ok(json!({"deleted": id}))
}),
]
}