mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 12:08:10 -06:00
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:
co-authored by
Claude Opus 5.5
parent
7f97bdbb5f
commit
9ed971149b
@@ -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
@@ -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(©) > 4.0 * g(&full), "the fixture's copy is much brighter: {} vs {}", g(©), 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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user