mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Never crash: color, geom, raster, develop, scenes, preview, pipeline
- color: RgbSpace::to_xyz/from_xyz and bradford() no longer expect a
matrix inverse. Degenerate (collinear) primaries, which readers reject
before building a space from a file, leave the primaries unscaled /
give the identity; the constant Bradford and OKLab matrices fall back
to the identity, which can't happen (round trips are tested).
- pipeline geometry: sample() matches (mode, warp) instead of
expect("warp"); a warp plan without a warp (sample_plan never makes
one) samples through the equivalent affine.
- color, geom, raster, develop, scenes, preview and pipeline now #![deny]
unwrap/expect/panic/unimplemented/todo/unreachable; clippy.toml as in
the steering PR.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fb8e1e93aa
commit
fc13807f91
10 files changed
+37
-18
No files matched your search
@@ -0,0 +1,4 @@
|
||||
allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
allow-panic-in-tests = true
|
||||
allow-indexing-slicing-in-tests = true
|
||||
@@ -9,6 +9,7 @@
|
||||
//!
|
||||
//! The pipeline's working space is **linear Rec.2020, D65** ([`WORKING`]).
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod cct;
|
||||
pub mod perceptual;
|
||||
@@ -125,14 +126,17 @@ pub const WORKING: RgbSpace = REC2020;
|
||||
|
||||
impl RgbSpace {
|
||||
/// Matrix taking linear RGB in this space to XYZ (white Y = 1) relative to its own white.
|
||||
/// Degenerate (collinear) primaries, which readers reject before building a space from a
|
||||
/// file, leave the primaries unscaled instead of panicking.
|
||||
pub fn to_xyz(&self) -> Mat3 {
|
||||
let p = [self.r.to_xyz(), self.g.to_xyz(), self.b.to_xyz()];
|
||||
let m = Mat3([[p[0][0], p[1][0], p[2][0]], [p[0][1], p[1][1], p[2][1]], [p[0][2], p[1][2], p[2][2]]]);
|
||||
let s = m.inverse().expect("primaries are independent").apply(self.white.to_xyz());
|
||||
let s = m.inverse().map_or([1.0; 3], |inv| inv.apply(self.white.to_xyz()));
|
||||
m.mul(&Mat3::diag(s[0], s[1], s[2]))
|
||||
}
|
||||
/// Inverse of [`to_xyz`](Self::to_xyz); the identity for degenerate primaries.
|
||||
pub fn from_xyz(&self) -> Mat3 {
|
||||
self.to_xyz().inverse().expect("invertible")
|
||||
self.to_xyz().inverse().unwrap_or(Mat3::IDENTITY)
|
||||
}
|
||||
/// Luminance weights (the Y row of `to_xyz`).
|
||||
pub fn luma(&self) -> [f64; 3] {
|
||||
@@ -154,7 +158,8 @@ pub fn bradford(src: Xy, dst: Xy) -> Mat3 {
|
||||
}
|
||||
let s = BRADFORD.apply(src.to_xyz());
|
||||
let d = BRADFORD.apply(dst.to_xyz());
|
||||
let inv = BRADFORD.inverse().expect("invertible");
|
||||
// a constant, invertible matrix: the fallback (no adaptation) can't happen
|
||||
let Some(inv) = BRADFORD.inverse() else { return Mat3::IDENTITY };
|
||||
inv.mul(&Mat3::diag(d[0] / s[0], d[1] / s[1], d[2] / s[2])).mul(&BRADFORD)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,10 @@ fn mats() -> &'static OkMats {
|
||||
let to_lms = XYZ_TO_LMS.mul(&REC2020.to_xyz());
|
||||
OkMats {
|
||||
to_lms: to_lms.to_f32(),
|
||||
from_lms: to_lms.inverse().expect("invertible").to_f32(),
|
||||
// constant, invertible matrices (round trips tested below): the identity fallback can't happen
|
||||
from_lms: to_lms.inverse().unwrap_or(Mat3::IDENTITY).to_f32(),
|
||||
to_lab: LMS_TO_LAB.to_f32(),
|
||||
from_lab: LMS_TO_LAB.inverse().expect("invertible").to_f32(),
|
||||
from_lab: LMS_TO_LAB.inverse().unwrap_or(Mat3::IDENTITY).to_f32(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! The pipeline evaluates it; the catalog stores it; presets, copy/paste, sync, versions and history
|
||||
//! are operations on it. Numeric sliders are addressed by id through [`controls`].
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod controls;
|
||||
pub mod presets;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Conventions: image coordinates are y-down. *Normalized* coordinates map the full (uncropped,
|
||||
//! oriented) image to `0..1` on both axes, so tools and settings are resolution independent.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
mod crop;
|
||||
mod homography;
|
||||
|
||||
@@ -211,19 +211,22 @@ impl Frame {
|
||||
Some((nw, nh)) => std::borrow::Cow::Owned(resize(o, nw, nh, Filter::Mitchell)),
|
||||
None => std::borrow::Cow::Borrowed(o),
|
||||
};
|
||||
match plan.mode {
|
||||
SampleMode::Copy => base.into_owned(),
|
||||
SampleMode::Warp(o2t) => sample_warped(&base, plan.sx, plan.sy, self.warp.as_ref().expect("warp"), o2t, w, h),
|
||||
SampleMode::Affine(xf) => {
|
||||
let mut out = Rgb32f::new(w, h);
|
||||
par_rows(&mut out.data, w, |y, row| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let p = xf.apply(Point::new(x as f64 + 0.5, y as f64 + 0.5));
|
||||
*px = base.sample_bilinear(p.x as f32, p.y as f32);
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
let affine = |xf: Affine| {
|
||||
let mut out = Rgb32f::new(w, h);
|
||||
par_rows(&mut out.data, w, |y, row| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let p = xf.apply(Point::new(x as f64 + 0.5, y as f64 + 0.5));
|
||||
*px = base.sample_bilinear(p.x as f32, p.y as f32);
|
||||
}
|
||||
});
|
||||
out
|
||||
};
|
||||
match (plan.mode, self.warp.as_ref()) {
|
||||
(SampleMode::Copy, _) => base.into_owned(),
|
||||
(SampleMode::Warp(o2t), Some(warp)) => sample_warped(&base, plan.sx, plan.sy, warp, o2t, w, h),
|
||||
// `sample_plan` plans a warp only when there is one; without it the same mapping is affine
|
||||
(SampleMode::Warp(o2t), None) => affine(Affine::scale(plan.sx, plan.sy) * o2t),
|
||||
(SampleMode::Affine(xf), _) => affine(xf),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
//! [`render_cached`] each stage's output is reused while its inputs are unchanged ([`StageCache`]):
|
||||
//! dragging a tone, colour or exposure slider re-runs only the per-pixel stage.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod auto;
|
||||
pub mod colorops;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
//!
|
||||
//! No UI dependencies (L3): the egui frontend, the CLI and the MCP server share it.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod disk;
|
||||
pub mod hash;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! - Blur ([`blur`]): separable Gaussian via repeated box filters (O(1) per pixel in the radius).
|
||||
//! - [`Histogram`] of display-encoded RGB + luminance.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod blur;
|
||||
pub mod histogram;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! develop pipeline's highlight recovery, tone mapping and colour tools have something real to do.
|
||||
//! Every scene is deterministic for its seed. No external assets: zero licensing or privacy risk.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
mod noise;
|
||||
mod paint;
|
||||
|
||||
Reference in new issue
Block a user