Skip to main content

otf_pixels_core/
codec.rs

1//! Container formats and the codec traits every format plugs in behind.
2//!
3//! Every format is implemented in this workspace (ADR-0004, then ADR-0013,
4//! ADR-0014 and ADR-0018), and that is invisible here on purpose: everything
5//! sits behind [`Decoder`] and [`Encoder`], so a format can be rewritten
6//! without an API change.
7
8use crate::{
9    ImageDescriptor, Orientation, PixelFormat, PixelsError, Region, Result, Sink, TileMut,
10};
11use core::fmt;
12
13/// A container format.
14///
15/// Format is **data**, not a type parameter: `output(format, options)` is the
16/// single encode terminal, which keeps the API surface stable as formats are
17/// added and maps cleanly onto host bindings (ADR-0006). Formats not yet
18/// implemented are still nameable, so an unsupported request is a catchable
19/// [`PixelsError::Unsupported`] rather than a compile error in a host binding.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum Format {
23    /// Uncompressed pixels described entirely by the caller.
24    Raw,
25    /// Portable Network Graphics.
26    Png,
27    /// JPEG.
28    Jpeg,
29    /// Graphics Interchange Format.
30    Gif,
31    /// Tagged Image File Format.
32    Tiff,
33    /// WebP.
34    WebP,
35    /// AV1 Image File Format.
36    Avif,
37    /// JPEG XL.
38    Jxl,
39}
40
41impl Format {
42    /// A short, stable, lowercase name for this format.
43    #[must_use]
44    pub const fn as_str(self) -> &'static str {
45        match self {
46            Self::Raw => "raw",
47            Self::Png => "png",
48            Self::Jpeg => "jpeg",
49            Self::Gif => "gif",
50            Self::Tiff => "tiff",
51            Self::WebP => "webp",
52            Self::Avif => "avif",
53            Self::Jxl => "jxl",
54        }
55    }
56}
57
58impl fmt::Display for Format {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str(self.as_str())
61    }
62}
63
64/// What a decoder can produce without a full decode.
65///
66/// The scheduler uses this to decide whether it may request arbitrary regions
67/// from a source or must pull rows in order (ARCHITECTURE §Layer 2).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69#[non_exhaustive]
70pub enum DecodeCapability {
71    /// Emits rows in top-to-bottom order as bytes arrive.
72    ///
73    /// Out-of-order region requests are not supported; the engine reads
74    /// forward. This covers non-interlaced PNG, baseline JPEG, GIF frames,
75    /// strip TIFF and raw.
76    Sequential,
77    /// Can produce an arbitrary region without decoding the whole image.
78    ///
79    /// This covers tiled TIFF, and is what makes the giant-image thumbnail
80    /// path constant-memory.
81    ///
82    /// A JPEG decoding at a reduced `M/8` scale is *not* this, though an
83    /// earlier version of this comment said it was: scaled decode lowers the
84    /// resolution but still emits rows in order, and every coefficient is
85    /// still entropy-decoded. It is a decoder configuration, not a capability,
86    /// and claiming otherwise would be a lie the scheduler acts on.
87    Regions,
88}
89
90/// How an animated image plays: its frames and their timing.
91///
92/// Reported by [`Decoder::animation`] for a file with more than one frame.
93/// The pipeline processes the first frame; this describes the source, so a
94/// caller can see it is animated and decide what to do (pass it through,
95/// refuse it, or take the still).
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97#[non_exhaustive]
98pub struct Animation {
99    /// Frames in the animation, always two or more.
100    pub frame_count: u32,
101    /// How many times it plays; 0 means forever, as in GIF and WebP.
102    pub loop_count: u32,
103    /// How long each frame shows, in milliseconds, in order.
104    pub frame_durations_ms: Vec<u32>,
105}
106
107impl Animation {
108    /// An animation of the given frame durations, playing `loop_count`
109    /// times (0 for forever). `None` for fewer than two frames, which is a
110    /// still image.
111    #[must_use]
112    pub fn new(frame_durations_ms: Vec<u32>, loop_count: u32) -> Option<Self> {
113        let frame_count = u32::try_from(frame_durations_ms.len()).ok()?;
114        (frame_count >= 2).then_some(Self {
115            frame_count,
116            loop_count,
117            frame_durations_ms,
118        })
119    }
120
121    /// The total time one play-through takes, in milliseconds.
122    #[must_use]
123    pub fn duration_ms(&self) -> u64 {
124        self.frame_durations_ms.iter().map(|&d| u64::from(d)).sum()
125    }
126}
127
128/// Header-only facts about an image.
129///
130/// Answering this must not decode pixels (SPEC §Guarantees 3).
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132#[non_exhaustive]
133pub struct Metadata {
134    /// Width in pixels.
135    pub width: u32,
136    /// Height in pixels.
137    pub height: u32,
138    /// The container format the image was read from.
139    pub format: Format,
140    /// The pixel format the decoder will produce.
141    pub pixel: PixelFormat,
142}
143
144impl Metadata {
145    /// Build metadata from a decoded header descriptor and its container.
146    #[must_use]
147    pub const fn new(desc: &ImageDescriptor, format: Format) -> Self {
148        Self {
149            width: desc.width,
150            height: desc.height,
151            format,
152            pixel: desc.pixel,
153        }
154    }
155}
156
157/// How an encoder samples chroma (sharp's `chromaSubsampling`).
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159#[non_exhaustive]
160pub enum ChromaSubsampling {
161    /// Chroma at half resolution both ways (`'4:2:0'`): smaller files, and
162    /// a loss photographs rarely show.
163    Yuv420,
164    /// Chroma at full resolution (`'4:4:4'`): edges between colours, as in
165    /// screenshots and text, stay sharp.
166    Yuv444,
167}
168
169/// Encoder tuning shared across formats.
170///
171/// Fields that a format has no notion of are ignored by that format rather
172/// than rejected, so the same options value can be handed to any encoder.
173/// Every field's default leaves a format's output as it was before the field
174/// existed.
175#[derive(Debug, Clone, Copy, PartialEq)]
176#[non_exhaustive]
177pub struct EncodeOptions {
178    /// Lossy quality, 1–100. Ignored by lossless formats, and by a format
179    /// with both modes when `lossless` is set.
180    pub quality: u8,
181    /// For a format that has both a lossy and a lossless mode (WebP), choose
182    /// lossless. Off by default: such a format encodes lossy at `quality`,
183    /// as `cwebp` and sharp do. Formats with one mode ignore it.
184    pub lossless: bool,
185    /// CPU spent for a smaller file, 0 (fastest) to 10, in each format's
186    /// own range as sharp defines it: WebP 0–6 and AVIF 0–9, both with a
187    /// default of 4. `None` is the format's default.
188    pub effort: Option<u8>,
189    /// PNG's DEFLATE level, 0 (store) to 9. `None` derives it from
190    /// `quality`, as before this option existed.
191    pub compression_level: Option<u8>,
192    /// Write PNG as a palette image of at most `colours` entries (sharp's
193    /// `palette`). Setting `colours` or `dither` implies it, as in sharp.
194    pub palette: bool,
195    /// The palette size for palette PNG and GIF, 2–256. `None` is 256 for
196    /// PNG, and for GIF the size `quality` implies.
197    pub colours: Option<u16>,
198    /// How much of the quantization error a palette image diffuses, 0 (none)
199    /// to 1 (full Floyd-Steinberg, the default). `None` is 1.
200    pub dither: Option<f32>,
201    /// Quality of lossy WebP's alpha channel, 0–100; 100, the default, keeps
202    /// it lossless.
203    pub alpha_quality: u8,
204    /// Lossless WebP that may change pixels a little where it saves bytes,
205    /// by as much as `quality` allows (sharp's `nearLossless`; 100 changes
206    /// nothing). Implies lossless.
207    pub near_lossless: bool,
208    /// Chroma sampling for JPEG and AVIF. `None` is each format's default:
209    /// for JPEG 4:2:0 below quality 90 and 4:4:4 from it, for AVIF 4:2:0.
210    pub chroma_subsampling: Option<ChromaSubsampling>,
211    /// Copy the source's EXIF block into the output (sharp's `keepExif`),
212    /// where the format has a place for one: JPEG, PNG, WebP and AVIF. Off
213    /// by default, which strips camera data and location from uploads.
214    pub keep_exif: bool,
215    /// Write JPEG as progressive (sharp's `progressive`): a coarse picture
216    /// first, sharpened by later scans, and usually a little smaller. The
217    /// encoder then holds the image's coefficients rather than streaming.
218    pub progressive: bool,
219}
220
221/// Check `value` lies in `range`, naming `name` if not.
222fn within(name: &'static str, value: u16, range: core::ops::RangeInclusive<u16>) -> Result<()> {
223    if range.contains(&value) {
224        Ok(())
225    } else {
226        Err(PixelsError::invalid_argument(
227            name,
228            format!(
229                "must be in {}..={}, got {value}",
230                range.start(),
231                range.end()
232            ),
233        ))
234    }
235}
236
237impl EncodeOptions {
238    /// The default quality used when none is given.
239    pub const DEFAULT_QUALITY: u8 = 80;
240
241    /// Options with the given quality.
242    ///
243    /// # Errors
244    ///
245    /// Returns [`PixelsError::InvalidArgument`] unless `quality` is in 1..=100.
246    pub fn with_quality(quality: u8) -> Result<Self> {
247        within("quality", quality.into(), 1..=100)?;
248        Ok(Self {
249            quality,
250            ..Self::default()
251        })
252    }
253
254    /// These options with `lossless` replaced.
255    #[must_use]
256    pub const fn with_lossless(mut self, lossless: bool) -> Self {
257        self.lossless = lossless;
258        self
259    }
260
261    /// These options with an encoding effort, 0..=10 (see
262    /// [`EncodeOptions::effort`]). A value past a format's own range is
263    /// clamped to it.
264    ///
265    /// # Errors
266    ///
267    /// Returns [`PixelsError::InvalidArgument`] above 10.
268    pub fn with_effort(mut self, effort: u8) -> Result<Self> {
269        within("effort", effort.into(), 0..=10)?;
270        self.effort = Some(effort);
271        Ok(self)
272    }
273
274    /// These options with a PNG DEFLATE level, 0..=9.
275    ///
276    /// # Errors
277    ///
278    /// Returns [`PixelsError::InvalidArgument`] above 9.
279    pub fn with_compression_level(mut self, level: u8) -> Result<Self> {
280        within("compression_level", level.into(), 0..=9)?;
281        self.compression_level = Some(level);
282        Ok(self)
283    }
284
285    /// These options with palette PNG on or off.
286    #[must_use]
287    pub const fn with_palette(mut self, palette: bool) -> Self {
288        self.palette = palette;
289        self
290    }
291
292    /// These options with a palette size, 2..=256, which for PNG implies a
293    /// palette.
294    ///
295    /// # Errors
296    ///
297    /// Returns [`PixelsError::InvalidArgument`] outside 2..=256.
298    pub fn with_colours(mut self, colours: u16) -> Result<Self> {
299        within("colours", colours, 2..=256)?;
300        self.colours = Some(colours);
301        Ok(self)
302    }
303
304    /// These options with a dithering level, 0..=1, which for PNG implies a
305    /// palette.
306    ///
307    /// # Errors
308    ///
309    /// Returns [`PixelsError::InvalidArgument`] outside 0..=1.
310    pub fn with_dither(mut self, dither: f32) -> Result<Self> {
311        if !(0.0..=1.0).contains(&dither) {
312            return Err(PixelsError::invalid_argument(
313                "dither",
314                format!("must be in 0..=1, got {dither}"),
315            ));
316        }
317        self.dither = Some(dither);
318        Ok(self)
319    }
320
321    /// These options with a lossy WebP alpha quality, 0..=100.
322    ///
323    /// # Errors
324    ///
325    /// Returns [`PixelsError::InvalidArgument`] above 100.
326    pub fn with_alpha_quality(mut self, quality: u8) -> Result<Self> {
327        within("alpha_quality", quality.into(), 0..=100)?;
328        self.alpha_quality = quality;
329        Ok(self)
330    }
331
332    /// These options with near-lossless WebP on or off.
333    #[must_use]
334    pub const fn with_near_lossless(mut self, near_lossless: bool) -> Self {
335        self.near_lossless = near_lossless;
336        self
337    }
338
339    /// These options with an explicit chroma subsampling.
340    #[must_use]
341    pub const fn with_chroma_subsampling(mut self, subsampling: ChromaSubsampling) -> Self {
342        self.chroma_subsampling = Some(subsampling);
343        self
344    }
345
346    /// These options keeping the source's EXIF, or not.
347    #[must_use]
348    pub const fn with_keep_exif(mut self, keep: bool) -> Self {
349        self.keep_exif = keep;
350        self
351    }
352
353    /// These options with progressive JPEG on or off.
354    #[must_use]
355    pub const fn with_progressive(mut self, progressive: bool) -> Self {
356        self.progressive = progressive;
357        self
358    }
359
360    /// Whether a PNG should be written as a palette image.
361    #[must_use]
362    pub const fn wants_palette(&self) -> bool {
363        self.palette || self.colours.is_some() || self.dither.is_some()
364    }
365}
366
367impl Default for EncodeOptions {
368    fn default() -> Self {
369        Self {
370            quality: Self::DEFAULT_QUALITY,
371            lossless: false,
372            effort: None,
373            compression_level: None,
374            palette: false,
375            colours: None,
376            dither: None,
377            alpha_quality: 100,
378            near_lossless: false,
379            chroma_subsampling: None,
380            keep_exif: false,
381            progressive: false,
382        }
383    }
384}
385
386/// Format sniffing.
387///
388/// Detection is by **magic bytes only** — extensions and MIME types are
389/// ignored (SPEC §Formats), because they are attacker-controlled hints rather
390/// than facts about the bytes.
391pub trait Codec {
392    /// The format this codec handles.
393    fn format(&self) -> Format;
394
395    /// The number of leading bytes [`Codec::probe`] needs to decide.
396    fn magic_len(&self) -> usize;
397
398    /// Whether `prefix` looks like this format.
399    ///
400    /// `prefix` may be shorter than [`Codec::magic_len`] if the source ended
401    /// early; an implementation must then return `false`, never index blindly.
402    fn probe(&self, prefix: &[u8]) -> bool;
403}
404
405/// Decodes a byte stream into rows of pixels.
406///
407/// Implementations must return [`PixelsError::Malformed`] for **every** input
408/// they cannot parse. Panicking on hostile bytes is a defect, not a caller
409/// error (ARCHITECTURE §Failure model); every parser is fuzzed in CI from the
410/// first codec onward.
411///
412/// Dimension limits are enforced at header parse, before any pixel buffer is
413/// allocated, so a header claiming enormous dimensions costs nothing.
414pub trait Decoder: Send + fmt::Debug {
415    /// The shape of the image, known after the header is parsed.
416    fn descriptor(&self) -> ImageDescriptor;
417
418    /// What this decoder can produce without a full decode.
419    fn capability(&self) -> DecodeCapability {
420        DecodeCapability::Sequential
421    }
422
423    /// How the stored image must be turned to display upright, as its
424    /// metadata declares.
425    ///
426    /// Reported, never applied: rows come out in stored order whatever this
427    /// says. Applying it is the pipeline's `auto_orient` decision (SPEC
428    /// §Safety and limits), which a decoder that rotated its own output would
429    /// take away from the caller. Known after the header is parsed, like
430    /// [`Decoder::descriptor`].
431    fn orientation(&self) -> Orientation {
432        Orientation::Normal
433    }
434
435    /// The source's animation, if it has more than one frame.
436    ///
437    /// Reported, like [`Decoder::orientation`]: rows are always the first
438    /// frame's. Known once the decoder is constructed; a format that must
439    /// read further to count its frames does so up front.
440    fn animation(&self) -> Option<Animation> {
441        None
442    }
443
444    /// How many pages the source document holds, at least 1.
445    ///
446    /// A page is a separate image in one file, as in a multi-page TIFF: a
447    /// stack of scans or faxes, not the frames of an animation, which
448    /// [`Decoder::animation`] reports. Rows are always the page the decoder
449    /// was opened on. Known once the decoder is constructed, without decoding
450    /// any page.
451    fn pages(&self) -> u32 {
452        1
453    }
454
455    /// The embedded ICC colour profile, if the file carries one.
456    ///
457    /// Reported, never applied, like [`Decoder::orientation`]: samples come
458    /// out as stored, and converting them is the pipeline's decision. Known
459    /// after the header is parsed; a profile stored after the image data is
460    /// not seen.
461    fn icc_profile(&self) -> Option<&[u8]> {
462        None
463    }
464
465    /// The embedded EXIF block, if the file carries one: the TIFF-structured
466    /// bytes from the byte-order mark (`II` or `MM`), without the `Exif\0\0`
467    /// identifier a JPEG wraps it in.
468    ///
469    /// Reported for an encoder to copy, like [`Decoder::icc_profile`]; its
470    /// orientation is what [`Decoder::orientation`] reports.
471    fn exif(&self) -> Option<&[u8]> {
472        None
473    }
474
475    /// Decode the next row, top to bottom, into `out`.
476    ///
477    /// `out` is exactly [`ImageDescriptor::row_bytes`] long. Each call advances
478    /// the cursor by one row; the caller reads `descriptor().height` rows.
479    ///
480    /// # Errors
481    ///
482    /// Returns [`PixelsError::Malformed`] on invalid or truncated input,
483    /// [`PixelsError::Io`] on source failure, or
484    /// [`PixelsError::InvalidArgument`] if `out` is the wrong length or every
485    /// row has already been read.
486    fn read_row(&mut self, out: &mut [u8]) -> Result<()>;
487
488    /// Decode an arbitrary region into `out`.
489    ///
490    /// Only meaningful when [`Decoder::capability`] is
491    /// [`DecodeCapability::Regions`]; the default implementation reports that
492    /// this decoder is sequential.
493    ///
494    /// # Errors
495    ///
496    /// Returns [`PixelsError::Unsupported`] for sequential decoders, and
497    /// otherwise as [`Decoder::read_row`].
498    fn read_region(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
499        let _ = (region, out);
500        Err(PixelsError::unsupported(
501            "this decoder is sequential; region decode requires DecodeCapability::Regions",
502        ))
503    }
504
505    /// What this decoder would produce if asked for `target` or larger, when
506    /// the format lets it reach that size more cheaply than a full decode.
507    ///
508    /// JPEG is the motivating case — the low-frequency corner of a DCT block
509    /// is a smaller version of that block, so 1/8, 1/4 and 1/2 come almost
510    /// free — but nothing here is JPEG-specific: a pyramidal TIFF or a WebP
511    /// with scaled decode fits the same shape.
512    ///
513    /// **Pure**: nothing is committed. The planner asks before it knows
514    /// whether the reduction is legal for the pipeline as a whole.
515    ///
516    /// The returned descriptor is never smaller than `target` in either axis.
517    fn reduced_descriptor(&self, target: (u32, u32)) -> Option<ImageDescriptor> {
518        let _ = target;
519        None
520    }
521
522    /// Commit to producing `descriptor`, which
523    /// [`Decoder::reduced_descriptor`] must have returned.
524    ///
525    /// # Errors
526    ///
527    /// Returns [`PixelsError::Unsupported`] if this decoder has one
528    /// resolution, or [`PixelsError::InvalidArgument`] if any row has already
529    /// been read — the resolution is fixed from the first row onward.
530    fn reduce_to(&mut self, descriptor: ImageDescriptor) -> Result<()> {
531        let _ = descriptor;
532        Err(PixelsError::unsupported(
533            "this decoder has only one resolution",
534        ))
535    }
536}
537
538/// Encodes rows of pixels into a byte stream.
539///
540/// The sink is passed to each call rather than held by the encoder, which
541/// keeps [`Encoder`] object-safe and lets the caller retain ownership of the
542/// destination. Encoders write incrementally as rows arrive; they must not
543/// buffer the whole image unless the format leaves no choice (ADR-0005).
544pub trait Encoder: Send {
545    /// Embed `profile` as the image's ICC colour profile, or write none.
546    ///
547    /// Called before [`Encoder::write_header`], whose header usually holds
548    /// it. The default ignores it, which is right for a format with nowhere
549    /// to put one (raw pixels, GIF): the pixels are written as given either
550    /// way.
551    ///
552    /// # Errors
553    ///
554    /// Returns [`PixelsError::InvalidArgument`] if the header has already
555    /// been written.
556    fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
557        let _ = profile;
558        Ok(())
559    }
560
561    /// Embed `exif` (TIFF-structured bytes, as [`Decoder::exif`] gives
562    /// them) as the image's EXIF block, or write none.
563    ///
564    /// Called before [`Encoder::write_header`]. The default ignores it, which
565    /// is right for a format with nowhere to put one.
566    ///
567    /// # Errors
568    ///
569    /// Returns [`PixelsError::InvalidArgument`] if the header has already
570    /// been written.
571    fn set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
572        let _ = exif;
573        Ok(())
574    }
575
576    /// Begin an image, writing any container header.
577    ///
578    /// Must be called exactly once, before any [`Encoder::write_row`].
579    ///
580    /// # Errors
581    ///
582    /// Returns [`PixelsError::Io`] on sink failure, or
583    /// [`PixelsError::Unsupported`] if the encoder cannot represent `desc`
584    /// (for example an alpha channel in a format without one).
585    fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()>;
586
587    /// Write the next row, top to bottom.
588    ///
589    /// `row` is exactly [`ImageDescriptor::row_bytes`] long.
590    ///
591    /// # Errors
592    ///
593    /// Returns [`PixelsError::Io`] on sink failure, or
594    /// [`PixelsError::InvalidArgument`] if `row` is the wrong length or more
595    /// rows are written than the header declared.
596    fn write_row(&mut self, row: &[u8], sink: &mut dyn Sink) -> Result<()>;
597
598    /// Finish the image, writing any trailer and flushing the sink.
599    ///
600    /// # Errors
601    ///
602    /// Returns [`PixelsError::Io`] on sink failure, or
603    /// [`PixelsError::Malformed`] if fewer rows were written than the header
604    /// declared — a partial image is never silently emitted
605    /// (ARCHITECTURE §Failure model).
606    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()>;
607}
608
609#[cfg(test)]
610#[allow(
611    clippy::unwrap_used,
612    clippy::indexing_slicing,
613    reason = "tests operate on known-good values and assert shapes directly"
614)]
615mod tests {
616    use super::*;
617    use crate::ErrorCode;
618
619    #[test]
620    fn quality_is_validated_at_the_boundary() {
621        assert_eq!(EncodeOptions::with_quality(1).unwrap().quality, 1);
622        assert_eq!(EncodeOptions::with_quality(100).unwrap().quality, 100);
623        assert_eq!(
624            EncodeOptions::with_quality(0).unwrap_err().code(),
625            ErrorCode::InvalidArgument
626        );
627        assert_eq!(
628            EncodeOptions::with_quality(101).unwrap_err().code(),
629            ErrorCode::InvalidArgument
630        );
631        assert_eq!(EncodeOptions::default().quality, 80);
632    }
633
634    #[test]
635    fn format_names_are_stable_and_unique() {
636        let all = [
637            Format::Raw,
638            Format::Png,
639            Format::Jpeg,
640            Format::Gif,
641            Format::Tiff,
642            Format::WebP,
643            Format::Avif,
644            Format::Jxl,
645        ];
646        let mut seen = std::collections::HashSet::new();
647        for f in all {
648            assert!(seen.insert(f.as_str()), "duplicate name {f}");
649        }
650        assert_eq!(Format::WebP.as_str(), "webp");
651    }
652
653    #[test]
654    fn metadata_mirrors_the_descriptor() {
655        let desc = ImageDescriptor::new(7, 5, PixelFormat::Rgba8).unwrap();
656        let meta = Metadata::new(&desc, Format::Png);
657        assert_eq!((meta.width, meta.height), (7, 5));
658        assert_eq!(meta.pixel, PixelFormat::Rgba8);
659        assert_eq!(meta.format, Format::Png);
660    }
661
662    #[test]
663    fn sequential_decoders_decline_region_decode() {
664        #[derive(Debug)]
665        struct Seq;
666        impl Decoder for Seq {
667            fn descriptor(&self) -> ImageDescriptor {
668                ImageDescriptor::new(1, 1, PixelFormat::Gray8).unwrap_or(ImageDescriptor {
669                    width: 1,
670                    height: 1,
671                    pixel: PixelFormat::Gray8,
672                    color: crate::ColorModel::Srgb,
673                })
674            }
675            fn read_row(&mut self, _: &mut [u8]) -> Result<()> {
676                Ok(())
677            }
678        }
679        let mut buf = crate::TileBuf::zeroed(Region::from_size(1, 1), PixelFormat::Gray8).unwrap();
680        let mut tile = buf.as_tile_mut().unwrap();
681        let err = Seq
682            .read_region(Region::from_size(1, 1), &mut tile)
683            .unwrap_err();
684        assert_eq!(err.code(), ErrorCode::Unsupported);
685        assert_eq!(Seq.capability(), DecodeCapability::Sequential);
686    }
687}