Projection chips redraw the preview; auto-crop as the DNG default crop

Picking a chip stored the choice for the merge and changed nothing on
screen — the chip did not even highlight, since the selected property
was never written back. Now the pick is reflected, and the job, waiting
for its decision, takes a Preview request, draws the alignment on the
chosen surface at proxy cost and reports again; the drain puts the new
picture and its size up. Auto is the surface the field of view suggests.

Also:
The largest rectangle inside the frames' coverage is found a row at a
time — a histogram of consecutive covered rows and a stack pass per row —
so the composite is never held to be measured (FR-MRG-11). It is written
as DefaultCropOrigin/DefaultCropSize (FR-MRG-4): the file opens on the
picture, the border is still in it, and resetting the crop shows it.
rawler reports the crop as the picture, which the test checks.

FR-MRG-4 records the question raised the same day — fill the border
rather than crop it — as open: a non-generative fill through the heal,
or a generative inpainter with its licence and weights. Neither decided.
This commit is contained in:
2026-09-19 15:24:20 +02:00
parent 30bd276d0b
commit 57ed51c1c5
7 changed files with 301 additions and 47 deletions
+67 -14
View File
@@ -60,7 +60,12 @@ pub struct DngProfile {
/// ///
/// `PhotometricInterpretation = LinearRaw`, `DNGVersion 1.4`, uncompressed, /// `PhotometricInterpretation = LinearRaw`, `DNGVersion 1.4`, uncompressed,
/// `Orientation = 1` — the composite is written upright (panorama.md §8). /// `Orientation = 1` — the composite is written upright (panorama.md §8).
pub fn write_linear_dng<W, F>( ///
/// `crop` is asked once every strip is in, and its answer — the largest
/// rectangle the frames covered, found while the strips went by
/// (`Inscribed`) — becomes `DefaultCropOrigin`/`DefaultCropSize`
/// (FR-MRG-4): the file opens on the picture, and the border is still in it.
pub fn write_linear_dng<W, F, C>(
out: W, out: W,
width: u32, width: u32,
height: u32, height: u32,
@@ -68,10 +73,12 @@ pub fn write_linear_dng<W, F>(
profile: &DngProfile, profile: &DngProfile,
source: Option<&SourceMetadata>, source: Option<&SourceMetadata>,
mut strips: F, mut strips: F,
crop: C,
) -> Result<(), ExportError> ) -> Result<(), ExportError>
where where
W: Write + Seek, W: Write + Seek,
F: FnMut(usize, &mut Vec<u16>) -> Result<(), ExportError>, F: FnMut(usize, &mut Vec<u16>) -> Result<(), ExportError>,
C: FnOnce() -> Option<crate::Rect>,
{ {
let enc = |e: tiff::TiffError| ExportError::Encode(e.to_string()); let enc = |e: tiff::TiffError| ExportError::Encode(e.to_string());
let mut encoder = TiffEncoder::new(out).map_err(enc)?; let mut encoder = TiffEncoder::new(out).map_err(enc)?;
@@ -99,6 +106,22 @@ where
} }
image.write_strip(&buf).map_err(enc)?; image.write_strip(&buf).map_err(enc)?;
} }
if let Some(r) = crop().filter(|r| r.width > 0 && r.height > 0) {
let r = crate::Rect {
x: r.x.min(width - 1),
y: r.y.min(height - 1),
width: r.width.min(width - r.x.min(width - 1)),
height: r.height.min(height - r.y.min(height - 1)),
};
image
.encoder()
.write_tag(Tag::Unknown(tag::DEFAULT_CROP_ORIGIN), &[r.x, r.y][..])
.map_err(enc)?;
image
.encoder()
.write_tag(Tag::Unknown(tag::DEFAULT_CROP_SIZE), &[r.width, r.height][..])
.map_err(enc)?;
}
image.finish().map_err(enc) image.finish().map_err(enc)
} }
@@ -154,6 +177,8 @@ mod tag {
pub const UNIQUE_CAMERA_MODEL: u16 = 50708; pub const UNIQUE_CAMERA_MODEL: u16 = 50708;
pub const BLACK_LEVEL: u16 = 50714; pub const BLACK_LEVEL: u16 = 50714;
pub const WHITE_LEVEL: u16 = 50717; pub const WHITE_LEVEL: u16 = 50717;
pub const DEFAULT_CROP_ORIGIN: u16 = 50719;
pub const DEFAULT_CROP_SIZE: u16 = 50720;
pub const COLOR_MATRIX_1: u16 = 50721; pub const COLOR_MATRIX_1: u16 = 50721;
pub const COLOR_MATRIX_2: u16 = 50722; pub const COLOR_MATRIX_2: u16 = 50722;
pub const AS_SHOT_NEUTRAL: u16 = 50728; pub const AS_SHOT_NEUTRAL: u16 = 50728;
@@ -205,16 +230,32 @@ mod tests {
model: Some("Canon EOS 6D".into()), model: Some("Canon EOS 6D".into()),
..Default::default() ..Default::default()
}; };
write_linear_dng(&mut bytes, width, height, rows, &profile(), Some(&source), |k, buf| { write_linear_dng(
let first = k as u32 * rows; &mut bytes,
let n = rows.min(height - first); width,
for y in first..first + n { height,
for x in 0..width { rows,
buf.extend([(x + y * width) as u16, 1000, 2000]); &profile(),
Some(&source),
|k, buf| {
let first = k as u32 * rows;
let n = rows.min(height - first);
for y in first..first + n {
for x in 0..width {
buf.extend([(x + y * width) as u16, 1000, 2000]);
}
} }
} Ok(())
Ok(()) },
}) || {
Some(crate::Rect {
x: 2,
y: 1,
width: 15,
height: 10,
})
},
)
.expect("written"); .expect("written");
bytes.into_inner() bytes.into_inner()
} }
@@ -242,15 +283,27 @@ mod tests {
// The profile came through as the camera's. // The profile came through as the camera's.
assert!(image.camera.color_matrix.len() >= 1); assert!(image.camera.color_matrix.len() >= 1);
assert_eq!(image.model, "Canon EOS 6D"); assert_eq!(image.model, "Canon EOS 6D");
// The default crop is what the decoder reports as the picture.
let crop = image.crop_area.expect("a crop");
assert_eq!((crop.p.x, crop.p.y, crop.d.w, crop.d.h), (2, 1, 15, 10));
} }
#[test] #[test]
fn a_strip_of_the_wrong_length_is_refused() { fn a_strip_of_the_wrong_length_is_refused() {
let mut bytes = std::io::Cursor::new(Vec::new()); let mut bytes = std::io::Cursor::new(Vec::new());
let err = write_linear_dng(&mut bytes, 8, 8, 8, &profile(), None, |_, buf| { let err = write_linear_dng(
buf.extend([0u16; 10]); &mut bytes,
Ok(()) 8,
}) 8,
8,
&profile(),
None,
|_, buf| {
buf.extend([0u16; 10]);
Ok(())
},
|| None,
)
.unwrap_err(); .unwrap_err();
assert!(matches!(err, ExportError::Encode(_))); assert!(matches!(err, ExportError::Encode(_)));
} }
+161
View File
@@ -0,0 +1,161 @@
//! TRACES: FR-MRG-4
//! The largest rectangle inside a coverage mask, found a row at a time.
//!
//! A merged panorama has ragged edges: the frames' footprints under a
//! cylinder or a sphere are not rectangles, and the composite carries a
//! black border where none of them reached. FR-MRG-4 asks for an auto-crop
//! to the largest inscribed rectangle. This finds it as the bands are
//! produced, so the composite is never held to be measured (FR-MRG-11):
//! each row extends a running histogram of consecutive covered rows above
//! it, and the largest rectangle ending on that row is the largest
//! rectangle under the histogram — a stack pass, linear in the width.
//!
//! The crop is written as the DNG's `DefaultCropOrigin`/`DefaultCropSize`,
//! which every reader honours and which discards nothing: the pixels
//! outside it are still in the file for a photographer who wants them.
/// The rectangle so far, in pixels from the top left.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Rect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl Rect {
pub fn area(&self) -> u64 {
u64::from(self.width) * u64::from(self.height)
}
}
/// Feed rows top to bottom; ask for the best at any point.
#[derive(Debug, Clone)]
pub struct Inscribed {
width: usize,
/// How many consecutive covered rows end at the last row fed, per column.
heights: Vec<u32>,
rows: u32,
best: Rect,
}
impl Inscribed {
pub fn new(width: u32) -> Self {
Inscribed {
width: width as usize,
heights: vec![0; width as usize],
rows: 0,
best: Rect::default(),
}
}
/// One more row of coverage, `width` long.
pub fn push_row(&mut self, covered: &[bool]) {
debug_assert_eq!(covered.len(), self.width);
for (h, &c) in self.heights.iter_mut().zip(covered) {
*h = if c { *h + 1 } else { 0 };
}
self.rows += 1;
// Largest rectangle under the histogram, with a sentinel column of
// height 0 at the end so every bar is popped.
let mut stack: Vec<usize> = Vec::new();
for i in 0..=self.width {
let h = if i < self.width { self.heights[i] } else { 0 };
while let Some(&top) = stack.last() {
if self.heights[top] <= h {
break;
}
stack.pop();
let height = self.heights[top];
let left = stack.last().map_or(0, |&l| l + 1);
let width = (i - left) as u32;
let area = u64::from(width) * u64::from(height);
if area > self.best.area() {
self.best = Rect {
x: left as u32,
y: self.rows - height,
width,
height,
};
}
}
stack.push(i);
}
}
/// Several rows at once, as a band hands them over.
pub fn push_rows(&mut self, covered: &[bool], rows: u32) {
for r in 0..rows as usize {
self.push_row(&covered[r * self.width..(r + 1) * self.width]);
}
}
pub fn best(&self) -> Rect {
self.best
}
}
#[cfg(test)]
mod tests {
use super::*;
fn from_art(art: &[&str]) -> Rect {
let mut ins = Inscribed::new(art[0].len() as u32);
for row in art {
let covered: Vec<bool> = row.chars().map(|c| c == '#').collect();
ins.push_row(&covered);
}
ins.best()
}
#[test]
fn a_full_mask_is_its_own_rectangle() {
let r = from_art(&["####", "####", "####"]);
assert_eq!(
r,
Rect {
x: 0,
y: 0,
width: 4,
height: 3
}
);
}
#[test]
fn ragged_edges_are_cut_off() {
// A cylinder's footprint: narrower at top and bottom.
let r = from_art(&[
"..####..",
".######.",
"########",
"########",
".######.",
"..####..",
]);
// 6 wide × 4 tall = 24 beats 8 × 2 = 16 and 4 × 6 = 24 ties; the
// first found wins a tie, which is the wider one here.
assert_eq!(r.area(), 24);
assert!(r.width == 6 && r.height == 4 || r.width == 4 && r.height == 6);
}
#[test]
fn a_hole_is_avoided() {
let r = from_art(&["#####", "##.##", "#####", "#####"]);
// Left of the hole: 2 × 4 = 8; right: 2 × 4 = 8; below: 5 × 2 = 10.
assert_eq!(
r,
Rect {
x: 0,
y: 2,
width: 5,
height: 2
}
);
}
#[test]
fn nothing_covered_is_nothing() {
assert_eq!(from_art(&["....", "...."]).area(), 0);
}
}
+2
View File
@@ -26,6 +26,7 @@ use dr_types::{ColourSpace, ExportFormat, ExportSettings};
mod dng; mod dng;
mod encode; mod encode;
mod inscribed;
mod error; mod error;
mod exif; mod exif;
pub mod icc; pub mod icc;
@@ -35,6 +36,7 @@ mod sharpen;
mod size; mod size;
pub use dng::{write_linear_dng, DngProfile}; pub use dng::{write_linear_dng, DngProfile};
pub use inscribed::{Inscribed, Rect};
pub use error::ExportError; pub use error::ExportError;
pub use metadata::SourceMetadata; pub use metadata::SourceMetadata;
pub use name::{resolve_name, NameContext}; pub use name::{resolve_name, NameContext};
+7
View File
@@ -1732,6 +1732,13 @@ field of view and overridable; the horizon levelled from the estimated rotations
drag; auto-crop to the largest inscribed rectangle, overridable. No boundary fill: painting pixels drag; auto-crop to the largest inscribed rectangle, overridable. No boundary fill: painting pixels
that were never captured is the pixel editing §1.3 excludes. that were never captured is the pixel editing §1.3 excludes.
*The auto-crop is non-destructive* (built 2026-09-19): it is the composite's default crop, not a
cut — the whole merge including its border is in the file, and resetting the crop shows it.
*Boundary fill is an open question, raised the same day:* a fill of the border from the picture's
own edge through FR-DEV-8's heal would be non-generative and honest about what it is; a generative
inpainter is a model licence, hundreds of megabytes of weights and an inference path the
application does not have. Neither is decided; the clause stands as written until one is.
**FR-MRG-5 — Honesty of failure.** *(general to any merge)* A frame that cannot be aligned is **FR-MRG-5 — Honesty of failure.** *(general to any merge)* A frame that cannot be aligned is
named, with why — too few matches, no overlap with any other frame, a residual above the stated named, with why — too few matches, no overlap with any other frame, a residual above the stated
bound — and the merge stops. Never a silent drop, never a best-effort composite with a frame bound — and the merge stops. Never a silent drop, never a best-effort composite with a frame
+8 -8
View File
@@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
| Metric | Value | | Metric | Value |
|---|---| |---|---|
| Source files scanned | 375 | | Source files scanned | 376 |
| TRACES tags found | 1537 | | TRACES tags found | 1538 |
| Requirements defined | 184 | | Requirements defined | 184 |
| Requirements deferred (post-v1) | 24 | | Requirements deferred (post-v1) | 24 |
| Requirements covered | 152 | | Requirements covered | 152 |
@@ -90,22 +90,22 @@ _None._
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:4105`](../ui/dr-ui/src/develop.rs#L4105), [`ui/dr-ui/src/develop.rs:804`](../ui/dr-ui/src/develop.rs#L804), [`ui/dr-ui/src/develop.rs:8641`](../ui/dr-ui/src/develop.rs#L8641), [`ui/dr-ui/src/develop.rs:8673`](../ui/dr-ui/src/develop.rs#L8673), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:2078`](../ui/dr-ui/src/lib.rs#L2078), [`ui/dr-ui/src/lib.rs:426`](../ui/dr-ui/src/lib.rs#L426), [`ui/dr-ui/ui/histogram.slint:184`](../ui/dr-ui/ui/histogram.slint#L184), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | | FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:4105`](../ui/dr-ui/src/develop.rs#L4105), [`ui/dr-ui/src/develop.rs:804`](../ui/dr-ui/src/develop.rs#L804), [`ui/dr-ui/src/develop.rs:8641`](../ui/dr-ui/src/develop.rs#L8641), [`ui/dr-ui/src/develop.rs:8673`](../ui/dr-ui/src/develop.rs#L8673), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:2078`](../ui/dr-ui/src/lib.rs#L2078), [`ui/dr-ui/src/lib.rs:426`](../ui/dr-ui/src/lib.rs#L426), [`ui/dr-ui/ui/histogram.slint:184`](../ui/dr-ui/ui/histogram.slint#L184), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |
| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:3997`](../ui/dr-ui/src/develop.rs#L3997), [`ui/dr-ui/src/develop.rs:4051`](../ui/dr-ui/src/develop.rs#L4051), [`ui/dr-ui/src/develop.rs:8967`](../ui/dr-ui/src/develop.rs#L8967), [`ui/dr-ui/src/develop.rs:9013`](../ui/dr-ui/src/develop.rs#L9013), [`ui/dr-ui/src/develop.rs:9032`](../ui/dr-ui/src/develop.rs#L9032), [`ui/dr-ui/src/develop.rs:957`](../ui/dr-ui/src/develop.rs#L957), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1880`](../ui/dr-ui/src/lib.rs#L1880), [`ui/dr-ui/src/lib.rs:1944`](../ui/dr-ui/src/lib.rs#L1944), [`ui/dr-ui/src/lib.rs:3634`](../ui/dr-ui/src/lib.rs#L3634), [`ui/dr-ui/src/lib.rs:3727`](../ui/dr-ui/src/lib.rs#L3727), [`ui/dr-ui/ui/app.slint:1939`](../ui/dr-ui/ui/app.slint#L1939), [`ui/dr-ui/ui/app.slint:62`](../ui/dr-ui/ui/app.slint#L62), [`ui/dr-ui/ui/settings.slint:155`](../ui/dr-ui/ui/settings.slint#L155), [`ui/dr-ui/ui/settings.slint:895`](../ui/dr-ui/ui/settings.slint#L895) | | FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:3997`](../ui/dr-ui/src/develop.rs#L3997), [`ui/dr-ui/src/develop.rs:4051`](../ui/dr-ui/src/develop.rs#L4051), [`ui/dr-ui/src/develop.rs:8967`](../ui/dr-ui/src/develop.rs#L8967), [`ui/dr-ui/src/develop.rs:9013`](../ui/dr-ui/src/develop.rs#L9013), [`ui/dr-ui/src/develop.rs:9032`](../ui/dr-ui/src/develop.rs#L9032), [`ui/dr-ui/src/develop.rs:957`](../ui/dr-ui/src/develop.rs#L957), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1880`](../ui/dr-ui/src/lib.rs#L1880), [`ui/dr-ui/src/lib.rs:1944`](../ui/dr-ui/src/lib.rs#L1944), [`ui/dr-ui/src/lib.rs:3634`](../ui/dr-ui/src/lib.rs#L3634), [`ui/dr-ui/src/lib.rs:3727`](../ui/dr-ui/src/lib.rs#L3727), [`ui/dr-ui/ui/app.slint:1939`](../ui/dr-ui/ui/app.slint#L1939), [`ui/dr-ui/ui/app.slint:62`](../ui/dr-ui/ui/app.slint#L62), [`ui/dr-ui/ui/settings.slint:155`](../ui/dr-ui/ui/settings.slint#L155), [`ui/dr-ui/ui/settings.slint:895`](../ui/dr-ui/ui/settings.slint#L895) |
| FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:155`](../core/dr-export/src/lib.rs#L155), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:55`](../core/dr-export/src/lib.rs#L55), [`core/dr-gpu/src/adjust.rs:2676`](../core/dr-gpu/src/adjust.rs#L2676), [`core/dr-pipeline/src/graph.rs:835`](../core/dr-pipeline/src/graph.rs#L835), [`core/dr-pipeline/src/graph.rs:933`](../core/dr-pipeline/src/graph.rs#L933), [`core/dr-pipeline/src/operation.rs:512`](../core/dr-pipeline/src/operation.rs#L512), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:934`](../core/dr-types/src/settings.rs#L934), [`ui/dr-ui/src/develop.rs:9032`](../ui/dr-ui/src/develop.rs#L9032), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:157`](../core/dr-export/src/lib.rs#L157), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:57`](../core/dr-export/src/lib.rs#L57), [`core/dr-gpu/src/adjust.rs:2676`](../core/dr-gpu/src/adjust.rs#L2676), [`core/dr-pipeline/src/graph.rs:835`](../core/dr-pipeline/src/graph.rs#L835), [`core/dr-pipeline/src/graph.rs:933`](../core/dr-pipeline/src/graph.rs#L933), [`core/dr-pipeline/src/operation.rs:512`](../core/dr-pipeline/src/operation.rs#L512), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:934`](../core/dr-types/src/settings.rs#L934), [`ui/dr-ui/src/develop.rs:9032`](../ui/dr-ui/src/develop.rs#L9032), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-3 | [`core/dr-export/src/lib.rs:184`](../core/dr-export/src/lib.rs#L184), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:136`](../core/dr-export/src/size.rs#L136), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-export/src/size.rs:84`](../core/dr-export/src/size.rs#L84), [`core/dr-types/src/settings.rs:1003`](../core/dr-types/src/settings.rs#L1003), [`core/dr-types/src/settings.rs:1072`](../core/dr-types/src/settings.rs#L1072), [`core/dr-types/src/settings.rs:1100`](../core/dr-types/src/settings.rs#L1100), [`core/dr-types/src/settings.rs:1111`](../core/dr-types/src/settings.rs#L1111), [`core/dr-types/src/settings.rs:1117`](../core/dr-types/src/settings.rs#L1117), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:979`](../core/dr-types/src/settings.rs#L979), [`core/dr-types/src/settings.rs:987`](../core/dr-types/src/settings.rs#L987), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:274`](../ui/dr-ui/src/settings_ui.rs#L274), [`ui/dr-ui/src/settings_ui.rs:287`](../ui/dr-ui/src/settings_ui.rs#L287), [`ui/dr-ui/src/settings_ui.rs:302`](../ui/dr-ui/src/settings_ui.rs#L302), [`ui/dr-ui/src/settings_ui.rs:701`](../ui/dr-ui/src/settings_ui.rs#L701), [`ui/dr-ui/ui/settings.slint:798`](../ui/dr-ui/ui/settings.slint#L798), [`ui/dr-ui/ui/settings.slint:808`](../ui/dr-ui/ui/settings.slint#L808) | | FR-EXP-3 | [`core/dr-export/src/lib.rs:186`](../core/dr-export/src/lib.rs#L186), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:136`](../core/dr-export/src/size.rs#L136), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-export/src/size.rs:84`](../core/dr-export/src/size.rs#L84), [`core/dr-types/src/settings.rs:1003`](../core/dr-types/src/settings.rs#L1003), [`core/dr-types/src/settings.rs:1072`](../core/dr-types/src/settings.rs#L1072), [`core/dr-types/src/settings.rs:1100`](../core/dr-types/src/settings.rs#L1100), [`core/dr-types/src/settings.rs:1111`](../core/dr-types/src/settings.rs#L1111), [`core/dr-types/src/settings.rs:1117`](../core/dr-types/src/settings.rs#L1117), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:979`](../core/dr-types/src/settings.rs#L979), [`core/dr-types/src/settings.rs:987`](../core/dr-types/src/settings.rs#L987), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:274`](../ui/dr-ui/src/settings_ui.rs#L274), [`ui/dr-ui/src/settings_ui.rs:287`](../ui/dr-ui/src/settings_ui.rs#L287), [`ui/dr-ui/src/settings_ui.rs:302`](../ui/dr-ui/src/settings_ui.rs#L302), [`ui/dr-ui/src/settings_ui.rs:701`](../ui/dr-ui/src/settings_ui.rs#L701), [`ui/dr-ui/ui/settings.slint:798`](../ui/dr-ui/ui/settings.slint#L798), [`ui/dr-ui/ui/settings.slint:808`](../ui/dr-ui/ui/settings.slint#L808) |
| FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
| FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:603`](../ui/dr-ui/src/lib.rs#L603), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:815`](../ui/dr-ui/src/settings_ui.rs#L815) | | FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:603`](../ui/dr-ui/src/lib.rs#L603), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:815`](../ui/dr-ui/src/settings_ui.rs#L815) |
| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1005`](../ui/dr-ui/src/export.rs#L1005), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:264`](../ui/dr-ui/src/lib.rs#L264), [`ui/dr-ui/src/lib.rs:2783`](../ui/dr-ui/src/lib.rs#L2783), [`ui/dr-ui/src/lib.rs:603`](../ui/dr-ui/src/lib.rs#L603), [`ui/dr-ui/src/lib.rs:649`](../ui/dr-ui/src/lib.rs#L649), [`ui/dr-ui/src/lib.rs:676`](../ui/dr-ui/src/lib.rs#L676), [`ui/dr-ui/src/library_ui.rs:4275`](../ui/dr-ui/src/library_ui.rs#L4275), [`ui/dr-ui/src/library_ui.rs:719`](../ui/dr-ui/src/library_ui.rs#L719), [`ui/dr-ui/src/library_ui.rs:8026`](../ui/dr-ui/src/library_ui.rs#L8026), [`ui/dr-ui/src/library_ui.rs:8103`](../ui/dr-ui/src/library_ui.rs#L8103), [`ui/dr-ui/src/library_ui.rs:8115`](../ui/dr-ui/src/library_ui.rs#L8115), [`ui/dr-ui/src/library_ui.rs:819`](../ui/dr-ui/src/library_ui.rs#L819), [`ui/dr-ui/src/library_ui.rs:876`](../ui/dr-ui/src/library_ui.rs#L876), [`ui/dr-ui/ui/app.slint:1116`](../ui/dr-ui/ui/app.slint#L1116), [`ui/dr-ui/ui/app.slint:1676`](../ui/dr-ui/ui/app.slint#L1676), [`ui/dr-ui/ui/library.slint:1583`](../ui/dr-ui/ui/library.slint#L1583), [`ui/dr-ui/ui/library.slint:4291`](../ui/dr-ui/ui/library.slint#L4291) | | FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1005`](../ui/dr-ui/src/export.rs#L1005), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:264`](../ui/dr-ui/src/lib.rs#L264), [`ui/dr-ui/src/lib.rs:2783`](../ui/dr-ui/src/lib.rs#L2783), [`ui/dr-ui/src/lib.rs:603`](../ui/dr-ui/src/lib.rs#L603), [`ui/dr-ui/src/lib.rs:649`](../ui/dr-ui/src/lib.rs#L649), [`ui/dr-ui/src/lib.rs:676`](../ui/dr-ui/src/lib.rs#L676), [`ui/dr-ui/src/library_ui.rs:4275`](../ui/dr-ui/src/library_ui.rs#L4275), [`ui/dr-ui/src/library_ui.rs:719`](../ui/dr-ui/src/library_ui.rs#L719), [`ui/dr-ui/src/library_ui.rs:8026`](../ui/dr-ui/src/library_ui.rs#L8026), [`ui/dr-ui/src/library_ui.rs:8103`](../ui/dr-ui/src/library_ui.rs#L8103), [`ui/dr-ui/src/library_ui.rs:8115`](../ui/dr-ui/src/library_ui.rs#L8115), [`ui/dr-ui/src/library_ui.rs:819`](../ui/dr-ui/src/library_ui.rs#L819), [`ui/dr-ui/src/library_ui.rs:876`](../ui/dr-ui/src/library_ui.rs#L876), [`ui/dr-ui/ui/app.slint:1116`](../ui/dr-ui/ui/app.slint#L1116), [`ui/dr-ui/ui/app.slint:1676`](../ui/dr-ui/ui/app.slint#L1676), [`ui/dr-ui/ui/library.slint:1583`](../ui/dr-ui/ui/library.slint#L1583), [`ui/dr-ui/ui/library.slint:4291`](../ui/dr-ui/ui/library.slint#L4291) |
| FR-EXP-8 | [`core/dr-decode/src/lib.rs:346`](../core/dr-decode/src/lib.rs#L346), [`core/dr-decode/src/lib.rs:370`](../core/dr-decode/src/lib.rs#L370), [`core/dr-decode/src/lib.rs:384`](../core/dr-decode/src/lib.rs#L384), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:138`](../core/dr-export/src/lib.rs#L138), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:655`](../core/dr-types/src/lib.rs#L655), [`core/dr-types/src/settings.rs:647`](../core/dr-types/src/settings.rs#L647), [`ui/dr-ui/src/develop.rs:4539`](../ui/dr-ui/src/develop.rs#L4539), [`ui/dr-ui/src/develop.rs:4651`](../ui/dr-ui/src/develop.rs#L4651), [`ui/dr-ui/src/develop.rs:729`](../ui/dr-ui/src/develop.rs#L729), [`ui/dr-ui/src/export.rs:474`](../ui/dr-ui/src/export.rs#L474), [`ui/dr-ui/src/export.rs:641`](../ui/dr-ui/src/export.rs#L641), [`ui/dr-ui/src/export.rs:687`](../ui/dr-ui/src/export.rs#L687), [`ui/dr-ui/src/export.rs:715`](../ui/dr-ui/src/export.rs#L715), [`ui/dr-ui/src/export.rs:839`](../ui/dr-ui/src/export.rs#L839), [`ui/dr-ui/src/export.rs:857`](../ui/dr-ui/src/export.rs#L857), [`ui/dr-ui/src/lib.rs:264`](../ui/dr-ui/src/lib.rs#L264), [`ui/dr-ui/src/lib.rs:287`](../ui/dr-ui/src/lib.rs#L287), [`ui/dr-ui/src/lib.rs:635`](../ui/dr-ui/src/lib.rs#L635), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-8 | [`core/dr-decode/src/lib.rs:346`](../core/dr-decode/src/lib.rs#L346), [`core/dr-decode/src/lib.rs:370`](../core/dr-decode/src/lib.rs#L370), [`core/dr-decode/src/lib.rs:384`](../core/dr-decode/src/lib.rs#L384), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:140`](../core/dr-export/src/lib.rs#L140), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:655`](../core/dr-types/src/lib.rs#L655), [`core/dr-types/src/settings.rs:647`](../core/dr-types/src/settings.rs#L647), [`ui/dr-ui/src/develop.rs:4539`](../ui/dr-ui/src/develop.rs#L4539), [`ui/dr-ui/src/develop.rs:4651`](../ui/dr-ui/src/develop.rs#L4651), [`ui/dr-ui/src/develop.rs:729`](../ui/dr-ui/src/develop.rs#L729), [`ui/dr-ui/src/export.rs:474`](../ui/dr-ui/src/export.rs#L474), [`ui/dr-ui/src/export.rs:641`](../ui/dr-ui/src/export.rs#L641), [`ui/dr-ui/src/export.rs:687`](../ui/dr-ui/src/export.rs#L687), [`ui/dr-ui/src/export.rs:715`](../ui/dr-ui/src/export.rs#L715), [`ui/dr-ui/src/export.rs:839`](../ui/dr-ui/src/export.rs#L839), [`ui/dr-ui/src/export.rs:857`](../ui/dr-ui/src/export.rs#L857), [`ui/dr-ui/src/lib.rs:264`](../ui/dr-ui/src/lib.rs#L264), [`ui/dr-ui/src/lib.rs:287`](../ui/dr-ui/src/lib.rs#L287), [`ui/dr-ui/src/lib.rs:635`](../ui/dr-ui/src/lib.rs#L635), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-9 | [`core/dr-decode/src/lib.rs:526`](../core/dr-decode/src/lib.rs#L526), [`core/dr-export/src/lib.rs:130`](../core/dr-export/src/lib.rs#L130), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1134`](../core/dr-gpu/src/adjust.rs#L1134), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:4672`](../ui/dr-ui/src/develop.rs#L4672), [`ui/dr-ui/src/develop.rs:4710`](../ui/dr-ui/src/develop.rs#L4710), [`ui/dr-ui/src/develop.rs:6878`](../ui/dr-ui/src/develop.rs#L6878), [`ui/dr-ui/src/lib.rs:603`](../ui/dr-ui/src/lib.rs#L603), [`ui/dr-ui/src/library.rs:4052`](../ui/dr-ui/src/library.rs#L4052), [`ui/dr-ui/src/library.rs:4522`](../ui/dr-ui/src/library.rs#L4522), [`ui/dr-ui/src/library.rs:4558`](../ui/dr-ui/src/library.rs#L4558), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | | FR-EXP-9 | [`core/dr-decode/src/lib.rs:526`](../core/dr-decode/src/lib.rs#L526), [`core/dr-export/src/lib.rs:132`](../core/dr-export/src/lib.rs#L132), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1134`](../core/dr-gpu/src/adjust.rs#L1134), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:4672`](../ui/dr-ui/src/develop.rs#L4672), [`ui/dr-ui/src/develop.rs:4710`](../ui/dr-ui/src/develop.rs#L4710), [`ui/dr-ui/src/develop.rs:6878`](../ui/dr-ui/src/develop.rs#L6878), [`ui/dr-ui/src/lib.rs:603`](../ui/dr-ui/src/lib.rs#L603), [`ui/dr-ui/src/library.rs:4052`](../ui/dr-ui/src/library.rs#L4052), [`ui/dr-ui/src/library.rs:4522`](../ui/dr-ui/src/library.rs#L4522), [`ui/dr-ui/src/library.rs:4558`](../ui/dr-ui/src/library.rs#L4558), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) |
| FR-MRG-1 | [`core/dr-pano/src/align.rs:1`](../core/dr-pano/src/align.rs#L1), [`core/dr-pano/src/lib.rs:1`](../core/dr-pano/src/lib.rs#L1), [`ui/dr-ui/src/lib.rs:1546`](../ui/dr-ui/src/lib.rs#L1546), [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/app.slint:1681`](../ui/dr-ui/ui/app.slint#L1681), [`ui/dr-ui/ui/app.slint:820`](../ui/dr-ui/ui/app.slint#L820), [`ui/dr-ui/ui/library.slint:1591`](../ui/dr-ui/ui/library.slint#L1591), [`ui/dr-ui/ui/library.slint:4236`](../ui/dr-ui/ui/library.slint#L4236), [`ui/dr-ui/ui/merge.slint:1`](../ui/dr-ui/ui/merge.slint#L1) | | FR-MRG-1 | [`core/dr-pano/src/align.rs:1`](../core/dr-pano/src/align.rs#L1), [`core/dr-pano/src/lib.rs:1`](../core/dr-pano/src/lib.rs#L1), [`ui/dr-ui/src/lib.rs:1546`](../ui/dr-ui/src/lib.rs#L1546), [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/app.slint:1681`](../ui/dr-ui/ui/app.slint#L1681), [`ui/dr-ui/ui/app.slint:820`](../ui/dr-ui/ui/app.slint#L820), [`ui/dr-ui/ui/library.slint:1591`](../ui/dr-ui/ui/library.slint#L1591), [`ui/dr-ui/ui/library.slint:4236`](../ui/dr-ui/ui/library.slint#L4236), [`ui/dr-ui/ui/merge.slint:1`](../ui/dr-ui/ui/merge.slint#L1) |
| FR-MRG-10 | [`core/dr-gpu/src/merge.rs:1`](../core/dr-gpu/src/merge.rs#L1), [`core/dr-gpu/src/shaders/merge.wgsl:1`](../core/dr-gpu/src/shaders/merge.wgsl#L1), [`core/dr-pano/src/lib.rs:1`](../core/dr-pano/src/lib.rs#L1) | | FR-MRG-10 | [`core/dr-gpu/src/merge.rs:1`](../core/dr-gpu/src/merge.rs#L1), [`core/dr-gpu/src/shaders/merge.wgsl:1`](../core/dr-gpu/src/shaders/merge.wgsl#L1), [`core/dr-pano/src/lib.rs:1`](../core/dr-pano/src/lib.rs#L1) |
| FR-MRG-11 | [`core/dr-gpu/src/merge.rs:1`](../core/dr-gpu/src/merge.rs#L1), [`core/dr-gpu/src/shaders/merge.wgsl:1`](../core/dr-gpu/src/shaders/merge.wgsl#L1) | | FR-MRG-11 | [`core/dr-gpu/src/merge.rs:1`](../core/dr-gpu/src/merge.rs#L1), [`core/dr-gpu/src/shaders/merge.wgsl:1`](../core/dr-gpu/src/shaders/merge.wgsl#L1) |
| FR-MRG-2 | [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:1157`](../core/dr-gpu/src/adjust.rs#L1157), [`core/dr-gpu/src/adjust.rs:1276`](../core/dr-gpu/src/adjust.rs#L1276), [`core/dr-gpu/src/adjust.rs:316`](../core/dr-gpu/src/adjust.rs#L316), [`core/dr-pipeline/src/graph.rs:852`](../core/dr-pipeline/src/graph.rs#L852), [`core/dr-pipeline/src/operation.rs:427`](../core/dr-pipeline/src/operation.rs#L427), [`core/dr-pipeline/src/operation.rs:595`](../core/dr-pipeline/src/operation.rs#L595), [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1) | | FR-MRG-2 | [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:1157`](../core/dr-gpu/src/adjust.rs#L1157), [`core/dr-gpu/src/adjust.rs:1276`](../core/dr-gpu/src/adjust.rs#L1276), [`core/dr-gpu/src/adjust.rs:316`](../core/dr-gpu/src/adjust.rs#L316), [`core/dr-pipeline/src/graph.rs:852`](../core/dr-pipeline/src/graph.rs#L852), [`core/dr-pipeline/src/operation.rs:427`](../core/dr-pipeline/src/operation.rs#L427), [`core/dr-pipeline/src/operation.rs:595`](../core/dr-pipeline/src/operation.rs#L595), [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1) |
| FR-MRG-3 | [`core/dr-decode/examples/linear_dng.rs:1`](../core/dr-decode/examples/linear_dng.rs#L1), [`core/dr-decode/src/lib.rs:137`](../core/dr-decode/src/lib.rs#L137), [`core/dr-decode/src/lib.rs:144`](../core/dr-decode/src/lib.rs#L144), [`core/dr-decode/src/lib.rs:578`](../core/dr-decode/src/lib.rs#L578), [`core/dr-decode/src/profile.rs:395`](../core/dr-decode/src/profile.rs#L395), [`core/dr-export/src/dng.rs:1`](../core/dr-export/src/dng.rs#L1), [`core/dr-gpu/src/demosaic.rs:254`](../core/dr-gpu/src/demosaic.rs#L254), [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1) | | FR-MRG-3 | [`core/dr-decode/examples/linear_dng.rs:1`](../core/dr-decode/examples/linear_dng.rs#L1), [`core/dr-decode/src/lib.rs:137`](../core/dr-decode/src/lib.rs#L137), [`core/dr-decode/src/lib.rs:144`](../core/dr-decode/src/lib.rs#L144), [`core/dr-decode/src/lib.rs:578`](../core/dr-decode/src/lib.rs#L578), [`core/dr-decode/src/profile.rs:395`](../core/dr-decode/src/profile.rs#L395), [`core/dr-export/src/dng.rs:1`](../core/dr-export/src/dng.rs#L1), [`core/dr-gpu/src/demosaic.rs:254`](../core/dr-gpu/src/demosaic.rs#L254), [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1) |
| FR-MRG-4 | [`core/dr-pano/src/projection.rs:1`](../core/dr-pano/src/projection.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/merge.slint:1`](../ui/dr-ui/ui/merge.slint#L1) | | FR-MRG-4 | [`core/dr-export/src/inscribed.rs:1`](../core/dr-export/src/inscribed.rs#L1), [`core/dr-pano/src/projection.rs:1`](../core/dr-pano/src/projection.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/merge.slint:1`](../ui/dr-ui/ui/merge.slint#L1) |
| FR-MRG-5 | [`core/dr-pano/src/align.rs:1`](../core/dr-pano/src/align.rs#L1), [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/merge.slint:1`](../ui/dr-ui/ui/merge.slint#L1) | | FR-MRG-5 | [`core/dr-pano/src/align.rs:1`](../core/dr-pano/src/align.rs#L1), [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/merge.slint:1`](../ui/dr-ui/ui/merge.slint#L1) |
| FR-MRG-6 | [`core/dr-pipeline/src/sidecar.rs:1033`](../core/dr-pipeline/src/sidecar.rs#L1033), [`core/dr-pipeline/src/sidecar.rs:113`](../core/dr-pipeline/src/sidecar.rs#L113), [`core/dr-pipeline/src/sidecar.rs:123`](../core/dr-pipeline/src/sidecar.rs#L123), [`core/dr-pipeline/src/sidecar.rs:2241`](../core/dr-pipeline/src/sidecar.rs#L2241), [`core/dr-pipeline/src/sidecar.rs:841`](../core/dr-pipeline/src/sidecar.rs#L841), [`ui/dr-ui/src/merge.rs:464`](../ui/dr-ui/src/merge.rs#L464) | | FR-MRG-6 | [`core/dr-pipeline/src/sidecar.rs:1033`](../core/dr-pipeline/src/sidecar.rs#L1033), [`core/dr-pipeline/src/sidecar.rs:113`](../core/dr-pipeline/src/sidecar.rs#L113), [`core/dr-pipeline/src/sidecar.rs:123`](../core/dr-pipeline/src/sidecar.rs#L123), [`core/dr-pipeline/src/sidecar.rs:2241`](../core/dr-pipeline/src/sidecar.rs#L2241), [`core/dr-pipeline/src/sidecar.rs:841`](../core/dr-pipeline/src/sidecar.rs#L841), [`ui/dr-ui/src/merge.rs:480`](../ui/dr-ui/src/merge.rs#L480) |
| FR-MRG-7 | [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/app.slint:820`](../ui/dr-ui/ui/app.slint#L820), [`ui/dr-ui/ui/merge.slint:1`](../ui/dr-ui/ui/merge.slint#L1) | | FR-MRG-7 | [`ui/dr-ui/src/merge.rs:1`](../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/app.slint:820`](../ui/dr-ui/ui/app.slint#L820), [`ui/dr-ui/ui/merge.slint:1`](../ui/dr-ui/ui/merge.slint#L1) |
| FR-MRG-8 | [`core/dr-pano/src/xfeat.rs:1`](../core/dr-pano/src/xfeat.rs#L1), [`core/dr-segment/examples/onnx_probe.rs:1`](../core/dr-segment/examples/onnx_probe.rs#L1) | | FR-MRG-8 | [`core/dr-pano/src/xfeat.rs:1`](../core/dr-pano/src/xfeat.rs#L1), [`core/dr-segment/examples/onnx_probe.rs:1`](../core/dr-segment/examples/onnx_probe.rs#L1) |
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:361`](../core/dr-sync/src/account.rs#L361), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:361`](../core/dr-sync/src/account.rs#L361), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) |
+35 -19
View File
@@ -110,6 +110,10 @@ impl MergeRequest {
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum Decision { pub enum Decision {
Merge { projection: Option<Projection> }, Merge { projection: Option<Projection> },
/// Draw the alignment again on another surface, and report it again.
/// The page's projection chips: what a choice looks like before it is
/// confirmed, at proxy cost rather than the merge's.
Preview { projection: Option<Projection> },
Abandon, Abandon,
} }
@@ -279,8 +283,7 @@ fn run_inner(
let projection = request let projection = request
.projection .projection
.unwrap_or_else(|| Projection::suggest(hfov, vfov)); .unwrap_or_else(|| Projection::suggest(hfov, vfov));
let bounds = projection::bounds(projection, focal_full, &cameras, frame_size) let suggested = projection;
.ok_or("the frames project nowhere")?;
// 4. Gains, before the report so the preview shows them. // 4. Gains, before the report so the preview shows them.
let gains = if alignment.is_complete() { let gains = if alignment.is_complete() {
@@ -288,23 +291,25 @@ fn run_inner(
} else { } else {
vec![1.0; frames.len()] vec![1.0; frames.len()]
}; };
let preview = if alignment.is_complete() { let report = |projection: Projection| -> Result<AlignmentReport, String> {
progress("Drawing the preview", 0, 1); let bounds = projection::bounds(projection, focal_full, &cameras, frame_size)
Some(preview(&colour, &proxies, &alignment, &gains, projection, &frames[0].raw)) .ok_or("the frames project nowhere")?;
} else { let preview = alignment
None .is_complete()
.then(|| preview(&colour, &proxies, &alignment, &gains, projection, &frames[0].raw));
Ok(AlignmentReport {
focal_mm: focal_full * 36.0 / full_long,
rms_px: alignment.rms_px,
projection,
width: bounds.width().ceil() as u32,
height: bounds.height().ceil() as u32,
frames: describe(&alignment),
links: alignment.links.len(),
preview,
})
}; };
let report = AlignmentReport { progress("Drawing the preview", 0, 1);
focal_mm: focal_full * 36.0 / full_long, let _ = events.send(MergeEvent::Aligned(report(suggested)?));
rms_px: alignment.rms_px,
projection,
width: bounds.width().ceil() as u32,
height: bounds.height().ceil() as u32,
frames: describe(&alignment),
links: alignment.links.len(),
preview,
};
let _ = events.send(MergeEvent::Aligned(report));
if !alignment.is_complete() { if !alignment.is_complete() {
let names: Vec<String> = alignment let names: Vec<String> = alignment
.unaligned .unaligned
@@ -322,7 +327,10 @@ fn run_inner(
return Ok(None); return Ok(None);
} }
match decision.recv_timeout(std::time::Duration::from_millis(100)) { match decision.recv_timeout(std::time::Duration::from_millis(100)) {
Ok(Decision::Merge { projection: p }) => break p.unwrap_or(projection), Ok(Decision::Merge { projection: p }) => break p.unwrap_or(suggested),
Ok(Decision::Preview { projection: p }) => {
let _ = events.send(MergeEvent::Aligned(report(p.unwrap_or(suggested))?));
}
Ok(Decision::Abandon) => return Ok(None), Ok(Decision::Abandon) => return Ok(None),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue, Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Ok(None), Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Ok(None),
@@ -388,6 +396,10 @@ fn run_inner(
// other's pace more than one band. // other's pace more than one band.
let (band_tx, band_rx) = std::sync::mpsc::sync_channel::<Vec<u16>>(1); let (band_tx, band_rx) = std::sync::mpsc::sync_channel::<Vec<u16>>(1);
let rows_per_strip = request.chunk.1.max(1); let rows_per_strip = request.chunk.1.max(1);
// The crop (FR-MRG-4), found as the bands go by; the writer reads it
// once the last strip is in, by which time every band has been seen.
let inscribed = Arc::new(std::sync::Mutex::new(dr_export::Inscribed::new(out_w)));
let inscribed_for_writer = inscribed.clone();
let file = std::fs::File::create(&out_path).map_err(|e| format!("{}: {e}", out_path.display()))?; let file = std::fs::File::create(&out_path).map_err(|e| format!("{}: {e}", out_path.display()))?;
let writer = std::thread::spawn(move || -> Result<(), String> { let writer = std::thread::spawn(move || -> Result<(), String> {
let mut file = std::io::BufWriter::new(file); let mut file = std::io::BufWriter::new(file);
@@ -405,6 +417,7 @@ fn run_inner(
buf.extend_from_slice(&band); buf.extend_from_slice(&band);
Ok(()) Ok(())
}, },
|| inscribed_for_writer.lock().ok().map(|i| i.best()),
) )
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
}); });
@@ -435,6 +448,9 @@ fn run_inner(
|band| { |band| {
bands_done += 1; bands_done += 1;
progress("Merging", bands_done, total_bands); progress("Merging", bands_done, total_bands);
if let Ok(mut i) = inscribed.lock() {
i.push_rows(band.covered, band.rows);
}
band_tx band_tx
.send(band.rgb.to_vec()) .send(band.rgb.to_vec())
.map_err(|_| dr_gpu::GpuError::Readback("the writer stopped".into())) .map_err(|_| dr_gpu::GpuError::Readback("the writer stopped".into()))
+21 -6
View File
@@ -224,12 +224,7 @@ pub fn wire<S, C, F>(
let ctl = ctl.clone(); let ctl = ctl.clone();
window.on_merge_confirm(move || { window.on_merge_confirm(move || {
let Some(w) = weak.upgrade() else { return }; let Some(w) = weak.upgrade() else { return };
let projection = match ctl.projection.get() { let projection = chip_projection(ctl.projection.get());
1 => Some(dr_pano::Projection::Perspective),
2 => Some(dr_pano::Projection::Cylindrical),
3 => Some(dr_pano::Projection::Spherical),
_ => None,
};
if let Some(job) = ctl.job.borrow().as_ref() { if let Some(job) = ctl.job.borrow().as_ref() {
let _ = job.decide.send(Decision::Merge { projection }); let _ = job.decide.send(Decision::Merge { projection });
job.activity.detail("merging"); job.activity.detail("merging");
@@ -240,9 +235,19 @@ pub fn wire<S, C, F>(
}); });
} }
{ {
let weak = window.as_weak();
let ctl = ctl.clone(); let ctl = ctl.clone();
window.on_merge_projection_picked(move |i| { window.on_merge_projection_picked(move |i| {
let Some(w) = weak.upgrade() else { return };
ctl.projection.set(i); ctl.projection.set(i);
w.set_merge_projection_selected(i);
// Show it: the job redraws the preview on that surface and
// reports again, and the drain puts the new picture up.
if let Some(job) = ctl.job.borrow().as_ref() {
let _ = job.decide.send(Decision::Preview {
projection: chip_projection(i),
});
}
}); });
} }
@@ -348,6 +353,16 @@ fn start<Fetch>(
*ctl.timer.borrow_mut() = Some(timer); *ctl.timer.borrow_mut() = Some(timer);
} }
/// The projection a chip index names; 0 is "Auto".
fn chip_projection(i: i32) -> Option<dr_pano::Projection> {
match i {
1 => Some(dr_pano::Projection::Perspective),
2 => Some(dr_pano::Projection::Cylindrical),
3 => Some(dr_pano::Projection::Spherical),
_ => None,
}
}
/// Take everything the job has said and reflect it on the page. /// Take everything the job has said and reflect it on the page.
fn drain(window: &AppWindow, ctl: &Rc<MergeController>, on_done: &Rc<impl Fn(&AppWindow)>) { fn drain(window: &AppWindow, ctl: &Rc<MergeController>, on_done: &Rc<impl Fn(&AppWindow)>) {
let events = { let events = {