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}