Files
lightcraft/crates/catalog/src/model.rs
T
Brandon ThomasandClaude Opus 5.5 a0da99660d M4.2: optics — manual distortion/vignetting, lateral CA (auto + manual), defringe, DNG-embedded profile corrections
- pipeline::optics::Warp: one inverse map (lens-corrected → source) inside the single geometry resample:
  manual radial distortion, embedded DNG WarpRectilinear (per plane), per-channel lateral CA scale,
  radial vignetting gain (manual + embedded FixVignetteRadial); all in normalized radii
- automatic lateral CA estimate: radial edge profiles matched sub-pixel per plane, robust LSQ fit of
  displacement = α·r (cached per source)
- defringe: purple/green hue ranges (OkLab hue) desaturated next to high-contrast edges, width relative
  to the image long edge
- "Enable Lens Corrections" applies only lens data embedded in DNGs (opcodes are no longer baked at decode;
  stored on the photo record, on by default at import when present). No LCP data anywhere.
- new controls: optics.caRed/caBlue, defringe hue ranges; Optics panel: Manual sub-section, Defringe flyout
- tests: barrel grid straightened, CA removal (auto + manual), embedded corrections toggle/scale, preview vs
  export resolution independence, CA estimator accuracy, orientation of embedded lens data

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:57:47 -04:00

207 lines
5.8 KiB
Rust

//! Catalog records.
use std::sync::Arc;
use lightcraft_develop::DevelopSettings;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PhotoId(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AlbumId(pub u64);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MediaKind {
#[default]
Image,
Raw,
Video,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Flag {
#[default]
None,
Pick,
Reject,
}
impl Flag {
pub fn parse(s: &str) -> Option<Flag> {
match s.to_ascii_lowercase().as_str() {
"pick" | "picked" | "flagged" => Some(Flag::Pick),
"reject" | "rejected" => Some(Flag::Reject),
"none" | "unflagged" | "" => Some(Flag::None),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ColorLabel {
Red,
Yellow,
Green,
Blue,
Purple,
}
impl ColorLabel {
pub const ALL: [ColorLabel; 5] = [ColorLabel::Red, ColorLabel::Yellow, ColorLabel::Green, ColorLabel::Blue, ColorLabel::Purple];
pub fn parse(s: &str) -> Option<ColorLabel> {
ColorLabel::ALL.into_iter().find(|c| format!("{c:?}").eq_ignore_ascii_case(s))
}
}
/// Where the pixels come from.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Source {
/// A file on disk (native) or in the browser's storage (web).
File { path: String },
/// A procedurally generated demo scene (by `lightcraft-scenes` id).
Demo { scene: u32 },
}
/// Descriptive + capture metadata.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Meta {
pub camera: String,
pub lens: String,
pub focal_mm: Option<f32>,
pub aperture: Option<f32>,
pub shutter: String,
pub iso: Option<u32>,
pub location: String,
pub gps: Option<(f64, f64)>,
pub title: String,
pub caption: String,
pub copyright: String,
pub creator: String,
pub keywords: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Version {
pub name: String,
pub created: String,
pub settings: Arc<DevelopSettings>,
#[serde(default)]
pub auto: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct HistoryStep {
pub label: String,
pub settings: Arc<DevelopSettings>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Photo {
pub id: PhotoId,
pub source: Source,
pub file_name: String,
#[serde(default)]
pub kind: MediaKind,
pub format: String,
pub width: u32,
pub height: u32,
#[serde(default)]
pub file_size: u64,
/// ISO 8601 local capture time.
#[serde(default)]
pub captured: Option<String>,
pub imported: String,
#[serde(default)]
pub meta: Meta,
#[serde(default)]
pub rating: u8,
#[serde(default)]
pub flag: Flag,
#[serde(default)]
pub label: Option<ColorLabel>,
pub develop: Arc<DevelopSettings>,
#[serde(default)]
pub edited: Option<String>,
#[serde(default)]
pub versions: Vec<Version>,
#[serde(default)]
pub history: Vec<HistoryStep>,
/// In "Recently Deleted".
#[serde(default)]
pub deleted: bool,
/// Video duration in seconds.
#[serde(default)]
pub duration: Option<f64>,
/// Raw files: the camera's as-shot white balance (Kelvin, tint).
#[serde(default)]
pub as_shot_wb: Option<(f64, f64)>,
/// Hash of the file's bytes (hex), for duplicate detection and preview-cache keys.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
/// Lens corrections embedded in the file (DNG `WarpRectilinear` / `FixVignetteRadial`), applied when
/// "Enable Profile Corrections" is on.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedded_lens: Option<lightcraft_develop::EmbeddedLens>,
}
impl Photo {
pub fn new(id: PhotoId, source: Source, file_name: &str, format: &str, width: u32, height: u32, imported: &str) -> Photo {
Photo {
id,
source,
file_name: file_name.to_string(),
kind: MediaKind::Image,
format: format.to_string(),
width,
height,
file_size: 0,
captured: None,
imported: imported.to_string(),
meta: Meta::default(),
rating: 0,
flag: Flag::None,
label: None,
develop: Arc::new(DevelopSettings::default()),
edited: None,
versions: Vec::new(),
history: Vec::new(),
deleted: false,
duration: None,
as_shot_wb: None,
content_hash: None,
embedded_lens: None,
}
}
pub fn is_edited(&self) -> bool {
!self.develop.is_unedited()
}
/// Capture time if known, else import time (sort key).
pub fn date(&self) -> &str {
self.captured.as_deref().unwrap_or(&self.imported)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Album {
pub id: AlbumId,
pub name: String,
/// Containing folder (an album with `folder = true`).
#[serde(default)]
pub parent: Option<AlbumId>,
/// Folders hold albums, not photos.
#[serde(default)]
pub folder: bool,
#[serde(default)]
pub photos: Vec<PhotoId>,
#[serde(default)]
pub cover: Option<PhotoId>,
}