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
+25 -12
View File
@@ -71,7 +71,7 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use anyhow::Result;
use dr_decode::{Metadata, PreviewSize};
use dr_decode::{Decoder, Metadata, PreviewSize};
use slint::ComponentHandle as _;
pub use develop::DevelopSession;
@@ -198,7 +198,11 @@ struct Loaded {
/// (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
/// 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
// (ARCH §9.0). Say so plainly rather than reporting a decode failure.
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())?;
load_bytes(ctx, &bytes)
load_bytes(ctx, decoder, &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
/// as a WebDAV response body, and writing it to disk purely to read it back
/// would be a round-trip for nothing.
fn load_bytes(ctx: Option<&dr_gpu::GpuContext>, bytes: &[u8]) -> Result<Loaded, String> {
let meta = dr_decode::metadata(bytes).unwrap_or_default();
fn load_bytes(
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
// 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();
if let Some(ctx) = ctx {
match open_session(ctx, bytes, &meta) {
match open_session(ctx, decoder, bytes, &meta) {
Ok(session) => {
let (width, height) = session.source_size();
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
// could open for editing. Read-only, and the adjust panel is disabled.
let mut preview =
dr_decode::extract_preview(bytes, PreviewSize::Screen).map_err(|e| e.to_string())?;
let mut preview = decoder
.preview(bytes, PreviewSize::Screen)
.map_err(|e| e.to_string())?;
preview.downscale_to(MAX_DISPLAY_DIM);
// 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
@@ -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.
pub(crate) fn open_session(
ctx: &dr_gpu::GpuContext,
decoder: &dyn Decoder,
bytes: &[u8],
meta: &Metadata,
) -> Result<DevelopSession, String> {
// How the file stored its pixels. A file that says nothing is taken as
// 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
// 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.
fn open_pixels(
ctx: &dr_gpu::GpuContext,
decoder: &dyn Decoder,
bytes: &[u8],
orientation: dr_types::Orientation,
) -> Result<DevelopSession, String> {
@@ -338,7 +349,8 @@ fn open_pixels(
// A failure here is expected for bodies rawler does not know, and must not
// stop the image displaying (FR-RAW-4).
dr_decode::decode(bytes)
decoder
.decode(bytes)
.map_err(|e| e.to_string())
.and_then(|raw| DevelopSession::open(ctx, &raw, orientation))
}
@@ -676,6 +688,7 @@ fn batch_request(
sidecar_cache: library.sidecar_cache_dir().unwrap_or_default(),
offline: library.is_offline(),
gpu: gpu.cloned(),
decoder: dr_decode::default(),
}
}
@@ -2539,7 +2552,7 @@ fn build_show(
window.set_filename(name.clone().into());
window.set_index(i as i32);
match load(gpu.as_ref(), path) {
match load(gpu.as_ref(), dr_decode::default(), path) {
Ok(l) => {
window.set_load_error("".into());
let capture = window.global::<Capture>();
@@ -2802,7 +2815,7 @@ fn wire_remote_open(
// transfers are under way while the UI thread is busy.
prefetch_neighbours(&library, &prefetch, &path);
match load_bytes(gpu.as_ref(), &bytes) {
match load_bytes(gpu.as_ref(), dr_decode::default(), &bytes) {
Ok(l) => {
w.set_load_error("".into());
let capture = w.global::<Capture>();