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}