Put the RAW decoder behind a Decoder trait

FR-RAW-2 says a second decoder may be added for broader camera coverage
without changing callers, and D2 names LibRaw as that second decoder.
Nothing tested the claim: dr_decode was one decoder reached through free
functions, so adding another would have meant editing every caller at
the moment there was most pressure not to.

Decoder is an object-safe trait over bytes: header_bytes, metadata,
orientation, locate_preview, preview and decode. Rawler implements it by
delegating to the existing free functions, so behaviour is unchanged,
and dr_decode::default() hands it out as a &'static dyn Decoder, which
is what the places that start work will name. JPEG recognition, decoding
and completeness checks stay free functions: they are not a RAW
decoder's to vary.

Nothing in the trait takes a path or a SourceRef; the decoder states how
much of a file it needs and where its preview sits, and the caller's
storage fetches that.
This commit is contained in:
2026-09-24 21:26:17 -04:00
parent e8f68a92f8
commit d8fb382ce9
2 changed files with 126 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
//! TRACES: FR-RAW-2
//! The seam a second decoder plugs into.
//!
//! D2 keeps LibRaw as the fallback for bodies rawler does not cover. Adding
//! it later should be a new `impl Decoder`, not an edit to every caller that
//! reads a header, cuts a thumbnail or opens a photograph for export — which
//! is what the free functions alone would have made it. So the callers take a
//! `&dyn Decoder`, and only the places that start a job name [`default`].
//!
//! Bytes in, always. Nothing here takes a path or a `SourceRef`: resolving a
//! file to bytes is `Storage`'s job at the caller, so the same decoder serves a
//! local file, an Android document and a range fetched from Nextcloud. The
//! decoder's part in that is to say how much of a file it needs
//! ([`Decoder::header_bytes`]) and where its preview sits
//! ([`Decoder::locate_preview`]); the storage layer fetches exactly that.
//!
//! What stays a free function is what is not a decoder's to vary: recognising
//! a JPEG ([`crate::probe`]), decoding one ([`crate::decode_jpeg`]) and
//! checking one is whole ([`crate::is_complete_jpeg`]). A second RAW decoder
//! would not read a JPEG differently.
use dr_types::Orientation;
use crate::{DecodeError, Metadata, Preview, PreviewLocation, PreviewSize, RawImage};
/// TRACES: FR-RAW-2
/// A RAW decoder, over bytes.
///
/// Object-safe so a caller can hold `&dyn Decoder` without becoming generic,
/// and `Send + Sync` because the callers that need one most — the thumbnail
/// lanes, the export worker — run off the UI thread.
pub trait Decoder: Send + Sync {
/// How much of the start of a file [`Self::metadata`] and
/// [`Self::locate_preview`] need. A caller reading over a network fetches
/// this range and no more.
fn header_bytes(&self) -> u64;
/// Capture metadata, from a header or a whole file, without touching
/// sensor data.
fn metadata(&self, bytes: &[u8]) -> Result<Metadata, DecodeError>;
/// How the stored pixels are turned, from a header. `None` where the file
/// does not say, which callers take as upright.
fn orientation(&self, header: &[u8]) -> Option<Orientation>;
/// Where the embedded preview best suited to a thumbnail sits in the file,
/// from its header, so a remote caller can fetch that range alone.
fn locate_preview(&self, header: &[u8], file_len: u64) -> Option<PreviewLocation>;
/// The embedded preview at the size asked for, falling through the ladder
/// to the next size where the file lacks it.
fn preview(&self, bytes: &[u8], size: PreviewSize) -> Result<Preview, DecodeError>;
/// Sensor data, for develop and export. The expensive path.
fn decode(&self, bytes: &[u8]) -> Result<RawImage, DecodeError>;
}
/// TRACES: FR-RAW-2
/// The decoder the application ships: rawler for sensor data and the
/// previews it knows, DarkRoom's own container walk for headers and ranges.
///
/// Its methods are the crate's free functions, unchanged. They stay public
/// for the tools and examples that read one file and have no caller to keep
/// decoder-agnostic.
#[derive(Debug, Clone, Copy, Default)]
pub struct Rawler;
impl Decoder for Rawler {
fn header_bytes(&self) -> u64 {
crate::HEADER_BYTES
}
fn metadata(&self, bytes: &[u8]) -> Result<Metadata, DecodeError> {
crate::metadata(bytes)
}
fn orientation(&self, header: &[u8]) -> Option<Orientation> {
crate::orientation(header)
}
fn locate_preview(&self, header: &[u8], file_len: u64) -> Option<PreviewLocation> {
crate::locate_preview(header, file_len)
}
fn preview(&self, bytes: &[u8], size: PreviewSize) -> Result<Preview, DecodeError> {
crate::extract_preview(bytes, size)
}
fn decode(&self, bytes: &[u8]) -> Result<RawImage, DecodeError> {
crate::decode(bytes)
}
}
/// TRACES: FR-RAW-2
/// The decoder a job uses unless it was handed another.
///
/// Named by the places that start work — a thread, a UI handler — and by
/// nothing below them. Returning `&'static dyn Decoder` rather than `Rawler`
/// is the point: a caller that only has this cannot reach past the trait.
pub fn default() -> &'static dyn Decoder {
static RAWLER: Rawler = Rawler;
&RAWLER
}
#[cfg(test)]
mod tests {
use super::*;
/// The default is the shipped decoder, reached through the trait: same
/// header budget, and the same answer to bytes neither can read.
#[test]
fn the_default_is_rawler_behind_the_trait() {
let d = default();
assert_eq!(d.header_bytes(), crate::HEADER_BYTES);
let junk = [0u8; 64];
assert_eq!(d.metadata(&junk).is_err(), crate::metadata(&junk).is_err());
assert!(d.decode(&junk).is_err());
assert_eq!(d.orientation(&junk), crate::orientation(&junk));
}
}
+6
View File
@@ -11,14 +11,20 @@
//! //!
//! Fusing them would force a full decode where a header read suffices, which //! Fusing them would force a full decode where a header read suffices, which
//! is exactly why Lightroom stalls ~2 s per image during culling. //! is exactly why Lightroom stalls ~2 s per image during culling.
//!
//! Callers reach these through the [`Decoder`] trait rather than by name, so a
//! second decoder can be put behind them without changing any of them
//! (FR-RAW-2). [`Rawler`] is the one that ships; [`default`] hands it out.
pub mod base_curve; pub mod base_curve;
mod decoder;
mod error; mod error;
mod locate; mod locate;
mod preview; mod preview;
pub mod profile; pub mod profile;
pub use base_curve::BaseCurve; pub use base_curve::BaseCurve;
pub use decoder::{default, Decoder, Rawler};
pub use error::DecodeError; pub use error::DecodeError;
pub use locate::{ pub use locate::{
defects, is_complete_jpeg, jpeg_metadata, locate_preview, tiff_metadata, BadLine, BadPixel, defects, is_complete_jpeg, jpeg_metadata, locate_preview, tiff_metadata, BadLine, BadPixel,