- 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>
207 lines
5.8 KiB
Rust
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>,
|
|
}
|