Route dr-ui's decoding through the Decoder trait

With the trait in place the claim still meant nothing while every caller
named dr_decode's free functions: a second decoder would have had to be
threaded through the scan, the thumbnail ladder, import, the viewer,
export, merge and repairs at the moment it arrived.

Each of those now takes a &dyn Decoder and reads headers, previews,
orientation and sensor data through it, including the header budget a
remote fetch asks for (header_bytes) and where it finds the embedded
preview (locate_preview). Only the places that start a job name
dr_decode::default(): the thumbnail, sweep and thumbnail-sweep threads,
the viewer's open handlers, and the request structs a job is handed
(BatchRequest, MergeRequest, the import Request, the repairs Toolkit),
so a caller can be given another decoder by changing what it is handed.

The default is rawler through the same free functions as before, so
nothing a user sees changes. The trait gains Debug as a supertrait so
request structs that derive Debug can carry one.
This commit is contained in:
2026-09-24 21:33:14 -04:00
parent d8fb382ce9
commit 414094bd38
10 changed files with 214 additions and 129 deletions
+4 -3
View File
@@ -27,9 +27,10 @@ use crate::{DecodeError, Metadata, Preview, PreviewLocation, PreviewSize, RawIma
/// A RAW decoder, over bytes. /// A RAW decoder, over bytes.
/// ///
/// Object-safe so a caller can hold `&dyn Decoder` without becoming generic, /// Object-safe so a caller can hold `&dyn Decoder` without becoming generic,
/// and `Send + Sync` because the callers that need one most — the thumbnail /// `Send + Sync` because the callers that need one most — the thumbnail
/// lanes, the export worker — run off the UI thread. /// lanes, the export worker — run off the UI thread, and `Debug` so a job
pub trait Decoder: Send + Sync { /// description that carries one can still be printed.
pub trait Decoder: Send + Sync + std::fmt::Debug {
/// How much of the start of a file [`Self::metadata`] and /// How much of the start of a file [`Self::metadata`] and
/// [`Self::locate_preview`] need. A caller reading over a network fetches /// [`Self::locate_preview`] need. A caller reading over a network fetches
/// this range and no more. /// this range and no more.
File diff suppressed because one or more lines are too long
+24 -5
View File
@@ -517,6 +517,10 @@ pub struct BatchRequest {
/// rendered at all — reported per image rather than refused up front, so /// rendered at all — reported per image rather than refused up front, so
/// the reason lands in the same place every other failure does. /// the reason lands in the same place every other failure does.
pub gpu: Option<dr_gpu::GpuContext>, pub gpu: Option<dr_gpu::GpuContext>,
/// TRACES: FR-RAW-2
/// What reads the header and decodes the sensor data, chosen by whoever
/// assembled the batch; the worker never names one.
pub decoder: &'static dyn dr_decode::Decoder,
} }
/// TRACES: NFR-ARCH-4 /// TRACES: NFR-ARCH-4
@@ -750,6 +754,22 @@ type RenderedItem = (
Option<dr_export::SourceMetadata>, Option<dr_export::SourceMetadata>,
); );
/// TRACES: FR-RAW-2 | FR-EXP-8
/// Read a fetched photograph's header and open it for rendering, through
/// whichever decoder the batch was given.
///
/// Split from [`render_from_library`] so the half that decodes can be run
/// without a server: the stub-decoder test drives it directly.
pub(crate) fn open_for_export(
gpu: &dr_gpu::GpuContext,
decoder: &dyn dr_decode::Decoder,
bytes: &[u8],
) -> Result<(dr_decode::Metadata, crate::DevelopSession), String> {
let meta = decoder.metadata(bytes).unwrap_or_default();
let session = crate::open_session(gpu, decoder, bytes, &meta)?;
Ok((meta, session))
}
/// Fetch a photograph, apply its stored edit, and render it at full size. /// Fetch a photograph, apply its stored edit, and render it at full size.
/// ///
/// `None` means the work was cancelled, which is not a failure and has no /// `None` means the work was cancelled, which is not a failure and has no
@@ -802,13 +822,11 @@ fn render_from_library(
Waited::Silent => None, Waited::Silent => None,
}; };
let meta = dr_decode::metadata(&bytes).unwrap_or_default(); let (meta, mut session) = match open_for_export(gpu, request.decoder, &bytes) {
let (date, carried) = from_header(&meta); Ok(opened) => opened,
let mut session = match crate::open_session(gpu, &bytes, &meta) {
Ok(session) => session,
Err(e) => return Some(Err(ItemError::Open(e))), Err(e) => return Some(Err(ItemError::Open(e))),
}; };
let (date, carried) = from_header(&meta);
// TRACES: FR-CAT-8 // TRACES: FR-CAT-8
// The stored edit. FR-EXP-9 is about resolution; this is the other half of // The stored edit. FR-EXP-9 is about resolution; this is the other half of
@@ -1400,6 +1418,7 @@ mod tests {
sidecar_cache: std::env::temp_dir().join("dr-batch-test-sidecars"), sidecar_cache: std::env::temp_dir().join("dr-batch-test-sidecars"),
offline: true, offline: true,
gpu: None, gpu: None,
decoder: dr_decode::default(),
} }
} }
+30 -10
View File
@@ -92,6 +92,9 @@ pub struct Request {
/// Where the photographs are going. An import without one has nowhere to /// Where the photographs are going. An import without one has nowhere to
/// put anything, and the page refuses to start. /// put anything, and the page refuses to start.
pub upload: Upload, pub upload: Upload,
/// TRACES: FR-RAW-2
/// What reads each candidate's header and cuts its thumbnail.
pub decoder: &'static dyn dr_decode::Decoder,
} }
/// TRACES: FR-NC-7a | FR-NC-7b /// TRACES: FR-NC-7a | FR-NC-7b
@@ -283,7 +286,7 @@ fn run(request: Request, cancel: &Cancel, tx: &Sender<Message>) -> Result<(), St
let report = ingest.run( let report = ingest.run(
&candidates, &candidates,
&|c| probe(&card, c), &|c| probe(&card, request.decoder, c),
&|key| is_duplicate(catalog.connection(), key), &|key| is_duplicate(catalog.connection(), key),
&|| cancel.load(Ordering::Relaxed), &|| cancel.load(Ordering::Relaxed),
&mut |p| { &mut |p| {
@@ -303,7 +306,14 @@ fn run(request: Request, cancel: &Cancel, tx: &Sender<Message>) -> Result<(), St
// photograph — only a transfer. // photograph — only a transfer.
{ {
let upload = &request.upload; let upload = &request.upload;
let (sent, thumbnails) = upload_all(upload, &library, &report.imported, cancel, tx); let (sent, thumbnails) = upload_all(
upload,
request.decoder,
&library,
&report.imported,
cancel,
tx,
);
outcome.thumbnails = thumbnails; outcome.thumbnails = thumbnails;
outcome.uploaded = sent.iter().filter(|s| s.transferred).count(); outcome.uploaded = sent.iter().filter(|s| s.transferred).count();
outcome.already_on_server = sent.len() - outcome.uploaded; outcome.already_on_server = sent.len() - outcome.uploaded;
@@ -370,6 +380,7 @@ fn run(request: Request, cancel: &Cancel, tx: &Sender<Message>) -> Result<(), St
/// kept and a card emptied. /// kept and a card emptied.
fn upload_all( fn upload_all(
upload: &Upload, upload: &Upload,
decoder: &dyn dr_decode::Decoder,
library: &LocalStorage, library: &LocalStorage,
imported: &[Imported], imported: &[Imported],
cancel: &Cancel, cancel: &Cancel,
@@ -418,7 +429,7 @@ fn upload_all(
// Before the body is handed over: `upload_original` takes it by // Before the body is handed over: `upload_original` takes it by
// value, and re-reading the file to make a thumbnail afterwards // value, and re-reading the file to make a thumbnail afterwards
// would be a second full read of an 80 MB original. // would be a second full read of an 80 MB original.
let thumbnail = make_thumbnail(&bytes); let thumbnail = make_thumbnail(decoder, &bytes);
// The same folder segments the local copy went into, so the two // The same folder segments the local copy went into, so the two
// libraries have the same shape (FR-NC-7a). // libraries have the same shape (FR-NC-7a).
@@ -473,10 +484,15 @@ struct Sent {
/// than the full preview's. An embedded preview is written in the sensor's /// than the full preview's. An embedded preview is written in the sensor's
/// orientation, so without this every portrait frame lies on its side — and /// orientation, so without this every portrait frame lies on its side — and
/// the store is keyed by file and size alone, so it would stay that way. /// the store is keyed by file and size alone, so it would stay that way.
fn make_thumbnail(bytes: &[u8]) -> Option<dr_thumbs::Thumbnail> { pub(crate) fn make_thumbnail(
let mut preview = dr_decode::extract_preview(bytes, dr_decode::PreviewSize::Thumbnail).ok()?; decoder: &dyn dr_decode::Decoder,
bytes: &[u8],
) -> Option<dr_thumbs::Thumbnail> {
let mut preview = decoder
.preview(bytes, dr_decode::PreviewSize::Thumbnail)
.ok()?;
preview.downscale_to(dr_thumbs::ThumbSize::Grid.edge()); preview.downscale_to(dr_thumbs::ThumbSize::Grid.edge());
preview.apply_orientation(dr_decode::orientation(bytes).unwrap_or_default()); preview.apply_orientation(decoder.orientation(bytes).unwrap_or_default());
let encoded = dr_thumbs::encode_rgba(preview.width, preview.height, &preview.rgba).ok()?; let encoded = dr_thumbs::encode_rgba(preview.width, preview.height, &preview.rgba).ok()?;
Some(dr_thumbs::Thumbnail { Some(dr_thumbs::Thumbnail {
width: preview.width, width: preview.width,
@@ -676,14 +692,18 @@ pub fn survey(
/// Read one candidate's capture metadata. /// Read one candidate's capture metadata.
/// ///
/// A header read rather than the whole file: `dr_decode::HEADER_BYTES` is /// A header read rather than the whole file: the decoder's `header_bytes` is
/// enough for EXIF on every format in the tree, and reading 80 MB per file to /// enough for EXIF on every format in the tree, and reading 80 MB per file to
/// learn a date would take longer than the import itself. /// learn a date would take longer than the import itself.
fn probe(card: &dyn Storage, candidate: &Candidate) -> Option<Shot> { fn probe(
card: &dyn Storage,
decoder: &dyn dr_decode::Decoder,
candidate: &Candidate,
) -> Option<Shot> {
let header = card let header = card
.read_range(&candidate.source, 0..dr_decode::HEADER_BYTES) .read_range(&candidate.source, 0..decoder.header_bytes())
.ok()?; .ok()?;
let md = dr_decode::metadata(&header).ok()?; let md = decoder.metadata(&header).ok()?;
Some(Shot { Some(Shot {
captured_at: md.captured_at, captured_at: md.captured_at,
captured_offset: md.captured_offset, captured_offset: md.captured_offset,
+1
View File
@@ -589,6 +589,7 @@ fn start(
filter: context.filter.clone(), filter: context.filter.clone(),
options: ctl.ingest_options(), options: ctl.ingest_options(),
upload, upload,
decoder: dr_decode::default(),
}; };
let cancel: import::Cancel = Default::default(); let cancel: import::Cancel = Default::default();
+25 -12
View File
@@ -71,7 +71,7 @@ use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use anyhow::Result; use anyhow::Result;
use dr_decode::{Metadata, PreviewSize}; use dr_decode::{Decoder, Metadata, PreviewSize};
use slint::ComponentHandle as _; use slint::ComponentHandle as _;
pub use develop::DevelopSession; pub use develop::DevelopSession;
@@ -198,7 +198,11 @@ struct Loaded {
/// (FR-NC-3) fetches only a byte range for *browsing*, which is why the /// (FR-NC-3) fetches only a byte range for *browsing*, which is why the
/// preview API is separate — this is the develop path, and it is expected to /// preview API is separate — this is the develop path, and it is expected to
/// be expensive. /// be expensive.
fn load(ctx: Option<&dr_gpu::GpuContext>, path: &Path) -> Result<Loaded, String> { fn load(
ctx: Option<&dr_gpu::GpuContext>,
decoder: &dyn Decoder,
path: &Path,
) -> Result<Loaded, String> {
// A VFS placeholder holds one byte and reading it triggers no fetch // A VFS placeholder holds one byte and reading it triggers no fetch
// (ARCH §9.0). Say so plainly rather than reporting a decode failure. // (ARCH §9.0). Say so plainly rather than reporting a decode failure.
if path if path
@@ -210,7 +214,7 @@ fn load(ctx: Option<&dr_gpu::GpuContext>, path: &Path) -> Result<Loaded, String>
} }
let bytes = std::fs::read(path).map_err(|e| e.to_string())?; let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
load_bytes(ctx, &bytes) load_bytes(ctx, decoder, &bytes)
} }
/// Open already-fetched bytes. /// Open already-fetched bytes.
@@ -218,8 +222,12 @@ fn load(ctx: Option<&dr_gpu::GpuContext>, path: &Path) -> Result<Loaded, String>
/// Split from [`load`] because a library image has no local file: it arrives /// Split from [`load`] because a library image has no local file: it arrives
/// as a WebDAV response body, and writing it to disk purely to read it back /// as a WebDAV response body, and writing it to disk purely to read it back
/// would be a round-trip for nothing. /// would be a round-trip for nothing.
fn load_bytes(ctx: Option<&dr_gpu::GpuContext>, bytes: &[u8]) -> Result<Loaded, String> { fn load_bytes(
let meta = dr_decode::metadata(bytes).unwrap_or_default(); ctx: Option<&dr_gpu::GpuContext>,
decoder: &dyn Decoder,
bytes: &[u8],
) -> Result<Loaded, String> {
let meta = decoder.metadata(bytes).unwrap_or_default();
// How the file stored its pixels, for the preview path below — the develop // How the file stored its pixels, for the preview path below — the develop
// path takes it from the same header inside `open_session`. A file that // path takes it from the same header inside `open_session`. A file that
@@ -227,7 +235,7 @@ fn load_bytes(ctx: Option<&dr_gpu::GpuContext>, bytes: &[u8]) -> Result<Loaded,
let orientation = meta.orientation.unwrap_or_default(); let orientation = meta.orientation.unwrap_or_default();
if let Some(ctx) = ctx { if let Some(ctx) = ctx {
match open_session(ctx, bytes, &meta) { match open_session(ctx, decoder, bytes, &meta) {
Ok(session) => { Ok(session) => {
let (width, height) = session.source_size(); let (width, height) = session.source_size();
return Ok(Loaded { return Ok(Loaded {
@@ -244,8 +252,9 @@ fn load_bytes(ctx: Option<&dr_gpu::GpuContext>, bytes: &[u8]) -> Result<Loaded,
// Fall back to the embedded preview: no GPU, or a file neither decoder // Fall back to the embedded preview: no GPU, or a file neither decoder
// could open for editing. Read-only, and the adjust panel is disabled. // could open for editing. Read-only, and the adjust panel is disabled.
let mut preview = let mut preview = decoder
dr_decode::extract_preview(bytes, PreviewSize::Screen).map_err(|e| e.to_string())?; .preview(bytes, PreviewSize::Screen)
.map_err(|e| e.to_string())?;
preview.downscale_to(MAX_DISPLAY_DIM); preview.downscale_to(MAX_DISPLAY_DIM);
// No graph here to carry the baseline, so the pixels are turned instead. // No graph here to carry the baseline, so the pixels are turned instead.
// Cheaper than it sounds after the downscale, and this path is the one a // Cheaper than it sounds after the downscale, and this path is the one a
@@ -283,12 +292,13 @@ fn load_bytes(ctx: Option<&dr_gpu::GpuContext>, bytes: &[u8]) -> Result<Loaded,
/// construction rather than by everybody remembering to say so. /// construction rather than by everybody remembering to say so.
pub(crate) fn open_session( pub(crate) fn open_session(
ctx: &dr_gpu::GpuContext, ctx: &dr_gpu::GpuContext,
decoder: &dyn Decoder,
bytes: &[u8], bytes: &[u8],
meta: &Metadata, meta: &Metadata,
) -> Result<DevelopSession, String> { ) -> Result<DevelopSession, String> {
// How the file stored its pixels. A file that says nothing is taken as // How the file stored its pixels. A file that says nothing is taken as
// upright — see `Orientation::from_exif`. // upright — see `Orientation::from_exif`.
let mut session = open_pixels(ctx, bytes, meta.orientation.unwrap_or_default())?; let mut session = open_pixels(ctx, decoder, bytes, meta.orientation.unwrap_or_default())?;
// TRACES: FR-EXP-8 // TRACES: FR-EXP-8
// The header goes with the session rather than being read again later, // The header goes with the session rather than being read again later,
@@ -308,6 +318,7 @@ pub(crate) fn open_session(
/// the pixels came from. /// the pixels came from.
fn open_pixels( fn open_pixels(
ctx: &dr_gpu::GpuContext, ctx: &dr_gpu::GpuContext,
decoder: &dyn Decoder,
bytes: &[u8], bytes: &[u8],
orientation: dr_types::Orientation, orientation: dr_types::Orientation,
) -> Result<DevelopSession, String> { ) -> Result<DevelopSession, String> {
@@ -338,7 +349,8 @@ fn open_pixels(
// A failure here is expected for bodies rawler does not know, and must not // A failure here is expected for bodies rawler does not know, and must not
// stop the image displaying (FR-RAW-4). // stop the image displaying (FR-RAW-4).
dr_decode::decode(bytes) decoder
.decode(bytes)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
.and_then(|raw| DevelopSession::open(ctx, &raw, orientation)) .and_then(|raw| DevelopSession::open(ctx, &raw, orientation))
} }
@@ -676,6 +688,7 @@ fn batch_request(
sidecar_cache: library.sidecar_cache_dir().unwrap_or_default(), sidecar_cache: library.sidecar_cache_dir().unwrap_or_default(),
offline: library.is_offline(), offline: library.is_offline(),
gpu: gpu.cloned(), gpu: gpu.cloned(),
decoder: dr_decode::default(),
} }
} }
@@ -2539,7 +2552,7 @@ fn build_show(
window.set_filename(name.clone().into()); window.set_filename(name.clone().into());
window.set_index(i as i32); window.set_index(i as i32);
match load(gpu.as_ref(), path) { match load(gpu.as_ref(), dr_decode::default(), path) {
Ok(l) => { Ok(l) => {
window.set_load_error("".into()); window.set_load_error("".into());
let capture = window.global::<Capture>(); let capture = window.global::<Capture>();
@@ -2802,7 +2815,7 @@ fn wire_remote_open(
// transfers are under way while the UI thread is busy. // transfers are under way while the UI thread is busy.
prefetch_neighbours(&library, &prefetch, &path); prefetch_neighbours(&library, &prefetch, &path);
match load_bytes(gpu.as_ref(), &bytes) { match load_bytes(gpu.as_ref(), dr_decode::default(), &bytes) {
Ok(l) => { Ok(l) => {
w.set_load_error("".into()); w.set_load_error("".into());
let capture = w.global::<Capture>(); let capture = w.global::<Capture>();
+16 -8
View File
@@ -69,8 +69,9 @@ pub(super) fn flush_metadata(
/// otherwise tell: a header that carried no EXIF and a fetch that never /// otherwise tell: a header that carried no EXIF and a fetch that never
/// happened both leave `found` untouched, and recording the second as "this /// happened both leave `found` untouched, and recording the second as "this
/// image has no date" would let one lock mark it dateless for good. /// image has no date" would let one lock mark it dateless for good.
pub(super) async fn read_metadata_only( pub(crate) async fn read_metadata_only(
backend: &dyn RemoteBackend, backend: &dyn RemoteBackend,
decoder: &dyn dr_decode::Decoder,
req: &ThumbnailRequest, req: &ThumbnailRequest,
found: &mut Vec<MetadataFound>, found: &mut Vec<MetadataFound>,
) -> bool { ) -> bool {
@@ -86,9 +87,9 @@ pub(super) async fn read_metadata_only(
// three round trips before the sweep moves on. // three round trips before the sweep moves on.
const ATTEMPTS: usize = 3; const ATTEMPTS: usize = 3;
for attempt in 1..=ATTEMPTS { for attempt in 1..=ATTEMPTS {
match backend.get(&id, Some(0..dr_decode::HEADER_BYTES)).await { match backend.get(&id, Some(0..decoder.header_bytes())).await {
Ok(header) => { Ok(header) => {
collect_metadata(backend, &id, &header, req, found).await; collect_metadata(backend, decoder, &id, &header, req, found).await;
return true; return true;
} }
Err(e) if e.is_transient() && attempt < ATTEMPTS => { Err(e) if e.is_transient() && attempt < ATTEMPTS => {
@@ -275,6 +276,9 @@ pub(crate) const SWEEP_MAX_ORIGINAL_BYTES: u64 = 256 * 1024 * 1024;
/// and its chunked commits keep write transactions short. /// and its chunked commits keep write transactions short.
pub fn spawn_sweep(conn: Connection, catalog_path: PathBuf) -> Receiver<SweepMessage> { pub fn spawn_sweep(conn: Connection, catalog_path: PathBuf) -> Receiver<SweepMessage> {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
// TRACES: FR-RAW-2
// The one place this job names a decoder; everything below takes it.
let decoder = dr_decode::default();
std::thread::spawn(move || { std::thread::spawn(move || {
let catalog = match Catalog::open(&catalog_path) { let catalog = match Catalog::open(&catalog_path) {
@@ -355,7 +359,7 @@ pub fn spawn_sweep(conn: Connection, catalog_path: PathBuf) -> Receiver<SweepMes
let mut found = Vec::new(); let mut found = Vec::new();
let mut reached = Vec::new(); let mut reached = Vec::new();
for req in lane { for req in lane {
if read_metadata_only(backend, req, &mut found).await { if read_metadata_only(backend, decoder, req, &mut found).await {
reached.push(req.image_id); reached.push(req.image_id);
} }
} }
@@ -511,7 +515,7 @@ pub(super) fn flush_sweep(catalog: &Catalog, found: &mut Vec<MetadataFound>) {
/// ways and comparing the crops — see `examples/face_native.rs`. A tool that /// ways and comparing the crops — see `examples/face_native.rs`. A tool that
/// had to reimplement the render would be measuring its own reimplementation. /// had to reimplement the render would be measuring its own reimplementation.
pub fn render_native(gpu: &dr_gpu::GpuContext, bytes: &[u8]) -> Result<dr_export::Frame, String> { pub fn render_native(gpu: &dr_gpu::GpuContext, bytes: &[u8]) -> Result<dr_export::Frame, String> {
open_native(gpu, bytes)?.render_for_export(dr_types::ColourSpace::Srgb) open_native(gpu, dr_decode::default(), bytes)?.render_for_export(dr_types::ColourSpace::Srgb)
} }
/// The session behind [`render_native`], crate-private because `DevelopSession` /// The session behind [`render_native`], crate-private because `DevelopSession`
@@ -520,13 +524,14 @@ pub fn render_native(gpu: &dr_gpu::GpuContext, bytes: &[u8]) -> Result<dr_export
/// opening the file twice (`repairs::Fetched`). /// opening the file twice (`repairs::Fetched`).
pub(crate) fn open_native( pub(crate) fn open_native(
gpu: &dr_gpu::GpuContext, gpu: &dr_gpu::GpuContext,
decoder: &dyn dr_decode::Decoder,
bytes: &[u8], bytes: &[u8],
) -> Result<crate::develop::DevelopSession, String> { ) -> Result<crate::develop::DevelopSession, String> {
// Whatever the header says, or an empty one for a file that has none: the // Whatever the header says, or an empty one for a file that has none: the
// session takes its orientation from it, and remembers the rest for // session takes its orientation from it, and remembers the rest for
// anything that later exports from this session (FR-EXP-8). // anything that later exports from this session (FR-EXP-8).
let meta = dr_decode::metadata(bytes).unwrap_or_default(); let meta = decoder.metadata(bytes).unwrap_or_default();
crate::open_session(gpu, bytes, &meta) crate::open_session(gpu, decoder, bytes, &meta)
} }
/// The class the whole-library pass fills. /// The class the whole-library pass fills.
@@ -593,6 +598,9 @@ pub fn spawn_thumbnail_sweep(
store_dir: PathBuf, store_dir: PathBuf,
) -> Receiver<ThumbSweepMessage> { ) -> Receiver<ThumbSweepMessage> {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
// TRACES: FR-RAW-2
// The one place this job names a decoder; everything below takes it.
let decoder = dr_decode::default();
std::thread::spawn(move || { std::thread::spawn(move || {
let finish_empty = |tx: &Sender<ThumbSweepMessage>| { let finish_empty = |tx: &Sender<ThumbSweepMessage>| {
@@ -727,7 +735,7 @@ pub fn spawn_thumbnail_sweep(
} }
}; };
match fetch_preview(backend, req, &mut found).await { match fetch_preview(backend, decoder, req, &mut found).await {
PreviewOutcome::Ready(preview) => { PreviewOutcome::Ready(preview) => {
match encode_preview(file_id, &preview) { match encode_preview(file_id, &preview) {
Some(thumb) => made.push((file_id, thumb)), Some(thumb) => made.push((file_id, thumb)),
+17 -11
View File
@@ -97,6 +97,9 @@ pub fn spawn_thumbnails(
catalog_path: PathBuf, catalog_path: PathBuf,
) -> Receiver<ThumbnailMessage> { ) -> Receiver<ThumbnailMessage> {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
// TRACES: FR-RAW-2
// The one place this job names a decoder; everything below takes it.
let decoder = dr_decode::default();
std::thread::spawn(move || { std::thread::spawn(move || {
let mut store = match ThumbStore::open(&store_dir) { let mut store = match ThumbStore::open(&store_dir) {
@@ -222,7 +225,7 @@ pub fn spawn_thumbnails(
let mut offline = false; let mut offline = false;
for req in to_fetch { for req in to_fetch {
let msg = fetch_one(&*backend, store.as_mut(), &req, &mut found).await; let msg = fetch_one(&*backend, decoder, store.as_mut(), &req, &mut found).await;
offline = matches!(msg, ThumbnailMessage::Offline { .. }); offline = matches!(msg, ThumbnailMessage::Offline { .. });
// A closed channel means the window went away mid-fetch. // A closed channel means the window went away mid-fetch.
if tx.send(msg).is_err() || offline { if tx.send(msg).is_err() || offline {
@@ -247,7 +250,7 @@ pub fn spawn_thumbnails(
if tx.send(ThumbnailMessage::DateProgress).is_err() { if tx.send(ThumbnailMessage::DateProgress).is_err() {
break; break;
} }
read_metadata_only(&*backend, &req, &mut found).await; read_metadata_only(&*backend, decoder, &req, &mut found).await;
if found.len() >= FLUSH_EVERY { if found.len() >= FLUSH_EVERY {
flush_metadata(&catalog_path, &mut found, &tx); flush_metadata(&catalog_path, &mut found, &tx);
@@ -267,11 +270,12 @@ pub fn spawn_thumbnails(
pub(super) async fn fetch_one( pub(super) async fn fetch_one(
backend: &dyn RemoteBackend, backend: &dyn RemoteBackend,
decoder: &dyn dr_decode::Decoder,
store: Option<&mut ThumbStore>, store: Option<&mut ThumbStore>,
req: &ThumbnailRequest, req: &ThumbnailRequest,
found_metadata: &mut Vec<MetadataFound>, found_metadata: &mut Vec<MetadataFound>,
) -> ThumbnailMessage { ) -> ThumbnailMessage {
let preview = match fetch_preview(backend, req, found_metadata).await { let preview = match fetch_preview(backend, decoder, req, found_metadata).await {
PreviewOutcome::Ready(p) => p, PreviewOutcome::Ready(p) => p,
PreviewOutcome::Unavailable(reason) => { PreviewOutcome::Unavailable(reason) => {
return ThumbnailMessage::Unavailable { return ThumbnailMessage::Unavailable {
@@ -308,7 +312,7 @@ pub(super) async fn fetch_one(
/// ([`spawn_thumbnail_sweep`]) fetches on several lanes at once and stores the /// ([`spawn_thumbnail_sweep`]) fetches on several lanes at once and stores the
/// results on the one thread that owns the store, so it needs the pixels /// results on the one thread that owns the store, so it needs the pixels
/// *before* anything is written or addressed to a cell. /// *before* anything is written or addressed to a cell.
pub(super) enum PreviewOutcome { pub(crate) enum PreviewOutcome {
Ready(dr_decode::Preview), Ready(dr_decode::Preview),
/// This image has no usable preview. The batch continues past it. /// This image has no usable preview. The batch continues past it.
Unavailable(String), Unavailable(String),
@@ -322,8 +326,9 @@ pub(super) enum PreviewOutcome {
/// was wrong in a way that looked like corruption: fetching a fixed prefix cut /// was wrong in a way that looked like corruption: fetching a fixed prefix cut
/// the embedded JPEG partway through, and decoders render a truncated JPEG as /// the embedded JPEG partway through, and decoders render a truncated JPEG as
/// the top fraction of the frame rather than reporting an error. /// the top fraction of the frame rather than reporting an error.
pub(super) async fn fetch_preview( pub(crate) async fn fetch_preview(
backend: &dyn RemoteBackend, backend: &dyn RemoteBackend,
decoder: &dyn dr_decode::Decoder,
req: &ThumbnailRequest, req: &ThumbnailRequest,
found_metadata: &mut Vec<MetadataFound>, found_metadata: &mut Vec<MetadataFound>,
) -> PreviewOutcome { ) -> PreviewOutcome {
@@ -341,7 +346,7 @@ pub(super) async fn fetch_preview(
}; };
// Stage one: the header, enough to parse the container's IFDs. // Stage one: the header, enough to parse the container's IFDs.
let header = match backend.get(&id, Some(0..dr_decode::HEADER_BYTES)).await { let header = match backend.get(&id, Some(0..decoder.header_bytes())).await {
Ok(b) => b, Ok(b) => b,
Err(e) => return classify(e), Err(e) => return classify(e),
}; };
@@ -349,15 +354,15 @@ pub(super) async fn fetch_preview(
// The same bytes carry EXIF. Reading it here is free — the alternative is // The same bytes carry EXIF. Reading it here is free — the alternative is
// a second 256 KB fetch per image over the whole library. // a second 256 KB fetch per image over the whole library.
if req.needs_metadata { if req.needs_metadata {
collect_metadata(backend, &id, &header, req, found_metadata).await; collect_metadata(backend, decoder, &id, &header, req, found_metadata).await;
} }
// Read unconditionally, unlike the rest of the EXIF above: `needs_metadata` // Read unconditionally, unlike the rest of the EXIF above: `needs_metadata`
// is false once an image has been catalogued, but a thumbnail can still be // is false once an image has been catalogued, but a thumbnail can still be
// regenerated long after that — a cleared cache, a new size — and a // regenerated long after that — a cleared cache, a new size — and a
// thumbnail that came out upright the first time must come out upright // thumbnail that came out upright the first time must come out upright
// every time. This is a header walk, not a decode; see `dr_decode::orientation`. // every time. This is a header walk, not a decode; see `Decoder::orientation`.
let orientation = dr_decode::orientation(&header).unwrap_or_default(); let orientation = decoder.orientation(&header).unwrap_or_default();
// A plain JPEG is its own preview; anything else needs locating. // A plain JPEG is its own preview; anything else needs locating.
let bytes = if header.starts_with(&[0xFF, 0xD8, 0xFF]) { let bytes = if header.starts_with(&[0xFF, 0xD8, 0xFF]) {
@@ -366,7 +371,7 @@ pub(super) async fn fetch_preview(
Err(e) => return classify(e), Err(e) => return classify(e),
} }
} else { } else {
let Some(loc) = dr_decode::locate_preview(&header, req.size) else { let Some(loc) = decoder.locate_preview(&header, req.size) else {
// No locatable preview. Declining beats fetching the whole file: // No locatable preview. Declining beats fetching the whole file:
// that is the 370 GB path FR-NC-3 exists to avoid. // that is the 370 GB path FR-NC-3 exists to avoid.
return PreviewOutcome::Unavailable("no locatable embedded preview".into()); return PreviewOutcome::Unavailable("no locatable embedded preview".into());
@@ -474,12 +479,13 @@ pub(crate) fn store_thumbnail(
/// and the header-only pass for images whose pixels were already cached. /// and the header-only pass for images whose pixels were already cached.
pub(super) async fn collect_metadata( pub(super) async fn collect_metadata(
backend: &dyn RemoteBackend, backend: &dyn RemoteBackend,
decoder: &dyn dr_decode::Decoder,
id: &RemoteId, id: &RemoteId,
header: &[u8], header: &[u8],
req: &ThumbnailRequest, req: &ThumbnailRequest,
out: &mut Vec<MetadataFound>, out: &mut Vec<MetadataFound>,
) { ) {
let md = match dr_decode::metadata(header) { let md = match decoder.metadata(header) {
Ok(md) => md, Ok(md) => md,
Err(first) => { Err(first) => {
// A file whose IFDs follow its pixels — the linear DNG a merge // A file whose IFDs follow its pixels — the linear DNG a merge
+9 -2
View File
@@ -102,6 +102,9 @@ pub struct MergeRequest {
/// The border filler's weights, if the device has them (FR-MRG-4). /// The border filler's weights, if the device has them (FR-MRG-4).
/// `None` and the fill is not offered. /// `None` and the fill is not offered.
pub inpaint_model: Option<PathBuf>, pub inpaint_model: Option<PathBuf>,
/// TRACES: FR-RAW-2
/// What reads and decodes each frame.
pub decoder: &'static dyn dr_decode::Decoder,
} }
impl MergeRequest { impl MergeRequest {
@@ -113,6 +116,7 @@ impl MergeRequest {
feather_px: 200.0, feather_px: 200.0,
chunk: (2048, 512), chunk: (2048, 512),
inpaint_model: None, inpaint_model: None,
decoder: dr_decode::default(),
} }
} }
} }
@@ -328,8 +332,11 @@ fn run_inner(
} }
progress("Reading", i, request.frames.len()); progress("Reading", i, request.frames.len());
let bytes = &input.bytes; let bytes = &input.bytes;
let meta = dr_decode::metadata(bytes).unwrap_or_default(); let meta = request.decoder.metadata(bytes).unwrap_or_default();
let raw = dr_decode::decode(bytes).map_err(|e| format!("{}: {e}", input.name))?; let raw = request
.decoder
.decode(bytes)
.map_err(|e| format!("{}: {e}", input.name))?;
let orientation = meta.orientation.unwrap_or_default(); let orientation = meta.orientation.unwrap_or_default();
let mut graph = EditGraph::default_chain(); let mut graph = EditGraph::default_chain();
graph.set_orientation(orientation); graph.set_orientation(orientation);
+18 -8
View File
@@ -61,7 +61,7 @@ use crate::library::{FaceModelPaths, MetadataFound};
/// native render serves every face handler and the metadata one besides. /// native render serves every face handler and the metadata one besides.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Input { pub enum Input {
/// The first `dr_decode::HEADER_BYTES` of the file: EXIF, and the /// The first `Decoder::header_bytes` of the file: EXIF, and the
/// embedded preview's offset. /// embedded preview's offset.
Header, Header,
/// The whole original, undecoded. /// The whole original, undecoded.
@@ -165,15 +165,18 @@ pub struct Target {
pub struct Fetched { pub struct Fetched {
bytes: Vec<u8>, bytes: Vec<u8>,
input: Input, input: Input,
/// What opens [`Self::session`]; the job's, from its [`Toolkit`].
decoder: &'static dyn dr_decode::Decoder,
session: Option<crate::develop::DevelopSession>, session: Option<crate::develop::DevelopSession>,
frame: Option<dr_export::Frame>, frame: Option<dr_export::Frame>,
} }
impl Fetched { impl Fetched {
fn new(bytes: Vec<u8>, input: Input) -> Self { fn new(bytes: Vec<u8>, input: Input, decoder: &'static dyn dr_decode::Decoder) -> Self {
Self { Self {
bytes, bytes,
input, input,
decoder,
session: None, session: None,
frame: None, frame: None,
} }
@@ -194,8 +197,10 @@ impl Fetched {
return Err(Failure::Other("only the header was fetched".into())); return Err(Failure::Other("only the header was fetched".into()));
} }
if self.session.is_none() { if self.session.is_none() {
self.session = self.session = Some(
Some(crate::library::open_native(gpu, &self.bytes).map_err(Failure::Unreadable)?); crate::library::open_native(gpu, self.decoder, &self.bytes)
.map_err(Failure::Unreadable)?,
);
} }
Ok(self.session.as_mut().expect("just opened")) Ok(self.session.as_mut().expect("just opened"))
} }
@@ -234,6 +239,9 @@ pub struct Toolkit {
pub model_id: String, pub model_id: String,
pub store: ThumbStore, pub store: ThumbStore,
pub options: dr_face::DetectOptions, pub options: dr_face::DetectOptions,
/// TRACES: FR-RAW-2
/// What reads headers and opens originals for every handler.
pub decoder: &'static dyn dr_decode::Decoder,
} }
impl Toolkit { impl Toolkit {
@@ -692,12 +700,12 @@ fn crop(
/// marked examined too, so it is not fetched again -- the state the sweep /// marked examined too, so it is not fetched again -- the state the sweep
/// records for the dateless, for the same reason. /// records for the dateless, for the same reason.
fn metadata( fn metadata(
_tk: &mut Toolkit, tk: &mut Toolkit,
catalog: &Catalog, catalog: &Catalog,
target: &Target, target: &Target,
fetched: &mut Fetched, fetched: &mut Fetched,
) -> Result<usize, Failure> { ) -> Result<usize, Failure> {
let found = match dr_decode::metadata(fetched.bytes()) { let found = match tk.decoder.metadata(fetched.bytes()) {
Ok(md) => MetadataFound { Ok(md) => MetadataFound {
image_id: target.image.0 as i64, image_id: target.image.0 as i64,
captured_at: md.captured_at, captured_at: md.captured_at,
@@ -1065,6 +1073,7 @@ pub fn spawn(
model_id: model_id.clone(), model_id: model_id.clone(),
store, store,
options, options,
decoder: dr_decode::default(),
}; };
let repairs = registry(scope, &model_id, detector, caps); let repairs = registry(scope, &model_id, detector, caps);
@@ -1144,6 +1153,7 @@ pub fn spawn(
// photograph. Every file borrowed is given back (ARCH §9.0a). // photograph. Every file borrowed is given back (ARCH §9.0a).
let pool = dr_sync_folder::BorrowPool::new(); let pool = dr_sync_folder::BorrowPool::new();
let header_bytes = tk.decoder.header_bytes();
for chunk in planned.chunks(LANES) { for chunk in planned.chunks(LANES) {
let fetched = crate::library::futures_join_all(chunk.iter().map(|p| { let fetched = crate::library::futures_join_all(chunk.iter().map(|p| {
let backend = &*backend; let backend = &*backend;
@@ -1151,7 +1161,7 @@ pub fn spawn(
async move { async move {
let id = RemoteId::Path(RemotePath::new(&p.target.path)); let id = RemoteId::Path(RemotePath::new(&p.target.path));
let got = if p.input == Input::Header { let got = if p.input == Input::Header {
backend.get(&id, Some(0..dr_decode::HEADER_BYTES)).await backend.get(&id, Some(0..header_bytes)).await
} else { } else {
let held = match pool let held = match pool
.borrow(backend, &RemotePath::new(&p.target.path)) .borrow(backend, &RemotePath::new(&p.target.path))
@@ -1185,7 +1195,7 @@ pub fn spawn(
} }
}; };
let mut fetched = Fetched::new(bytes, p.input); let mut fetched = Fetched::new(bytes, p.input, tk.decoder);
let mut wrote = 0usize; let mut wrote = 0usize;
let mut broke = false; let mut broke = false;
for (repair, set) in repairs.iter().zip(sets.iter()) { for (repair, set) in repairs.iter().zip(sets.iter()) {