LR-IMP-PREVIEW-READ: exports never decode a DNG's reduced copy

Reduced decoding is now opt-in: `files::load_vec_reduced` and the session's `reduced_loader` are used by grid
thumbnail jobs only; `load_bytes` / `load_vec` / `load_vec_pair` always decode the full image. A source developed
from a copy is marked (`DecodedSource::reduced`), so an export at a thumbnail's size reads the file again instead
of the cached thumbnail source, and Build Smart Previews never rebuilds a stale proxy from one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Matthew James Sivak
2026-10-20 13:08:03 +02:00
co-authored by Claude Opus 5.5
parent 7f97bdbb5f
commit 9ed971149b
4 changed files with 176 additions and 33 deletions
+6 -3
View File
@@ -212,8 +212,10 @@ struct SmartCounts {
fn original_only(source: crate::media::SourceRef) -> Option<crate::media::SourceRef> {
use crate::media::SourceRef;
match source {
SourceRef::File { path, max_edge, loader, denoise, .. } => Some(SourceRef::File { path, max_edge, loader, fallback: None, denoise }),
SourceRef::Loaded(d) if d.info.is_some() => Some(SourceRef::Loaded(d)),
SourceRef::File { path, max_edge, loader, denoise, .. } => {
Some(SourceRef::File { path, max_edge, loader, fallback: None, denoise, reduced: None })
}
SourceRef::Loaded(d) if d.info.is_some() && !d.reduced => Some(SourceRef::Loaded(d)),
_ => None,
}
}
@@ -548,7 +550,8 @@ mod tests {
let mut jobs = jobs_for(&mut s, id, Some(512));
assert_eq!(jobs.len(), 2, "view and thumbnail");
assert!(jobs[0].view_cache.is_some(), "the view renders first");
jobs[1].source = SourceRef::File { path: "/nonexistent/photo.arw".into(), max_edge: 512, loader: None, fallback: None, denoise: None };
jobs[1].source =
SourceRef::File { path: "/nonexistent/photo.arw".into(), max_edge: 512, loader: None, fallback: None, denoise: None, reduced: None };
assert!(run_photo(jobs), "the thumbnail rendered from the view's decoded source");
}
+120 -21
View File
@@ -11,7 +11,7 @@ use lightcraft_pipeline::SourceInfo;
use lightcraft_raster::Rgb32f;
use lightcraft_raster::resample::{Filter, fit};
use crate::media::{DenoiseSpec, FileLoader, FileProbe, PairLoader, PreviewLoader, ProbeInfo};
use crate::media::{DenoiseSpec, FileLoader, FileProbe, PairLoader, PreviewLoader, ProbeInfo, ReducedLoader};
fn meta_of(m: &lightcraft_meta::Metadata) -> (Meta, Option<String>) {
let shutter = m.exposure_time.map(|t| if t >= 1.0 { format!("{t:.0}") } else { format!("1/{:.0}", 1.0 / t) }).unwrap_or_default();
@@ -366,20 +366,27 @@ pub fn bin_factor(raw: &lightcraft_raw::RawImage, max_edge: usize) -> Option<usi
/// instead of each one waking the pool from outside and waiting for it (which costs more than the
/// loops themselves when the machine is busy).
pub fn load_bytes(bytes: &[u8], max_edge: usize) -> Result<(Rgb32f, SourceInfo), String> {
rayon::scope(|_| load_bytes_now(std::borrow::Cow::Borrowed(bytes), max_edge, None).map(|(img, _, info)| (img, info)))
rayon::scope(|_| load_bytes_now(std::borrow::Cow::Borrowed(bytes), max_edge, None, false).map(|(img, _, info, _)| (img, info)))
}
/// [`load_bytes`] taking the file's bytes: a raw file's bytes are freed as soon as it is decoded
/// (less memory held while it is developed).
pub fn load_vec(bytes: Vec<u8>, max_edge: usize) -> Result<(Rgb32f, SourceInfo), String> {
rayon::scope(move |_| load_bytes_now(std::borrow::Cow::Owned(bytes), max_edge, None).map(|(img, _, info)| (img, info)))
rayon::scope(move |_| load_bytes_now(std::borrow::Cow::Owned(bytes), max_edge, None, false).map(|(img, _, info, _)| (img, info)))
}
/// [`load_vec`] for a thumbnail: a DNG that keeps reduced-resolution copies of its raw image (Lightroom's HDR and
/// panorama merges) develops the smallest that covers `max_edge` instead of the whole image. The flag says whether
/// it did: such a picture is not the full image, and an export never renders from it.
pub fn load_vec_reduced(bytes: Vec<u8>, max_edge: usize) -> Result<(Rgb32f, SourceInfo, bool), String> {
rayon::scope(move |_| load_bytes_now(std::borrow::Cow::Owned(bytes), max_edge, None, true).map(|(img, _, info, reduced)| (img, info, reduced)))
}
/// [`load_vec`] that also develops the photo's cached denoised picture (`spec`) from the same decode: the plain
/// picture, the denoised one (`None` when it cannot be read or does not fit this raw: the plain one is then all
/// there is) and the source facts. Both pictures are the same size.
pub fn load_vec_pair(bytes: Vec<u8>, max_edge: usize, spec: &DenoiseSpec) -> Result<(Rgb32f, Option<Rgb32f>, SourceInfo), String> {
rayon::scope(move |_| load_bytes_now(std::borrow::Cow::Owned(bytes), max_edge, Some(spec)))
rayon::scope(move |_| load_bytes_now(std::borrow::Cow::Owned(bytes), max_edge, Some(spec), false).map(|(img, twin, info, _)| (img, twin, info)))
}
/// The denoised camera RGB of `raw` from its cached product, as the plain development would have it: the default
@@ -408,11 +415,15 @@ fn load_bytes_now(
bytes: std::borrow::Cow<'_, [u8]>,
max_edge: usize,
denoise: Option<&DenoiseSpec>,
) -> Result<(Rgb32f, Option<Rgb32f>, SourceInfo), String> {
allow_reduced: bool,
) -> Result<(Rgb32f, Option<Rgb32f>, SourceInfo, bool), String> {
if lightcraft_raw::probe(&bytes).is_some() {
// a thumbnail or preview of a DNG that keeps smaller copies of its raw image (Lightroom's merges) develops
// the smallest that covers it; a denoised photo's twin needs the full mosaic
let reduced = (denoise.is_none() && max_edge < usize::MAX).then(|| lightcraft_raw::decode_reduced(&bytes, max_edge).ok().flatten()).flatten();
// a thumbnail (`allow_reduced`, never an export) of a DNG that keeps smaller copies of its raw image
// (Lightroom's merges) develops the smallest that covers it; a denoised photo's twin needs the full mosaic
let reduced = (allow_reduced && denoise.is_none() && max_edge < usize::MAX)
.then(|| lightcraft_raw::decode_reduced(&bytes, max_edge).ok().flatten())
.flatten();
let used_reduced = reduced.is_some();
let decoded = match reduced {
Some(r) => Ok(r),
None => lightcraft_raw::decode(&bytes),
@@ -422,7 +433,7 @@ fn load_bytes_now(
Err(Ok(why)) => {
// show the camera's embedded JPEG (rendered, not raw) until the variant is supported
return load_embedded_preview(&bytes, max_edge)
.map(|(img, info)| (img, None, info))
.map(|(img, info)| (img, None, info, false))
.ok_or(format!("unsupported raw ({why}) without an embedded preview"));
}
Err(Err(e)) => return Err(e),
@@ -536,13 +547,14 @@ fn load_bytes_now(
// (the catalog knows the original's size: `media::source_info`)
native_long: None,
},
used_reduced,
));
}
let d = lightcraft_codecs::decode(&bytes, fit_box(max_edge)).map_err(|e| e.to_string())?;
drop(bytes);
let img = d.to_working();
let img = if img.width.max(img.height) > max_edge { fit(&img, max_edge, max_edge, Filter::Mitchell) } else { img };
Ok((img.into_oriented(Orientation::from_exif(d.orientation)), None, SourceInfo::default()))
Ok((img.into_oriented(Orientation::from_exif(d.orientation)), None, SourceInfo::default(), false))
}
/// Which white-balance scale a raw is edited on, decided from the file alone, the way the catalog decides it from
@@ -1067,17 +1079,7 @@ pub fn fs_pair_loader() -> PairLoader {
/// interactive loads (the loupe, exports) never wait — they are counted, so background work
/// yields to them.
pub fn fs_hooks() -> (FileLoader, FileProbe) {
let loader: FileLoader = Arc::new(|path: &str, max_edge: usize| {
let len = std::fs::metadata(path).map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX)).unwrap_or(0);
let weight = len * if max_edge <= crate::media::SourceLevel::Thumb.max_edge() { 3 } else { 6 };
let gate = crate::memory::work_gate();
let _permit = if crate::memory::is_background() { gate.acquire(weight) } else { gate.acquire_urgent(weight) };
let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?;
let r = load_vec(bytes, max_edge);
// the file, the samples and the intermediate images are gone: give their pages back
crate::memory::release();
r
});
let loader: FileLoader = Arc::new(|path: &str, max_edge: usize| fs_load(path, max_edge, |bytes| load_vec(bytes, max_edge)));
let probe: FileProbe = Arc::new(|path: &str| {
let len = std::fs::metadata(path).map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX)).unwrap_or(0);
let _permit = crate::memory::work_gate().acquire(len);
@@ -1089,6 +1091,24 @@ pub fn fs_hooks() -> (FileLoader, FileProbe) {
(loader, probe)
}
/// The filesystem [`ReducedLoader`]: [`load_vec_reduced`] of the file, gated like [`fs_hooks`]' loader.
pub fn fs_reduced_loader() -> ReducedLoader {
Arc::new(|path: &str, max_edge: usize| fs_load(path, max_edge, |bytes| load_vec_reduced(bytes, max_edge)))
}
/// Read `path` and `load` it, waiting on [`crate::memory::work_gate`] as [`fs_hooks`] explains.
fn fs_load<T>(path: &str, max_edge: usize, load: impl FnOnce(Vec<u8>) -> Result<T, String>) -> Result<T, String> {
let len = std::fs::metadata(path).map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX)).unwrap_or(0);
let weight = len.saturating_mul(if max_edge <= crate::media::SourceLevel::Thumb.max_edge() { 3 } else { 6 });
let gate = crate::memory::work_gate();
let _permit = if crate::memory::is_background() { gate.acquire(weight) } else { gate.acquire_urgent(weight) };
let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?;
let r = load(bytes);
// the file, the samples and the intermediate images are gone: give their pages back
crate::memory::release();
r
}
/// `path`'s modification time as ISO 8601 (see [`ProbeInfo::file_modified`]); `None` when the
/// filesystem doesn't say or the time is before 1970.
fn file_modified(path: &str) -> Option<String> {
@@ -1103,6 +1123,7 @@ impl crate::Session {
pub fn with_fs(mut self) -> Self {
let (l, p) = fs_hooks();
self.media.file_loader = Some(l);
self.media.reduced_loader = Some(fs_reduced_loader());
self.media.file_probe = Some(p);
self.media.preview_loader = Some(fs_preview_loader());
self.media.denoise.loader = Some(fs_pair_loader());
@@ -2097,6 +2118,84 @@ pub(crate) mod tests {
}
}
/// A Lightroom merge (a LinearRaw DNG keeping a reduced copy of its raw image): its grid thumbnail develops the
/// copy, but an export never does, whatever its size, and not from the thumbnail's source in memory either. The
/// copy's samples are much brighter than the image's here, so a picture says which one it came from.
#[test]
fn exports_never_develop_a_dngs_reduced_copy() {
use lightcraft_tiff::tags::{self as t, photometric};
use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, Value};
use serde_json::json;
let linear = |subfile: u32, w: u32, h: u32, sample: u16| {
let mut ifd = IfdBuilder::new();
ifd.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![subfile]));
ifd.set(t::IMAGE_WIDTH, Value::Long(vec![w]));
ifd.set(t::IMAGE_LENGTH, Value::Long(vec![h]));
ifd.set(t::BITS_PER_SAMPLE, Value::Short(vec![16, 16, 16]));
ifd.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![3]));
ifd.set(t::PHOTOMETRIC, Value::Short(vec![photometric::LINEAR_RAW]));
ifd.set(t::COMPRESSION, Value::Short(vec![1]));
ifd.set(t::WHITE_LEVEL, Value::Long(vec![32768]));
ifd.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![sample.to_le_bytes().repeat((w * h * 3) as usize)] });
ifd
};
// (the copy must cover a thumbnail source's 512 px)
let (w, h) = (1200u32, 600u32);
let mut main = linear(0, w, h, 1500);
main.set(t::DEFAULT_CROP_ORIGIN, Value::Long(vec![4, 2]));
main.set(t::DEFAULT_CROP_SIZE, Value::Long(vec![w - 8, h - 4]));
let mut ifd0 = IfdBuilder::new();
ifd0.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![1]));
ifd0.set(t::DNG_VERSION, Value::Byte(vec![1, 7, 0, 0]));
ifd0.set(t::MAKE, Value::Ascii("DJI".into()));
ifd0.set(t::COLOR_MATRIX_1, Value::SRational(vec![(1, 1), (0, 1), (0, 1), (0, 1), (1, 1), (0, 1), (0, 1), (0, 1), (1, 1)]));
ifd0.set(t::CALIBRATION_ILLUMINANT_1, Value::Short(vec![21]));
ifd0.set(t::PHOTOMETRIC, Value::Short(vec![photometric::RGB]));
ifd0.set(t::IMAGE_WIDTH, Value::Long(vec![4]));
ifd0.set(t::IMAGE_LENGTH, Value::Long(vec![2]));
ifd0.set(t::BITS_PER_SAMPLE, Value::Short(vec![8, 8, 8]));
ifd0.set(t::SAMPLES_PER_PIXEL, Value::Short(vec![3]));
ifd0.set(t::COMPRESSION, Value::Short(vec![1]));
ifd0.set_image(ImageData::Strips { rows_per_strip: 2, strips: vec![vec![128; 24]] });
ifd0.add_sub_ifd(main);
ifd0.add_sub_ifd(linear(1, w / 2, h / 2, 24000));
let bytes = TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap();
let g = |img: &Rgb32f| img.data.iter().map(|p| p[1] as f64).sum::<f64>() / img.data.len().max(1) as f64;
let mean = |img: &lightcraft_raster::Rgba8| img.data.iter().map(|p| p[1] as f64).sum::<f64>() / img.data.len().max(1) as f64;
// the loaders: only the thumbnail's develops the copy, and only when it covers the size
let (copy, _, used) = load_vec_reduced(bytes.clone(), 512).unwrap();
assert!(used, "a thumbnail develops the copy");
assert!(!load_vec_reduced(bytes.clone(), 1024).unwrap().2, "no copy covers 1024 px");
let (full, _) = load_vec(bytes.clone(), 512).unwrap();
assert!(g(&copy) > 4.0 * g(&full), "the fixture's copy is much brighter: {} vs {}", g(&copy), g(&full));
let dir = std::env::temp_dir().join(format!("lc-reduced-export-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("merge-pano.dng");
std::fs::write(&path, &bytes).unwrap();
let mut s = crate::Session::new().with_fs();
let r = s.execute("library.import", &json!({"paths": [path.to_string_lossy()]})).unwrap();
let id = lightcraft_catalog::PhotoId(r["imported"][0].as_u64().unwrap());
let (space, depth) = (lightcraft_pipeline::OutputSpace::Srgb, lightcraft_pipeline::OutputDepth::U8);
// an export at a thumbnail's size, before any thumbnail
assert_eq!(s.export_job(id, 64, 64, space, depth).unwrap().level, crate::media::SourceLevel::Thumb);
let export = mean(&s.render_export(id, 64, 64, space, depth).unwrap().image);
// the thumbnail develops the copy and its source stays in memory…
s.media.clear_sources();
let thumb = s.thumb_job(id, 64).unwrap().run();
assert!(thumb.loaded.as_ref().is_some_and(|l| l.reduced));
let thumb_mean = mean(&thumb.rendered.as_ref().unwrap().image);
s.accept(&thumb);
assert!(thumb_mean > export + 20.0, "the thumbnail shows the copy: {thumb_mean} vs {export}");
// …and an export after it still reads the full image
let job = s.export_job(id, 64, 64, space, depth).unwrap();
assert!(matches!(&job.source, crate::media::SourceRef::File { reduced: None, .. }), "the file, not the copy");
let again = mean(&s.render_export(id, 64, 64, space, depth).unwrap().image);
assert!((again - export).abs() < 1.0, "{again} vs {export}");
let _ = std::fs::remove_dir_all(&dir);
}
/// iPhone ProRAW-style semantic masks drive the Sky mask: Apple's sky matte (here the right
/// half of the sensor, i.e. the bottom of the photo once turned 90° clockwise — where the sky
/// heuristic would never look) is used; a matte with an unknown name is ignored.
+43 -8
View File
@@ -132,6 +132,11 @@ fn source_edge_needed(p: &Photo, settings: &DevelopSettings, apply_crop: bool, o
/// Decodes a file into a linear Rec.2020 image no larger than `max_edge` (set by the app).
pub type FileLoader = Arc<dyn Fn(&str, usize) -> Result<(Rgb32f, SourceInfo), String> + Send + Sync>;
/// [`FileLoader`] for a thumbnail: a raw file that keeps smaller copies of its raw image (Lightroom's merged DNGs)
/// develops the smallest that covers `max_edge` (set by the app); the flag says it did. Never used for an export,
/// which must render the full image.
pub type ReducedLoader = Arc<dyn Fn(&str, usize) -> Result<(Rgb32f, SourceInfo, bool), String> + Send + Sync>;
/// A raw file's embedded (camera-rendered) preview as display sRGB, oriented, no larger than
/// `max_edge` (set by the app). `None`: no usable preview.
pub type PreviewLoader = Arc<dyn Fn(&str, usize) -> Option<Rgba8> + Send + Sync>;
@@ -183,11 +188,14 @@ pub struct DecodedSource {
pub camera_tone: Option<lightcraft_pipeline::tone::CameraTone>,
/// The same picture developed from the AI-denoised mosaic, when the photo has one cached.
pub denoised: Option<Arc<Twin>>,
/// Developed from a smaller copy of the raw image that the file keeps ([`ReducedLoader`]), not from the image
/// itself: good for a thumbnail, never for an export.
pub reduced: bool,
}
impl DecodedSource {
pub fn new(image: Arc<Rgb32f>, info: Option<SourceInfo>) -> Self {
DecodedSource { image, info, camera_tone: None, denoised: None }
DecodedSource { image, info, camera_tone: None, denoised: None, reduced: false }
}
/// The picture to develop for a Denoise `amount` (0 to 1, see [`DevelopSettings::denoise_amount`]): the plain one,
@@ -258,6 +266,8 @@ pub enum SourceRef {
fallback: Option<std::path::PathBuf>,
/// The photo has a cached denoised picture: load it with the plain one (see [`PairLoader`]).
denoise: Option<(DenoiseSpec, PairLoader)>,
/// A thumbnail's loader, used instead of `loader` (see [`ReducedLoader`]); `None` for every other job.
reduced: Option<ReducedLoader>,
},
/// A smart preview standing in for a missing original.
Smart {
@@ -281,10 +291,13 @@ impl SourceRef {
return Ok(DecodedSource { image: Arc::new(image), denoised: None, ..(**from).clone() });
}
SourceRef::Demo { scene, max_edge } => crate::demo::scene_pixels(scene, *max_edge)?,
SourceRef::File { path, max_edge, loader, fallback, denoise } => {
let plain = || match loader {
Some(l) => l(path, *max_edge).map(|(image, info)| DecodedSource::new(Arc::new(image), Some(info))),
None => Err(format!("no decoder available for {path}")),
SourceRef::File { path, max_edge, loader, fallback, denoise, reduced } => {
let plain = || match (reduced, loader) {
(Some(l), _) => {
l(path, *max_edge).map(|(image, info, reduced)| DecodedSource { reduced, ..DecodedSource::new(Arc::new(image), Some(info)) })
}
(None, Some(l)) => l(path, *max_edge).map(|(image, info)| DecodedSource::new(Arc::new(image), Some(info))),
(None, None) => Err(format!("no decoder available for {path}")),
};
// the denoised picture comes with the plain one from a single decode; when it cannot be read the
// pair loader gives the plain picture alone (the cached file is never a reason to fail a render)
@@ -306,6 +319,7 @@ impl SourceRef {
info: Some(info),
camera_tone: None,
denoised: twin.map(|t| Arc::new(Twin::new(Arc::new(t)))),
reduced: false,
})
})
} else {
@@ -351,6 +365,8 @@ pub struct MediaCache {
/// share of [`crate::memory::budget`]); the least recently used entry of any of them goes first.
budget: usize,
pub file_loader: Option<FileLoader>,
/// What grid thumbnails load files with (see [`ReducedLoader`]); `None`: [`Self::file_loader`].
pub reduced_loader: Option<ReducedLoader>,
pub file_probe: Option<FileProbe>,
pub preview_loader: Option<PreviewLoader>,
/// Reads a photo file's bytes (Photo Merge); `None` = the local file system.
@@ -426,6 +442,7 @@ impl Default for MediaCache {
preview_capacity: 0,
budget,
file_loader: None,
reduced_loader: None,
file_probe: None,
preview_loader: None,
file_bytes: None,
@@ -718,11 +735,11 @@ impl MediaCache {
}
match self.scenes.iter().find(|s| s.id == *scene) {
Some(s) => SourceRef::Demo { scene: Box::new(s.clone()), max_edge },
None => SourceRef::File { path: format!("demo:{scene}"), max_edge, loader: None, fallback: None, denoise: None },
None => SourceRef::File { path: format!("demo:{scene}"), max_edge, loader: None, fallback: None, denoise: None, reduced: None },
}
}
Source::File { path } => {
SourceRef::File { path: path.clone(), max_edge, loader: self.file_loader.clone(), fallback: None, denoise: None }
SourceRef::File { path: path.clone(), max_edge, loader: self.file_loader.clone(), fallback: None, denoise: None, reduced: None }
}
}
}
@@ -1201,7 +1218,12 @@ impl crate::Session {
/// (≥ `long_edge`) and the result comes from / goes to the thumbnail cache (memory + disk).
pub fn thumb_job(&mut self, id: PhotoId, long_edge: usize) -> Option<RenderJob> {
let b = thumb_bucket(long_edge);
self.build_job(id, b, b, false, true, Some(b))
let mut job = self.build_job(id, b, b, false, true, Some(b))?;
// (a thumbnail may develop a smaller copy of the raw image that the file keeps)
if let SourceRef::File { reduced, .. } = &mut job.source {
reduced.clone_from(&self.media.reduced_loader);
}
Some(job)
}
fn build_job(
@@ -1610,6 +1632,17 @@ impl crate::Session {
depth: lightcraft_pipeline::OutputDepth,
) -> Result<RenderJob, String> {
let mut job = self.render_job(id, max_w, max_h, false, true).ok_or("no such photo")?;
// An export renders the full image: never a thumbnail's source developed from a smaller copy of the raw
// image that the file keeps (Lightroom's merged DNGs)
let reduced = match &job.source {
SourceRef::Loaded(s) => s.reduced,
SourceRef::Fit { from, .. } => from.reduced,
_ => false,
};
if reduced {
let p = self.catalog.photo(id).ok_or("no such photo")?.clone();
job.source = self.media.source_ref_uncached(&p, job.level);
}
// An export is made from the denoised picture its Denoise amount asks for, made when the job runs if the
// background queue has not got to it. (A source kept in memory may lack the picture, so the job reads the file.)
if let Some(wanted) = self.denoise_for_export(id, &job.settings)? {
@@ -1866,6 +1899,7 @@ mod tests {
})),
fallback: None,
denoise: None,
reduced: None,
};
job.info = SourceInfo::default(); // Header facts cannot override decoder facts.
job.settings = Arc::new(DevelopSettings::default());
@@ -1921,6 +1955,7 @@ mod tests {
loader: Some(Arc::new(move |_, _| Ok((loaded_pixels.clone(), loaded_info.clone())))),
fallback: None,
denoise: None,
reduced: None,
};
let undo_before = s.undo.len();
let request = job.request;
+7 -1
View File
@@ -356,7 +356,13 @@ pub fn mark_look_checked(dir: &Path) -> Result<(), String> {
/// Load the proxy at `path`.
pub fn load(path: &Path) -> Result<crate::media::DecodedSource, String> {
let b = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
decode(&b).map(|(image, camera_tone)| crate::media::DecodedSource { image: Arc::new(image), info: None, camera_tone, denoised: None })
decode(&b).map(|(image, camera_tone)| crate::media::DecodedSource {
image: Arc::new(image),
info: None,
camera_tone,
denoised: None,
reduced: false,
})
}
#[cfg(test)]