diff --git a/core/dr-decode/src/decoder.rs b/core/dr-decode/src/decoder.rs new file mode 100644 index 0000000..ca46877 --- /dev/null +++ b/core/dr-decode/src/decoder.rs @@ -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; + + /// 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; + + /// 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; + + /// 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; + + /// Sensor data, for develop and export. The expensive path. + fn decode(&self, bytes: &[u8]) -> Result; +} + +/// 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 { + crate::metadata(bytes) + } + + fn orientation(&self, header: &[u8]) -> Option { + crate::orientation(header) + } + + fn locate_preview(&self, header: &[u8], file_len: u64) -> Option { + crate::locate_preview(header, file_len) + } + + fn preview(&self, bytes: &[u8], size: PreviewSize) -> Result { + crate::extract_preview(bytes, size) + } + + fn decode(&self, bytes: &[u8]) -> Result { + 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)); + } +} diff --git a/core/dr-decode/src/lib.rs b/core/dr-decode/src/lib.rs index ddb5888..2334f84 100644 --- a/core/dr-decode/src/lib.rs +++ b/core/dr-decode/src/lib.rs @@ -11,14 +11,20 @@ //! //! Fusing them would force a full decode where a header read suffices, which //! 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; +mod decoder; mod error; mod locate; mod preview; pub mod profile; pub use base_curve::BaseCurve; +pub use decoder::{default, Decoder, Rawler}; pub use error::DecodeError; pub use locate::{ defects, is_complete_jpeg, jpeg_metadata, locate_preview, tiff_metadata, BadLine, BadPixel,