mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 05:28:25 -06:00
Merge branch 'worktree-agent-ad641003294616c82'
# Conflicts: # crates/engine/src/library.rs # crates/ui-egui/src/lib.rs # docs/parity.md
This commit is contained in:
@@ -128,7 +128,12 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
.iter()
|
||||
.filter_map(|id| s.develop_of(*id).map(|d| (*id, d)))
|
||||
.filter_map(|(id, d)| {
|
||||
let fresh = DevelopSettings { wb: lightcraft_develop::WhiteBalance { mode: WbMode::AsShot, ..d.wb }, ..Default::default() };
|
||||
// back to the photo's import defaults when a default preset gave it its look
|
||||
let look = s.catalog.photo(id).and_then(|p| p.import_look.clone());
|
||||
let fresh = match look {
|
||||
Some(l) => (*l).clone(),
|
||||
None => DevelopSettings { wb: lightcraft_develop::WhiteBalance { mode: WbMode::AsShot, ..d.wb }, ..Default::default() },
|
||||
};
|
||||
s.develop_op(id, fresh, "Reset")
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -11,6 +11,7 @@ mod library;
|
||||
mod masks;
|
||||
mod merge;
|
||||
mod organize;
|
||||
mod prefs;
|
||||
mod preset_files;
|
||||
mod query;
|
||||
mod xmp;
|
||||
@@ -111,6 +112,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(query::specs());
|
||||
v.extend(xmp::specs());
|
||||
v.extend(preset_files::specs());
|
||||
v.extend(prefs::specs());
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Library preferences (Settings → Import / Performance): develop defaults applied on import and
|
||||
//! the thumbnail cache budget. Saved in the library's `prefs.json` (see [`crate::library`]).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, cmd};
|
||||
use crate::import::{CameraDefault, ImportDefaults};
|
||||
use crate::{Result, Session};
|
||||
|
||||
const ID: &str = "library.preferences";
|
||||
|
||||
fn current(s: &Session) -> Value {
|
||||
json!({
|
||||
"xmp": s.xmp,
|
||||
"import": s.import_defaults,
|
||||
"cacheMb": if s.cache_mb == 0 { s.cache_bytes() >> 20 } else { u64::from(s.cache_mb) },
|
||||
"persistent": s.library.is_some(),
|
||||
})
|
||||
}
|
||||
|
||||
/// `"default"`, `""` or null = no preset; anything else must name an existing preset.
|
||||
fn preset_ref(s: &Session, v: &Value) -> Result<Option<String>> {
|
||||
match v {
|
||||
Value::Null => Ok(None),
|
||||
Value::String(x) if x.is_empty() || x == "default" => Ok(None),
|
||||
Value::String(x) if s.presets.iter().any(|p| p.id == *x) => Ok(Some(x.clone())),
|
||||
Value::String(x) => Err(bad(ID, format!("unknown preset `{x}` (see preset.list)"))),
|
||||
other => Err(bad(ID, format!("preset must be a preset id or \"default\", not {other}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn prefs(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut d: ImportDefaults = s.import_defaults.clone();
|
||||
let mut changed = false;
|
||||
if let Some(i) = p.get("import") {
|
||||
if let Some(v) = i.get("rawPreset") {
|
||||
d.raw_preset = preset_ref(s, v)?;
|
||||
}
|
||||
if let Some(v) = i.get("otherPreset") {
|
||||
d.other_preset = preset_ref(s, v)?;
|
||||
}
|
||||
if let Some(v) = i.get("perCamera").and_then(Value::as_bool) {
|
||||
d.per_camera = v;
|
||||
}
|
||||
if let Some(a) = i.get("cameras").and_then(Value::as_array) {
|
||||
d.cameras.clear();
|
||||
for c in a {
|
||||
let camera = c.get("camera").and_then(Value::as_str).unwrap_or_default().trim().to_string();
|
||||
if camera.is_empty() {
|
||||
return Err(bad(ID, "each camera default needs `camera` (make + model)"));
|
||||
}
|
||||
let preset = preset_ref(s, c.get("preset").unwrap_or(&Value::Null))?;
|
||||
d.cameras.push(CameraDefault { camera, preset });
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
// set or clear one camera's default: {camera: {camera: "Make Model", preset: id|null}}
|
||||
if let Some(c) = p.get("camera") {
|
||||
let camera = c.get("camera").and_then(Value::as_str).unwrap_or_default().trim().to_string();
|
||||
if camera.is_empty() {
|
||||
return Err(bad(ID, "`camera.camera` (make + model) is required"));
|
||||
}
|
||||
let preset = preset_ref(s, c.get("preset").unwrap_or(&Value::Null))?;
|
||||
d.cameras.retain(|x| !x.camera.eq_ignore_ascii_case(&camera));
|
||||
if c.get("remove").and_then(Value::as_bool) != Some(true) {
|
||||
d.cameras.push(CameraDefault { camera, preset });
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
s.import_defaults = d;
|
||||
if let Some(mb) = p.get("cacheMb") {
|
||||
let mb = mb.as_u64().ok_or_else(|| bad(ID, "cacheMb must be a number of megabytes"))?;
|
||||
s.set_cache_mb(mb.clamp(0, 1 << 20) as u32)?;
|
||||
} else if changed {
|
||||
s.save_prefs()?;
|
||||
}
|
||||
Ok(current(s))
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
"library.preferences",
|
||||
"Library Preferences",
|
||||
[],
|
||||
None,
|
||||
"{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}]}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, persistent}",
|
||||
always,
|
||||
prefs
|
||||
)]
|
||||
}
|
||||
@@ -16,7 +16,7 @@ use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use lightcraft_catalog::{Op, Photo, PhotoId, Source};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Session;
|
||||
use crate::media::ProbeInfo;
|
||||
@@ -59,14 +59,74 @@ pub struct ImportReport {
|
||||
|
||||
/// The develop settings a photo gets on import: raws start from their as-shot white balance with
|
||||
/// default sharpening / colour noise reduction, and file-embedded lens corrections on (as the
|
||||
/// camera intended).
|
||||
/// camera intended); a user default preset ([`ImportDefaults`]) goes on top.
|
||||
pub fn import_defaults(p: &Photo) -> lightcraft_develop::DevelopSettings {
|
||||
p.import_defaults()
|
||||
}
|
||||
|
||||
/// Does the photo still look as imported (its embedded camera preview is then a fair stand-in)?
|
||||
/// Does the photo still look as the camera rendered it (its embedded camera preview is then a
|
||||
/// fair stand-in)? Not when a default preset changed it on import.
|
||||
pub fn has_import_look(p: &Photo) -> bool {
|
||||
*p.develop == import_defaults(p)
|
||||
*p.develop == p.camera_defaults()
|
||||
}
|
||||
|
||||
/// User defaults applied on import (Settings → Import; saved in the library's `prefs.json`).
|
||||
/// Presets are referenced by id; a preset that no longer exists is ignored.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct ImportDefaults {
|
||||
/// Preset applied to raw files (`None` = the LightCraft default).
|
||||
pub raw_preset: Option<String>,
|
||||
/// Use a camera's own default (below) when the photo's camera has one.
|
||||
pub per_camera: bool,
|
||||
/// Per-camera raw defaults, keyed by the camera's make + model as shown in Info
|
||||
/// (`"Canon EOS R5"`).
|
||||
pub cameras: Vec<CameraDefault>,
|
||||
/// Preset applied to non-raw images (JPEG, PNG, TIFF, HEIC…; `None` = none).
|
||||
pub other_preset: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct CameraDefault {
|
||||
/// Make + model (`Meta::camera`).
|
||||
pub camera: String,
|
||||
/// Preset id; `None` = the LightCraft default for this camera.
|
||||
pub preset: Option<String>,
|
||||
}
|
||||
|
||||
impl ImportDefaults {
|
||||
/// The preset id that applies to a photo of this kind and camera, if any.
|
||||
pub fn preset_for(&self, raw: bool, camera: &str) -> Option<&str> {
|
||||
if !raw {
|
||||
return self.other_preset.as_deref();
|
||||
}
|
||||
if self.per_camera
|
||||
&& let Some(c) = self.cameras.iter().find(|c| !camera.is_empty() && c.camera.eq_ignore_ascii_case(camera))
|
||||
{
|
||||
return c.preset.as_deref();
|
||||
}
|
||||
self.raw_preset.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Give a freshly imported photo its default settings: the camera defaults, then the matching
|
||||
/// default preset ([`ImportDefaults::preset_for`]), remembered as the photo's import look.
|
||||
pub fn apply_import_defaults(s: &Session, p: &mut Photo) {
|
||||
let base = p.camera_defaults();
|
||||
let raw = p.kind == lightcraft_catalog::MediaKind::Raw;
|
||||
let preset = s.import_defaults.preset_for(raw, &p.meta.camera).and_then(|id| s.presets.iter().find(|x| x.id == id));
|
||||
match preset {
|
||||
Some(pr) => {
|
||||
let look = std::sync::Arc::new(pr.apply(&base, 1.0));
|
||||
p.develop = look.clone();
|
||||
p.import_look = (*look != base).then_some(look);
|
||||
}
|
||||
None => {
|
||||
p.develop = std::sync::Arc::new(base);
|
||||
p.import_look = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_supported(path: &Path) -> bool {
|
||||
@@ -237,7 +297,7 @@ pub fn import(s: &mut Session, paths: &[String], mode: ImportMode) -> crate::Res
|
||||
p.as_shot_wb = info.as_shot_wb;
|
||||
p.content_hash = info.content_hash;
|
||||
p.embedded_lens = info.embedded_lens;
|
||||
p.develop = std::sync::Arc::new(import_defaults(&p));
|
||||
apply_import_defaults(s, &mut p);
|
||||
let raw = p.kind == lightcraft_catalog::MediaKind::Raw;
|
||||
let packet = crate::sidecar::find_sidecar(&path, s.xmp.naming)
|
||||
.and_then(|f| std::fs::read_to_string(f).ok())
|
||||
|
||||
@@ -102,6 +102,10 @@ pub struct Session {
|
||||
pub xmp: sidecar::XmpPrefs,
|
||||
/// Parameters of the last export (`app.export` params, minus targets), persisted in prefs.json.
|
||||
pub last_export: Option<serde_json::Value>,
|
||||
/// Develop defaults applied on import (persisted in prefs.json).
|
||||
pub import_defaults: import::ImportDefaults,
|
||||
/// Disk budget of the library's thumbnail cache in MB (0 = default; persisted in prefs.json).
|
||||
pub cache_mb: u32,
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
@@ -137,6 +141,8 @@ impl Session {
|
||||
library: None,
|
||||
xmp: sidecar::XmpPrefs::default(),
|
||||
last_export: None,
|
||||
import_defaults: import::ImportDefaults::default(),
|
||||
cache_mb: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,4 +359,6 @@ mod tests_merge;
|
||||
#[cfg(test)]
|
||||
mod tests_organize;
|
||||
#[cfg(test)]
|
||||
mod tests_prefs;
|
||||
#[cfg(test)]
|
||||
mod tests_xmp;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! LightCraft Library/
|
||||
//! catalog.snap catalog.log (lightcraft-catalog journal)
|
||||
//! presets.json view.json (user presets + favourites; last source/filter/sort/selection)
|
||||
//! prefs.json (library preferences: XMP sidecars)
|
||||
//! prefs.json (library preferences: XMP sidecars, import defaults, cache size, last export)
|
||||
//! thumbs/ (rendered thumbnail cache, safe to delete)
|
||||
//! Originals/ (photos imported with "copy into library")
|
||||
//! ```
|
||||
@@ -99,11 +99,16 @@ impl Library {
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
struct PrefsFile {
|
||||
xmp: crate::sidecar::XmpPrefs,
|
||||
/// Parameters of the last export (for Export with Previous).
|
||||
#[serde(alias = "last_export")]
|
||||
last_export: Option<serde_json::Value>,
|
||||
/// Develop defaults for imported photos.
|
||||
import: crate::import::ImportDefaults,
|
||||
/// Thumbnail disk cache budget (MB, 0 = default).
|
||||
cache_mb: u32,
|
||||
}
|
||||
|
||||
fn presets_json(s: &Session) -> String {
|
||||
@@ -165,6 +170,8 @@ impl Session {
|
||||
let prefs = read_json::<PrefsFile>(files.as_mut(), "prefs.json").unwrap_or_default();
|
||||
self.xmp = prefs.xmp;
|
||||
self.last_export = prefs.last_export;
|
||||
self.import_defaults = prefs.import;
|
||||
self.cache_mb = prefs.cache_mb;
|
||||
// view state
|
||||
if let Some(v) = read_json::<ViewFile>(files.as_mut(), "view.json") {
|
||||
self.source = v.source;
|
||||
@@ -180,8 +187,10 @@ impl Session {
|
||||
self.selection = Selection::single(*first);
|
||||
}
|
||||
let presets_written = presets_json(self);
|
||||
// photo ids are per library: drop decoded sources of the previous one
|
||||
self.media.clear_sources();
|
||||
if on_disk {
|
||||
self.media.attach_disk_cache(&dir.join("thumbs"));
|
||||
self.media.attach_disk_cache(&dir.join("thumbs"), self.cache_bytes());
|
||||
}
|
||||
let view_written = self.view_json();
|
||||
self.library = Some(Library { dir, on_disk, journal, files, report, last_error: None, presets_written, view_written });
|
||||
@@ -262,11 +271,33 @@ impl Session {
|
||||
|
||||
/// Save the library preferences (no-op for in-memory sessions).
|
||||
pub fn save_prefs(&mut self) -> Result<()> {
|
||||
let v = serde_json::to_vec_pretty(&PrefsFile { xmp: self.xmp, last_export: self.last_export.clone() }).unwrap_or_default();
|
||||
let v = serde_json::to_vec_pretty(&PrefsFile {
|
||||
xmp: self.xmp,
|
||||
last_export: self.last_export.clone(),
|
||||
import: self.import_defaults.clone(),
|
||||
cache_mb: self.cache_mb,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let Some(lib) = self.library.as_mut() else { return Ok(()) };
|
||||
lib.files.write_atomic("prefs.json", &v).map_err(|e| EngineError::Other(format!("prefs: {e}")))
|
||||
}
|
||||
|
||||
/// The thumbnail disk cache budget in bytes ([`Session::cache_mb`], else the default).
|
||||
pub fn cache_bytes(&self) -> u64 {
|
||||
if self.cache_mb == 0 { crate::media::DISK_CACHE_BYTES } else { u64::from(self.cache_mb) << 20 }
|
||||
}
|
||||
|
||||
/// Change the thumbnail disk cache budget (MB, 0 = default): re-attaches the cache, which
|
||||
/// trims it to the new size. Saved with the library preferences.
|
||||
pub fn set_cache_mb(&mut self, mb: u32) -> Result<()> {
|
||||
self.cache_mb = mb;
|
||||
if let Some(lib) = self.library.as_ref().filter(|l| l.on_disk) {
|
||||
let dir = lib.thumbs_dir();
|
||||
self.media.attach_disk_cache(&dir, self.cache_bytes());
|
||||
}
|
||||
self.save_prefs()
|
||||
}
|
||||
|
||||
/// Compact the log into a snapshot now.
|
||||
pub fn compact_library(&mut self) -> Result<()> {
|
||||
self.persist()?;
|
||||
|
||||
@@ -143,8 +143,15 @@ fn source_bytes(img: &Rgb32f) -> usize {
|
||||
|
||||
impl MediaCache {
|
||||
/// Keep rendered thumbnails on disk in `dir` as well.
|
||||
pub fn attach_disk_cache(&mut self, dir: &std::path::Path) {
|
||||
self.rendered = Arc::new(PreviewCache::with_disk(rendered_budget(self.budget), dir, DISK_CACHE_BYTES));
|
||||
pub fn attach_disk_cache(&mut self, dir: &std::path::Path, disk_bytes: u64) {
|
||||
self.rendered = Arc::new(PreviewCache::with_disk(rendered_budget(self.budget), dir, disk_bytes));
|
||||
}
|
||||
|
||||
/// Forget every decoded source (photo ids changed meaning, e.g. another library was opened).
|
||||
pub fn clear_sources(&mut self) {
|
||||
self.thumbs = Lru::new(THUMB_SOURCE_BYTES.min(self.budget));
|
||||
self.previews.clear();
|
||||
self.full = None;
|
||||
}
|
||||
|
||||
/// Bytes the caches may hold together.
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//! Library preferences: import defaults (raw / other / per camera) applied by import, and their
|
||||
//! persistence in prefs.json.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use lightcraft_catalog::MediaKind;
|
||||
use lightcraft_develop::Preset;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn temp_dir(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("lc-prefs-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn user_preset(id: &str, settings: serde_json::Value) -> Preset {
|
||||
Preset { id: id.into(), name: id.into(), group: "User Presets".into(), settings, favorite: false, builtin: false }
|
||||
}
|
||||
|
||||
fn with_presets(s: &mut Session) {
|
||||
s.presets.push(user_preset("user.rawlook", json!({"light": {"exposure": 0.5}, "effects": {"clarity": 20.0}})));
|
||||
s.presets.push(user_preset("user.canonlook", json!({"light": {"contrast": 35.0}})));
|
||||
s.presets.push(user_preset("user.jpeglook", json!({"color": {"vibrance": 15.0}})));
|
||||
}
|
||||
|
||||
fn dng(dir: &Path, name: &str, make: &str, model: &str) {
|
||||
let meta = lightcraft_meta::Metadata { make: Some(make.into()), model: Some(model.into()), ..Default::default() };
|
||||
std::fs::write(dir.join(name), crate::tests_xmp::synthetic_dng_with(None, meta)).unwrap();
|
||||
}
|
||||
|
||||
fn png(path: &Path) {
|
||||
let (w, h) = (24usize, 16usize);
|
||||
let data: Vec<[u8; 4]> = (0..w * h).map(|i| [(i * 7) as u8, (i * 3) as u8, 90, 255]).collect();
|
||||
let img = lightcraft_raster::Rgba8 { width: w, height: h, data };
|
||||
let bytes = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
|
||||
std::fs::write(path, bytes).unwrap();
|
||||
}
|
||||
|
||||
fn photo_named<'a>(s: &'a Session, name: &str) -> &'a lightcraft_catalog::Photo {
|
||||
s.catalog.photos().find(|p| p.file_name == name).unwrap_or_else(|| panic!("{name} not imported"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_and_other_defaults_are_applied_on_import() {
|
||||
let src = temp_dir("apply");
|
||||
dng(&src, "nikon.dng", "Nikon", "Z 9");
|
||||
dng(&src, "canon.dng", "Canon", "EOS R5");
|
||||
png(&src.join("plain.png"));
|
||||
let mut s = Session::new().with_fs();
|
||||
with_presets(&mut s);
|
||||
let r = s
|
||||
.execute(
|
||||
"library.preferences",
|
||||
&json!({"import": {"rawPreset": "user.rawlook", "otherPreset": "user.jpeglook", "perCamera": true,
|
||||
"cameras": [{"camera": "Canon EOS R5", "preset": "user.canonlook"}]}}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(r["import"]["rawPreset"], "user.rawlook", "{r}");
|
||||
assert!(s.execute("library.preferences", &json!({"import": {"rawPreset": "nope"}})).is_err(), "unknown presets are rejected");
|
||||
|
||||
s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
|
||||
assert_eq!(s.catalog.len(), 3);
|
||||
let nikon = photo_named(&s, "nikon.dng");
|
||||
assert_eq!(nikon.kind, MediaKind::Raw);
|
||||
assert_eq!(nikon.meta.camera, "Nikon Z 9");
|
||||
assert_eq!((nikon.develop.light.exposure, nikon.develop.effects.clarity), (0.5, 20.0), "global raw default");
|
||||
assert_eq!(nikon.develop.detail.sharpen_amount, 40.0, "raw camera defaults stay underneath the preset");
|
||||
assert!(!nikon.is_edited(), "the default look counts as unedited");
|
||||
assert!(!crate::import::has_import_look(nikon), "a preset look isn't what the embedded preview shows");
|
||||
let canon = photo_named(&s, "canon.dng");
|
||||
assert_eq!((canon.develop.light.contrast, canon.develop.light.exposure), (35.0, 0.0), "per-camera default wins");
|
||||
let plain = photo_named(&s, "plain.png");
|
||||
assert_eq!((plain.develop.color.vibrance, plain.develop.light.exposure), (15.0, 0.0), "non-raw default");
|
||||
|
||||
// editing then Reset returns to the default look
|
||||
let id = nikon.id;
|
||||
s.execute("library.select", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("develop.set", &json!({"control": "light.exposure", "value": 2.0})).unwrap();
|
||||
assert!(s.catalog.photo(id).unwrap().is_edited());
|
||||
s.execute("develop.reset", &json!({})).unwrap();
|
||||
let p = s.catalog.photo(id).unwrap();
|
||||
assert_eq!(p.develop.light.exposure, 0.5);
|
||||
assert!(!p.is_edited());
|
||||
|
||||
// per-camera off: the Canon gets the global raw default; clearing it gives the plain defaults
|
||||
let mut s2 = Session::new().with_fs();
|
||||
with_presets(&mut s2);
|
||||
s2.execute("library.preferences", &json!({"camera": {"camera": "canon eos r5", "preset": "user.canonlook"}})).unwrap();
|
||||
s2.execute("library.import", &json!({"paths": [src.join("canon.dng").to_string_lossy()]})).unwrap();
|
||||
let c = s2.catalog.photos().next().unwrap();
|
||||
assert_eq!(c.develop.light.contrast, 0.0, "per-camera defaults are off unless enabled");
|
||||
assert!(c.import_look.is_none());
|
||||
let _ = std::fs::remove_dir_all(&src);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_persist_with_the_library() {
|
||||
let lib = temp_dir("lib");
|
||||
{
|
||||
let mut s = Session::new().with_fs();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
with_presets(&mut s);
|
||||
s.persist().unwrap(); // presets.json
|
||||
s.execute("library.xmpPreferences", &json!({"autoWrite": true})).unwrap();
|
||||
s.execute(
|
||||
"library.preferences",
|
||||
&json!({"import": {"rawPreset": "user.rawlook", "perCamera": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}),
|
||||
)
|
||||
.unwrap();
|
||||
s.close_library().unwrap();
|
||||
}
|
||||
let mut s = Session::new().with_fs();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
assert!(s.xmp.auto_write);
|
||||
assert_eq!(s.import_defaults.raw_preset.as_deref(), Some("user.rawlook"));
|
||||
assert!(s.import_defaults.per_camera);
|
||||
assert_eq!(s.import_defaults.cameras.len(), 1);
|
||||
assert_eq!(s.import_defaults.preset_for(true, "Canon EOS R5"), Some("user.canonlook"));
|
||||
assert_eq!(s.cache_mb, 512);
|
||||
assert_eq!(s.cache_bytes(), 512 << 20);
|
||||
let r = s.execute("library.preferences", &json!({})).unwrap();
|
||||
assert_eq!(r["cacheMb"], 512);
|
||||
assert_eq!(r["persistent"], true);
|
||||
// older prefs.json files (snake_case `last_export`, no import section) still load
|
||||
std::fs::write(lib.join("prefs.json"), r#"{"xmp": {"autoWrite": false}, "last_export": {"format": "png"}}"#).unwrap();
|
||||
let mut s = Session::new().with_fs();
|
||||
s.open_library(&lib, false).unwrap();
|
||||
assert_eq!(s.last_export, Some(json!({"format": "png"})));
|
||||
assert_eq!(s.import_defaults, Default::default());
|
||||
let _ = std::fs::remove_dir_all(&lib);
|
||||
}
|
||||
@@ -174,6 +174,11 @@ fn foreign_crs_sidecar_on_import_and_read_from_file() {
|
||||
}
|
||||
|
||||
fn synthetic_dng(xmp: &str) -> Vec<u8> {
|
||||
synthetic_dng_with(Some(xmp), lightcraft_meta::Metadata::default())
|
||||
}
|
||||
|
||||
/// A tiny Bayer DNG written by our own DNG writer (optional embedded XMP, camera metadata).
|
||||
pub(crate) fn synthetic_dng_with(xmp: Option<&str>, metadata: lightcraft_meta::Metadata) -> Vec<u8> {
|
||||
use lightcraft_raw::*;
|
||||
let (w, h) = (32usize, 24usize);
|
||||
let cfa = Cfa::bayer("RGGB").unwrap();
|
||||
@@ -203,9 +208,9 @@ fn synthetic_dng(xmp: &str) -> Vec<u8> {
|
||||
wb_multipliers: None,
|
||||
linearized: false,
|
||||
opcodes: OpcodeLists::default(),
|
||||
metadata: lightcraft_meta::Metadata::default(),
|
||||
metadata,
|
||||
};
|
||||
write_dng(&raw, &DngWriteOptions { xmp: Some(xmp.to_string()), ..Default::default() }).unwrap()
|
||||
write_dng(&raw, &DngWriteOptions { xmp: xmp.map(str::to_string), ..Default::default() }).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user