Skip to main content

zencodec/traits/
decoding.rs

1//! Decoder configuration and decode jobs.
2
3use alloc::borrow::Cow;
4use alloc::boxed::Box;
5
6use crate::estimate::{ComputeEnvironment, ImageCharacteristics, ResourceEstimate};
7use crate::format::ImageFormat;
8use crate::orientation::OrientationHint;
9use crate::{DecodeCapabilities, DecodeOutput, ImageInfo, OutputInfo, ResourceLimits, StopToken};
10use zenpixels::PixelDescriptor;
11
12use super::BoxedError;
13use super::decoder::{AnimationFrameDecoder, Decode, StreamingDecode};
14use super::dyn_decoding::{
15    AnimationFrameDecoderShim, DecoderShim, DynAnimationFrameDecoder, DynDecoder,
16    DynStreamingDecoder, StreamingDecoderShim,
17};
18
19// ===========================================================================
20// Decoder configuration
21// ===========================================================================
22
23/// Reusable decoder configuration.
24///
25/// Implemented by each codec's config type. Config types are `Clone + Send +
26/// Sync` with no lifetimes.
27///
28/// Probing lives on [`DecodeJob`], not here, because probing needs limits
29/// and cancellation context.
30pub trait DecoderConfig: Clone + Send + Sync {
31    /// The codec-specific error type.
32    type Error: core::error::Error + Send + Sync + 'static;
33
34    /// Per-operation job type.
35    ///
36    /// The GAT lifetime `'a` is the data lifetime — it becomes concrete
37    /// when the caller passes data to [`DecodeJob::decoder`].
38    type Job<'a>: DecodeJob<'a, Error = Self::Error> + 'static;
39
40    /// The image formats this decoder handles.
41    ///
42    /// A single-format decoder returns one element. Multi-format decoders
43    /// (e.g., a bitmap crate handling BMP, ICO, CUR, TGA, PNM) return all
44    /// formats they can decode. The dispatch layer registers this config
45    /// for each format in the list.
46    ///
47    /// Must not be empty.
48    fn formats() -> &'static [ImageFormat];
49
50    /// Pixel formats this decoder can produce natively.
51    ///
52    /// Every descriptor is a guarantee: the decoder can produce this format
53    /// without lossy conversion. Must not be empty.
54    fn supported_descriptors() -> &'static [PixelDescriptor];
55
56    /// Decoder capabilities (metadata support, cancellation, etc.).
57    ///
58    /// Returns a static reference describing what this decoder supports.
59    fn capabilities() -> &'static DecodeCapabilities {
60        &DecodeCapabilities::EMPTY
61    }
62
63    /// Predict peak memory, wall time, and CPU-core scaling for decoding an
64    /// image with the given [`ImageCharacteristics`] on the `compute`
65    /// environment.
66    ///
67    /// The default is [`ResourceEstimate::unknown`] — every field `None`,
68    /// i.e. "this codec does not model its resource use." Codecs with a
69    /// calibrated decode `heuristics` model override this (a quick option is
70    /// [`ResourceEstimate::conservative`]`(image).at_cores(compute.cores())`).
71    fn estimate_decode_resources(
72        &self,
73        image: &ImageCharacteristics,
74        compute: &ComputeEnvironment,
75    ) -> ResourceEstimate {
76        let _ = (image, compute);
77        ResourceEstimate::unknown()
78    }
79
80    /// Create a per-operation job, consuming the config.
81    ///
82    /// The job owns the config and all configuration set on it.
83    /// The lifetime parameter is the data lifetime, inferred from usage.
84    fn job<'a>(self) -> Self::Job<'a>;
85
86    /// One-shot convenience: decode `data` to owned pixels in the decoder's
87    /// native pixel format, with default job settings.
88    ///
89    /// Equivalent to `self.job().decoder(Cow::Borrowed(data), &[])?.decode()`.
90    /// For limits, cancellation, decode hints, or pixel-format preferences, go
91    /// through [`job()`](DecoderConfig::job).
92    ///
93    /// ```rust,ignore
94    /// let image = zenjpeg::JpegDecoderConfig::new().decode(&bytes)?;
95    /// ```
96    fn decode(self, data: &[u8]) -> Result<DecodeOutput, Self::Error>
97    where
98        Self: Sized,
99    {
100        self.job().decoder(Cow::Borrowed(data), &[])?.decode()
101    }
102
103    /// One-shot convenience: probe image metadata (header parse only, cheap)
104    /// with default job settings.
105    ///
106    /// Equivalent to `self.clone().job().probe(data)`. For limits or
107    /// cancellation context, go through [`job()`](DecoderConfig::job).
108    ///
109    /// ```rust,ignore
110    /// let info = zenjpeg::JpegDecoderConfig::new().probe(&bytes)?;
111    /// println!("{}x{}", info.width, info.height);
112    /// ```
113    fn probe(&self, data: &[u8]) -> Result<ImageInfo, Self::Error>
114    where
115        Self: Sized,
116    {
117        self.clone().job().probe(data)
118    }
119}
120
121// ===========================================================================
122// Decode job
123// ===========================================================================
124
125/// Per-operation decode job.
126///
127/// Created by [`DecoderConfig::job()`]. Holds limits, cancellation, and
128/// decode hints. Probing lives here because it needs the limits/stop context.
129///
130/// # Decode hints
131///
132/// Hints let the caller request spatial transforms (crop, scale, orientation)
133/// that the decoder may apply during decode. The decoder is free to ignore
134/// any hint. Call [`output_info()`](DecodeJob::output_info) after setting
135/// hints to learn what the decoder will actually produce.
136pub trait DecodeJob<'a>: Sized {
137    /// The codec-specific error type.
138    type Error: core::error::Error + Send + Sync + 'static;
139
140    /// Single-image decoder type.
141    type Dec: Decode<Error = Self::Error>;
142
143    /// Streaming decoder type.
144    ///
145    /// Implements [`StreamingDecode`] for batch/scanline-level decode.
146    /// Set to `()` if the codec does not support streaming decode.
147    type StreamDec: StreamingDecode<Error = Self::Error> + Send;
148
149    /// Full-frame animation decoder type.
150    ///
151    /// Must be `'static` and `Send` — frame decoders own their data (typically by
152    /// copying the input slice at construction time). This lets callers
153    /// drop the input buffer while still iterating frames, and use decoders
154    /// across thread boundaries (e.g., in pipeline `Source` implementations).
155    type AnimationFrameDec: AnimationFrameDecoder<Error = Self::Error> + Send + 'static;
156
157    /// Legacy cancellation-only adapter. Prefer the complete operation pulse.
158    #[deprecated(note = "use with_pulse; wrap cancellation-only controls in how_far::StopOnly")]
159    fn with_stop(self, stop: StopToken) -> Self {
160        self.with_pulse(how_far::SharedPulse::new(how_far::StopOnly::new(stop)))
161    }
162
163    /// Retain progress, cancellation, phase planning and sharing for this job.
164    /// Codecs must not erase this to a cancellation-only token.
165    fn with_pulse(self, pulse: how_far::SharedPulse) -> Self;
166
167    /// Override resource limits for this operation.
168    fn with_limits(self, limits: ResourceLimits) -> Self;
169
170    /// Set decode security policy (controls metadata extraction, parsing strictness, etc.).
171    ///
172    /// # Note
173    ///
174    /// The default implementation is a no-op. This published setter is
175    /// best-effort; static capability flags do not confirm policy enforcement.
176    /// Use [`try_set_policy`](Self::try_set_policy) or
177    /// [`try_with_policy`](Self::try_with_policy) for a required policy.
178    #[deprecated(
179        note = "may silently ignore policy; use try_with_policy or a checked operation request"
180    )]
181    fn with_policy(self, _policy: crate::DecodePolicy) -> Self {
182        self
183    }
184
185    /// Apply every specified policy field, or reject without changing this job.
186    ///
187    /// The default accepts only [`DecodePolicy::none()`](crate::DecodePolicy::none)
188    /// as a no-op. Codecs override this single primitive once they can enforce
189    /// the requested fields; type-erased jobs delegate to the same primitive.
190    /// Acceptance does not guarantee that content-dependent processing succeeds.
191    fn try_set_policy(
192        &mut self,
193        policy: crate::DecodePolicy,
194    ) -> Result<(), crate::UnsupportedOperation> {
195        if policy != crate::DecodePolicy::none() {
196            return Err(crate::UnsupportedOperation::DecodePolicy);
197        }
198        Ok(())
199    }
200
201    /// Builder form of [`try_set_policy`](Self::try_set_policy).
202    ///
203    /// A rejection consumes this builder. Use the mutable setter to retain it.
204    fn try_with_policy(
205        mut self,
206        policy: crate::DecodePolicy,
207    ) -> Result<Self, crate::UnsupportedOperation> {
208        self.try_set_policy(policy)?;
209        Ok(self)
210    }
211
212    // --- Probing (needs limits + stop context) ---
213
214    /// Probe image metadata cheaply (header parse only).
215    ///
216    /// O(header), not O(pixels). Parses container headers to extract
217    /// dimensions, format, and basic metadata. May not return frame
218    /// counts or data requiring a full parse.
219    fn probe(&self, data: &[u8]) -> Result<ImageInfo, Self::Error>;
220
221    /// Probe image metadata with a full parse.
222    ///
223    /// May be expensive (e.g., parsing all GIF frames to count them).
224    /// Returns complete metadata including frame counts.
225    ///
226    /// Default: delegates to [`probe()`](DecodeJob::probe).
227    fn probe_full(&self, data: &[u8]) -> Result<ImageInfo, Self::Error> {
228        self.probe(data)
229    }
230
231    /// Map every structural part of `data` without decoding pixels.
232    ///
233    /// The returned [`Inventory`](crate::inventory::Inventory) attributes every
234    /// byte of the input to a part — segment, chunk, box, item extent, block,
235    /// trailer or gap — and says what this decoder does with it
236    /// ([`Disposition`](crate::inventory::Disposition)). Bytes the decoder never
237    /// reads are listed, not skipped, so the inventory can be used to audit a
238    /// file for metadata that would slip through a pipeline.
239    ///
240    /// Malformed or truncated input still yields an inventory: record what was
241    /// understood and mark the rest
242    /// [`Malformed`](crate::inventory::Disposition::Malformed). Reserve errors
243    /// for cancellation and resource limits. The result must pass
244    /// [`Inventory::validate`](crate::inventory::Inventory::validate).
245    ///
246    /// Default: an [`UnsupportedOperation::Inventory`](crate::UnsupportedOperation::Inventory)
247    /// error. Codecs gate the walker behind a default-off `inventory` feature;
248    /// this method stays callable when that feature is disabled. A codec that
249    /// enables it also sets
250    /// [`DecodeCapabilities::with_inventory`](crate::decode::DecodeCapabilities::with_inventory).
251    /// The shared error envelope lets this default work without changing the
252    /// published bounds on [`Self::Error`]. Implementations preserve native
253    /// errors as its detail and retain location traces.
254    fn inventory(
255        &self,
256        data: &[u8],
257    ) -> Result<crate::inventory::Inventory, crate::At<crate::CodecError>> {
258        let _ = data;
259        Err(crate::inventory::unsupported())
260    }
261
262    // --- Decode hints (optional, decoder may ignore) ---
263
264    /// Hint: crop to this region in source coordinates.
265    ///
266    /// The decoder may adjust for block alignment (JPEG MCU boundaries).
267    fn with_crop_hint(self, _x: u32, _y: u32, _width: u32, _height: u32) -> Self {
268        self
269    }
270
271    /// Set orientation handling strategy.
272    ///
273    /// See [`OrientationHint`] for the available strategies.
274    /// Default: [`OrientationHint::Preserve`].
275    fn with_orientation(self, _hint: OrientationHint) -> Self {
276        self
277    }
278
279    /// Hint: start decoding from a specific frame (0-based).
280    ///
281    /// For animation formats, the decoder seeks to the nearest keyframe
282    /// at or before `index` and composites forward to produce the
283    /// requested frame as the first yielded result.
284    ///
285    /// Only meaningful before [`animation_frame_decoder()`](DecodeJob::animation_frame_decoder).
286    fn with_start_frame_index(self, _index: u32) -> Self {
287        self
288    }
289
290    /// Opt in to supplementary gain map extraction.
291    ///
292    /// When true, codecs that support gain maps (AVIF tmap, JXL jhgm,
293    /// JPEG UltraHDR) extract and attach gain map data to decode output
294    /// extras. Typically set after probing reveals a gain map is present.
295    ///
296    /// This is the 2-way toggle; `with_extract_gain_map(true)` is equivalent to
297    /// [`with_gain_map_render`](Self::with_gain_map_render) with
298    /// [`GainMapRender::Components`](crate::decode::GainMapRender::Components). Prefer
299    /// `with_gain_map_render` when you also want the HDR-reconstruction intent.
300    fn with_extract_gain_map(self, _extract: bool) -> Self {
301        self
302    }
303
304    /// Select how a gain-map (HDR) image is rendered — the caller's intent for what
305    /// `decode()` produces (SDR base, surfaced components, or reconstructed HDR).
306    ///
307    /// Default is a no-op (the codec's own default, normally
308    /// [`GainMapRender::BaseOnly`](crate::decode::GainMapRender)). Codecs that
309    /// support gain maps override this; check the codec's
310    /// [`DecodeCapabilities`](crate::decode::DecodeCapabilities) —
311    /// [`gain_map`](crate::decode::DecodeCapabilities::gain_map) for
312    /// `Components`/extraction, and
313    /// [`reconstructs_hdr`](crate::decode::DecodeCapabilities::reconstructs_hdr) for
314    /// [`ReconstructHdr`](crate::decode::GainMapRender::ReconstructHdr). zencodec
315    /// carries no HDR math: a codec that can't reconstruct surfaces `Components` (or
316    /// errors), and the apply happens one layer up (`ultrahdr-core`).
317    fn with_gain_map_render(self, _render: crate::GainMapRender) -> Self {
318        self
319    }
320
321    /// Access codec-specific extensions for this job.
322    ///
323    /// Returns a reference to a `'static` extension type stored inside
324    /// the job. Callers downcast via `Any::downcast_ref` to the codec's
325    /// extension type. Returns `None` if the codec has no extensions.
326    fn extensions(&self) -> Option<&dyn core::any::Any> {
327        None
328    }
329
330    /// Mutable access to codec-specific extensions.
331    fn extensions_mut(&mut self) -> Option<&mut dyn core::any::Any> {
332        None
333    }
334
335    // --- Output prediction ---
336
337    /// Predict what the decoder will produce given current hints.
338    ///
339    /// Returns dimensions, pixel format, and which hints were honored.
340    /// Call after setting hints, before creating a decoder. This method receives
341    /// no output preferences: `native_format` is not proof of the format selected
342    /// by a subsequent negotiated decode. Do not use it to reject a requested
343    /// format that differs from the native default.
344    fn output_info(&self, data: &[u8]) -> Result<OutputInfo, Self::Error>;
345
346    // --- Executor creation ---
347    //
348    // All executors bind `data` here so the DecodeJob is the single
349    // place where input is provided. This keeps Decode/StreamingDecode/
350    // AnimationFrameDecoder free of data parameters, and prepares for future
351    // IO-read sources (the job can bind a reader instead of a slice).
352    //
353    // Consistent parameter order: data, [sink], preferred.
354
355    /// Create a one-shot decoder bound to `data`.
356    ///
357    /// The decoder stores the [`Cow`] and borrows from it via [`core::ops::Deref`].
358    /// Pass `Cow::Borrowed(&slice)` for zero-copy slice access, or
359    /// `Cow::Owned(vec)` to donate a buffer (avoids a copy in codecs
360    /// that need owned data internally).
361    ///
362    /// `preferred` is a ranked list of desired output formats. The decoder
363    /// picks the first it can produce without lossy conversion. Pass `&[]`
364    /// for the decoder's native format.
365    fn decoder(
366        self,
367        data: Cow<'a, [u8]>,
368        preferred: &[PixelDescriptor],
369    ) -> Result<Self::Dec, Self::Error>;
370
371    /// Decode directly into a caller-owned sink (push model).
372    ///
373    /// Calls [`begin()`](crate::DecodeRowSink::begin) once, then pushes
374    /// strips via [`provide_next_buffer()`](crate::DecodeRowSink::provide_next_buffer),
375    /// then calls [`finish()`](crate::DecodeRowSink::finish).
376    /// Returns [`OutputInfo`] describing what was produced.
377    ///
378    /// `preferred` is a ranked list of desired output formats.
379    ///
380    /// Codecs with native row/strip streaming should write decoded rows
381    /// directly into the sink. Codecs that can only do one-shot decode
382    /// should call [`zencodec::helpers::copy_decode_to_sink()`](crate::helpers::copy_decode_to_sink) as a fallback.
383    fn push_decoder(
384        self,
385        data: Cow<'a, [u8]>,
386        sink: &mut dyn crate::DecodeRowSink,
387        preferred: &[PixelDescriptor],
388    ) -> Result<OutputInfo, Self::Error>;
389
390    /// Create a streaming decoder that yields scanline batches.
391    ///
392    /// Returns an error if the codec does not support streaming decode.
393    ///
394    /// `preferred` is a ranked list of desired output formats.
395    ///
396    /// See [`StreamingDecode`] for the batch pull API.
397    fn streaming_decoder(
398        self,
399        data: Cow<'a, [u8]>,
400        preferred: &[PixelDescriptor],
401    ) -> Result<Self::StreamDec, Self::Error>;
402
403    /// Create a full-frame animation decoder.
404    ///
405    /// The decoder composites internally and yields full-canvas frames.
406    /// The decoder calls [`Cow::into_owned()`] to take ownership of the
407    /// data (required because `AnimationFrameDec: 'static`). When the caller
408    /// passes `Cow::Owned(vec)`, this is a free move with no copy.
409    ///
410    /// `preferred` is a ranked list of desired output formats.
411    fn animation_frame_decoder(
412        self,
413        data: Cow<'a, [u8]>,
414        preferred: &[PixelDescriptor],
415    ) -> Result<Self::AnimationFrameDec, Self::Error>;
416
417    // --- Type-erased convenience methods ---
418
419    /// Create a type-erased one-shot decoder.
420    ///
421    /// Returns a boxed closure that decodes to owned pixels. All hints
422    /// and preferences are bound before this call.
423    ///
424    /// # Example
425    ///
426    /// ```rust,ignore
427    /// let decode = config.job()
428    ///     .with_crop_hint(0, 0, 800, 600)
429    ///     .dyn_decoder(data, &[PixelDescriptor::rgb8()])?;
430    ///
431    /// let output: DecodeOutput = decode.decode()?;
432    /// ```
433    fn dyn_decoder(
434        self,
435        data: Cow<'a, [u8]>,
436        preferred: &[PixelDescriptor],
437    ) -> Result<Box<dyn DynDecoder + 'a>, BoxedError>
438    where
439        Self: 'a,
440    {
441        let dec = self
442            .decoder(data, preferred)
443            .map_err(|e| Box::new(e) as BoxedError)?;
444        Ok(Box::new(DecoderShim(dec)))
445    }
446
447    /// Create a type-erased full-frame animation decoder.
448    ///
449    /// # Example
450    ///
451    /// ```rust,ignore
452    /// let mut dec = config.job()
453    ///     .dyn_animation_frame_decoder(data, &[])?;
454    ///
455    /// while let Some(frame) = dec.render_next_frame_owned(None)? {
456    ///     // process frame
457    /// }
458    /// ```
459    fn dyn_animation_frame_decoder(
460        self,
461        data: Cow<'a, [u8]>,
462        preferred: &[PixelDescriptor],
463    ) -> Result<Box<dyn DynAnimationFrameDecoder>, BoxedError>
464    where
465        Self: 'a,
466        Self::AnimationFrameDec: Send,
467    {
468        let dec = self
469            .animation_frame_decoder(data, preferred)
470            .map_err(|e| Box::new(e) as BoxedError)?;
471        Ok(Box::new(AnimationFrameDecoderShim(dec)))
472    }
473
474    /// Create a type-erased streaming decoder.
475    ///
476    /// # Example
477    ///
478    /// ```rust,ignore
479    /// let mut dec = config.job()
480    ///     .dyn_streaming_decoder(data, &[])?;
481    ///
482    /// while let Some((y, strip)) = dec.next_batch()? {
483    ///     // process strip
484    /// }
485    /// ```
486    fn dyn_streaming_decoder(
487        self,
488        data: Cow<'a, [u8]>,
489        preferred: &[PixelDescriptor],
490    ) -> Result<Box<dyn DynStreamingDecoder + 'a>, BoxedError>
491    where
492        Self: 'a,
493        Self::StreamDec: Send,
494    {
495        let dec = self
496            .streaming_decoder(data, preferred)
497            .map_err(|e| Box::new(e) as BoxedError)?;
498        Ok(Box::new(StreamingDecoderShim(dec)))
499    }
500
501    /// Create a type-erased incremental (Sans-I/O) decoder.
502    ///
503    /// Codecs that support incremental decoding override this method.
504    /// Default implementation returns [`UnsupportedOperation::IncrementalDecode`](crate::UnsupportedOperation::IncrementalDecode).
505    ///
506    /// The returned decoder is built with
507    /// [`erase_incremental`](crate::decode::erase_incremental), so the
508    /// termination rules hold whatever the codec does. Drivers for files,
509    /// streams and async sources are in `zencodec-io`; this loop is the
510    /// whole protocol, over bytes already in memory:
511    ///
512    /// ```
513    /// use zencodec::decode::{BoxedError, DecodeJob, DecodeRowSink, Event, Input, InputRequest};
514    ///
515    /// fn decode_in_memory<'a, J: DecodeJob<'a>>(
516    ///     job: J,
517    ///     data: &[u8],
518    ///     sink: &mut dyn DecodeRowSink,
519    /// ) -> Result<(), BoxedError> {
520    ///     let mut dec = job.dyn_incremental_decoder(&[])?;
521    ///     let total = data.len() as u64;
522    ///     let mut at = 0usize;
523    ///     loop {
524    ///         let input = Input::new(at as u64, &data[at..]).with_total_len(total);
525    ///         let p = dec.step(input, sink, None)?;
526    ///         at += p.consumed;
527    ///         match p.event {
528    ///             // Satisfy any one request; the length is always known here.
529    ///             Event::Need => match dec.requested()[0] {
530    ///                 InputRequest::AtLeast { offset, .. } => at = offset as usize,
531    ///                 InputRequest::Range(r) => at = r.offset as usize,
532    ///                 _ => {}
533    ///             },
534    ///             Event::Done(_) => return Ok(()),
535    ///             _ => {} // rows, metadata, or an event added later
536    ///         }
537    ///         at = at.min(data.len());
538    ///     }
539    /// }
540    /// ```
541    fn dyn_incremental_decoder(
542        self,
543        preferred: &[PixelDescriptor],
544    ) -> Result<Box<super::incremental::DynIncrementalDecoder>, BoxedError> {
545        let _ = preferred;
546        Err(Box::new(
547            crate::capabilities::UnsupportedOperation::IncrementalDecode,
548        ))
549    }
550
551    /// Start a decoder that must suspend before interpretation-dependent work.
552    ///
553    /// The default refuses; ordinary incremental support does not establish this
554    /// contract. Implementations preserve this job's configuration and return an
555    /// [`erase_controlled`](crate::decode::erase_controlled) session. `storage`
556    /// admits participating owned buffers; it is not a native-workspace bound.
557    /// Preferred descriptors remain preferences. A required output is checked
558    /// before adoption by [`DecodeRequest`](crate::operation::DecodeRequest).
559    fn dyn_controlled_decoder(
560        self,
561        preferred: &[PixelDescriptor],
562        storage: crate::storage::StorageBudget,
563    ) -> Result<Box<super::control::DynControlledDecoder>, BoxedError> {
564        let _ = (preferred, storage);
565        Err(Box::new(super::control::ControlError::NotControlled))
566    }
567
568    /// Decode from a random-access source into a caller-owned sink (pull
569    /// model).
570    ///
571    /// The codec reads exactly the byte ranges it needs from `source`, in
572    /// straight-line code, and writes rows into `sink` exactly as
573    /// [`push_decoder`](DecodeJob::push_decoder) does. Crop and region
574    /// intent stay bound on the job; the sink's coordinate space is the
575    /// output image. Codecs whose core reads a seekable input natively
576    /// (directory and tile formats) override this and set
577    /// [`DecodeCapabilities::with_seekable`](crate::decode::DecodeCapabilities::with_seekable);
578    /// drivers with a sequential or async source manufacture one with the
579    /// adapters in `zencodec-io`.
580    ///
581    /// Default implementation returns [`UnsupportedOperation::SeekableDecode`](crate::UnsupportedOperation::SeekableDecode).
582    fn push_decoder_from_source(
583        self,
584        source: &mut dyn crate::source::DynReadAt,
585        sink: &mut dyn crate::DecodeRowSink,
586        preferred: &[PixelDescriptor],
587    ) -> Result<OutputInfo, BoxedError> {
588        let _ = (source, sink, preferred);
589        Err(Box::new(
590            crate::capabilities::UnsupportedOperation::SeekableDecode,
591        ))
592    }
593}