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/// A resolution's unit, as the file states it.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159#[non_exhaustive]
160pub enum ResolutionUnit {
161    /// Pixels per inch.
162    Inch,
163    /// Pixels per centimetre.
164    Centimetre,
165}
166
167/// How densely the image is meant to be printed or shown (sharp's
168/// `density`, `resolutionUnit`).
169#[derive(Debug, Clone, Copy, PartialEq)]
170pub struct Density {
171    /// Horizontal pixels per `unit`.
172    pub x: f64,
173    /// Vertical pixels per `unit`.
174    pub y: f64,
175    /// The unit the file states.
176    pub unit: ResolutionUnit,
177}
178
179impl Density {
180    /// A density of `dpi` both ways, per inch.
181    #[must_use]
182    pub const fn dpi(dpi: f64) -> Self {
183        Self {
184            x: dpi,
185            y: dpi,
186            unit: ResolutionUnit::Inch,
187        }
188    }
189
190    /// The density an EXIF block states in IFD0 (`XResolution`,
191    /// `YResolution`, `ResolutionUnit`), or `None` if it states none.
192    #[must_use]
193    pub fn from_exif_block(exif: &[u8]) -> Option<Self> {
194        let tree = crate::exif::Exif::parse(exif)?;
195        let x = tree.rational(0x011A)?;
196        let y = tree.rational(0x011B).unwrap_or(x);
197        let unit = match tree.integer(0x0128).unwrap_or(2) {
198            3 => ResolutionUnit::Centimetre,
199            2 => ResolutionUnit::Inch,
200            _ => return None,
201        };
202        (x > 0.0 && y > 0.0).then_some(Self { x, y, unit })
203    }
204
205    /// The horizontal density in pixels per inch, sharp's `density`.
206    #[must_use]
207    pub fn x_dpi(&self) -> f64 {
208        match self.unit {
209            ResolutionUnit::Inch => self.x,
210            ResolutionUnit::Centimetre => self.x * 2.54,
211        }
212    }
213
214    /// The vertical density in pixels per inch.
215    #[must_use]
216    pub fn y_dpi(&self) -> f64 {
217        match self.unit {
218            ResolutionUnit::Inch => self.y,
219            ResolutionUnit::Centimetre => self.y * 2.54,
220        }
221    }
222}
223
224/// A text entry a file carries, such as PNG's `tEXt` (sharp's `comments`).
225#[derive(Debug, Clone, PartialEq, Eq, Hash)]
226pub struct Comment {
227    /// The entry's keyword, empty where the format has none.
228    pub keyword: String,
229    /// Its text.
230    pub text: String,
231}
232
233/// The colour a file names as its background, in its own samples.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235#[non_exhaustive]
236pub enum Background {
237    /// A grey level.
238    Grey(u16),
239    /// Red, green and blue.
240    Rgb(u16, u16, u16),
241}
242
243/// What a decoder learned about its file from the header, beyond the pixel
244/// layout: the facts sharp's `metadata()` reports per format. Every field is
245/// empty for a format with no notion of it.
246#[derive(Debug, Clone, Default, PartialEq)]
247#[non_exhaustive]
248pub struct SourceInfo {
249    /// The stated resolution, if any (JPEG's JFIF or EXIF, PNG's `pHYs`,
250    /// TIFF's tags).
251    pub density: Option<Density>,
252    /// Progressive JPEG, or interlaced PNG and GIF.
253    pub progressive: bool,
254    /// Samples are indices into a palette (PNG colour type 3, GIF, TIFF
255    /// palette).
256    pub palette: bool,
257    /// Bits per stored sample, which may be fewer than the output's (a
258    /// 10-bit AVIF, a 4-bit PNG).
259    pub bits_per_sample: Option<u8>,
260    /// Chroma subsampling as sharp spells it (`"4:2:0"`, `"4:4:4"`, ...),
261    /// for formats that subsample.
262    pub chroma_subsampling: Option<&'static str>,
263    /// The background colour the file names (PNG `bKGD`, GIF's).
264    pub background: Option<Background>,
265    /// The compression scheme, where a format has several (TIFF, AVIF).
266    pub compression: Option<&'static str>,
267    /// Text entries (PNG `tEXt`, `zTXt`, `iTXt`; GIF and JPEG comments).
268    pub comments: Vec<Comment>,
269}
270
271/// How an encoder samples chroma (sharp's `chromaSubsampling`).
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
273#[non_exhaustive]
274pub enum ChromaSubsampling {
275    /// Chroma at half resolution both ways (`'4:2:0'`): smaller files, and
276    /// a loss photographs rarely show.
277    Yuv420,
278    /// Chroma at full resolution (`'4:4:4'`): edges between colours, as in
279    /// screenshots and text, stay sharp.
280    Yuv444,
281}
282
283/// Encoder tuning shared across formats.
284///
285/// Fields that a format has no notion of are ignored by that format rather
286/// than rejected, so the same options value can be handed to any encoder.
287/// Every field's default leaves a format's output as it was before the field
288/// existed.
289#[derive(Debug, Clone, Copy, PartialEq)]
290#[non_exhaustive]
291pub struct EncodeOptions {
292    /// Lossy quality, 1–100. Ignored by lossless formats, and by a format
293    /// with both modes when `lossless` is set.
294    pub quality: u8,
295    /// For a format that has both a lossy and a lossless mode (WebP), choose
296    /// lossless. Off by default: such a format encodes lossy at `quality`,
297    /// as `cwebp` and sharp do. Formats with one mode ignore it.
298    pub lossless: bool,
299    /// CPU spent for a smaller file, 0 (fastest) to 10, in each format's
300    /// own range as sharp defines it: WebP 0–6 and AVIF 0–9, both with a
301    /// default of 4. `None` is the format's default.
302    pub effort: Option<u8>,
303    /// PNG's DEFLATE level, 0 (store) to 9. `None` derives it from
304    /// `quality`, as before this option existed.
305    pub compression_level: Option<u8>,
306    /// Write PNG as a palette image of at most `colours` entries (sharp's
307    /// `palette`). Setting `colours` or `dither` implies it, as in sharp.
308    pub palette: bool,
309    /// The palette size for palette PNG and GIF, 2–256. `None` is 256 for
310    /// PNG, and for GIF the size `quality` implies.
311    pub colours: Option<u16>,
312    /// How much of the quantization error a palette image diffuses, 0 (none)
313    /// to 1 (full Floyd-Steinberg, the default). `None` is 1.
314    pub dither: Option<f32>,
315    /// Quality of lossy WebP's alpha channel, 0–100; 100, the default, keeps
316    /// it lossless.
317    pub alpha_quality: u8,
318    /// Lossless WebP that may change pixels a little where it saves bytes,
319    /// by as much as `quality` allows (sharp's `nearLossless`; 100 changes
320    /// nothing). Implies lossless.
321    pub near_lossless: bool,
322    /// Chroma sampling for JPEG and AVIF. `None` is each format's default:
323    /// for JPEG 4:2:0 below quality 90 and 4:4:4 from it, for AVIF 4:2:0.
324    pub chroma_subsampling: Option<ChromaSubsampling>,
325    /// Copy the source's EXIF block into the output (sharp's `keepExif`),
326    /// where the format has a place for one: JPEG, PNG, WebP and AVIF. Off
327    /// by default, which strips camera data and location from uploads.
328    pub keep_exif: bool,
329    /// Copy the source's XMP packet into the output (sharp's `keepXmp`),
330    /// where the format has a place for one: JPEG, PNG, WebP, TIFF, AVIF
331    /// and JPEG XL. Off by default, as for EXIF.
332    pub keep_xmp: bool,
333    /// Write JPEG as progressive (sharp's `progressive`): a coarse picture
334    /// first, sharpened by later scans, and usually a little smaller. The
335    /// encoder then holds the image's coefficients rather than streaming.
336    pub progressive: bool,
337}
338
339/// Check `value` lies in `range`, naming `name` if not.
340fn within(name: &'static str, value: u16, range: core::ops::RangeInclusive<u16>) -> Result<()> {
341    if range.contains(&value) {
342        Ok(())
343    } else {
344        Err(PixelsError::invalid_argument(
345            name,
346            format!(
347                "must be in {}..={}, got {value}",
348                range.start(),
349                range.end()
350            ),
351        ))
352    }
353}
354
355impl EncodeOptions {
356    /// The default quality used when none is given.
357    pub const DEFAULT_QUALITY: u8 = 80;
358
359    /// Options with the given quality.
360    ///
361    /// # Errors
362    ///
363    /// Returns [`PixelsError::InvalidArgument`] unless `quality` is in 1..=100.
364    pub fn with_quality(quality: u8) -> Result<Self> {
365        within("quality", quality.into(), 1..=100)?;
366        Ok(Self {
367            quality,
368            ..Self::default()
369        })
370    }
371
372    /// These options with `lossless` replaced.
373    #[must_use]
374    pub const fn with_lossless(mut self, lossless: bool) -> Self {
375        self.lossless = lossless;
376        self
377    }
378
379    /// These options with an encoding effort, 0..=10 (see
380    /// [`EncodeOptions::effort`]). A value past a format's own range is
381    /// clamped to it.
382    ///
383    /// # Errors
384    ///
385    /// Returns [`PixelsError::InvalidArgument`] above 10.
386    pub fn with_effort(mut self, effort: u8) -> Result<Self> {
387        within("effort", effort.into(), 0..=10)?;
388        self.effort = Some(effort);
389        Ok(self)
390    }
391
392    /// These options with a PNG DEFLATE level, 0..=9.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`PixelsError::InvalidArgument`] above 9.
397    pub fn with_compression_level(mut self, level: u8) -> Result<Self> {
398        within("compression_level", level.into(), 0..=9)?;
399        self.compression_level = Some(level);
400        Ok(self)
401    }
402
403    /// These options with palette PNG on or off.
404    #[must_use]
405    pub const fn with_palette(mut self, palette: bool) -> Self {
406        self.palette = palette;
407        self
408    }
409
410    /// These options with a palette size, 2..=256, which for PNG implies a
411    /// palette.
412    ///
413    /// # Errors
414    ///
415    /// Returns [`PixelsError::InvalidArgument`] outside 2..=256.
416    pub fn with_colours(mut self, colours: u16) -> Result<Self> {
417        within("colours", colours, 2..=256)?;
418        self.colours = Some(colours);
419        Ok(self)
420    }
421
422    /// These options with a dithering level, 0..=1, which for PNG implies a
423    /// palette.
424    ///
425    /// # Errors
426    ///
427    /// Returns [`PixelsError::InvalidArgument`] outside 0..=1.
428    pub fn with_dither(mut self, dither: f32) -> Result<Self> {
429        if !(0.0..=1.0).contains(&dither) {
430            return Err(PixelsError::invalid_argument(
431                "dither",
432                format!("must be in 0..=1, got {dither}"),
433            ));
434        }
435        self.dither = Some(dither);
436        Ok(self)
437    }
438
439    /// These options with a lossy WebP alpha quality, 0..=100.
440    ///
441    /// # Errors
442    ///
443    /// Returns [`PixelsError::InvalidArgument`] above 100.
444    pub fn with_alpha_quality(mut self, quality: u8) -> Result<Self> {
445        within("alpha_quality", quality.into(), 0..=100)?;
446        self.alpha_quality = quality;
447        Ok(self)
448    }
449
450    /// These options with near-lossless WebP on or off.
451    #[must_use]
452    pub const fn with_near_lossless(mut self, near_lossless: bool) -> Self {
453        self.near_lossless = near_lossless;
454        self
455    }
456
457    /// These options with an explicit chroma subsampling.
458    #[must_use]
459    pub const fn with_chroma_subsampling(mut self, subsampling: ChromaSubsampling) -> Self {
460        self.chroma_subsampling = Some(subsampling);
461        self
462    }
463
464    /// These options keeping the source's EXIF, or not.
465    #[must_use]
466    pub const fn with_keep_exif(mut self, keep: bool) -> Self {
467        self.keep_exif = keep;
468        self
469    }
470
471    /// These options with `keep_xmp` replaced.
472    #[must_use]
473    pub const fn with_keep_xmp(mut self, keep: bool) -> Self {
474        self.keep_xmp = keep;
475        self
476    }
477
478    /// These options keeping EXIF and XMP both (sharp's `keepMetadata`;
479    /// the ICC profile is always kept).
480    #[must_use]
481    pub const fn with_keep_metadata(mut self, keep: bool) -> Self {
482        self.keep_exif = keep;
483        self.keep_xmp = keep;
484        self
485    }
486
487    /// These options with progressive JPEG on or off.
488    #[must_use]
489    pub const fn with_progressive(mut self, progressive: bool) -> Self {
490        self.progressive = progressive;
491        self
492    }
493
494    /// Whether a PNG should be written as a palette image.
495    #[must_use]
496    pub const fn wants_palette(&self) -> bool {
497        self.palette || self.colours.is_some() || self.dither.is_some()
498    }
499}
500
501impl Default for EncodeOptions {
502    fn default() -> Self {
503        Self {
504            quality: Self::DEFAULT_QUALITY,
505            lossless: false,
506            effort: None,
507            compression_level: None,
508            palette: false,
509            colours: None,
510            dither: None,
511            alpha_quality: 100,
512            near_lossless: false,
513            chroma_subsampling: None,
514            keep_exif: false,
515            keep_xmp: false,
516            progressive: false,
517        }
518    }
519}
520
521/// Format sniffing.
522///
523/// Detection is by **magic bytes only** — extensions and MIME types are
524/// ignored (SPEC §Formats), because they are attacker-controlled hints rather
525/// than facts about the bytes.
526pub trait Codec {
527    /// The format this codec handles.
528    fn format(&self) -> Format;
529
530    /// The number of leading bytes [`Codec::probe`] needs to decide.
531    fn magic_len(&self) -> usize;
532
533    /// Whether `prefix` looks like this format.
534    ///
535    /// `prefix` may be shorter than [`Codec::magic_len`] if the source ended
536    /// early; an implementation must then return `false`, never index blindly.
537    fn probe(&self, prefix: &[u8]) -> bool;
538}
539
540/// Decodes a byte stream into rows of pixels.
541///
542/// Implementations must return [`PixelsError::Malformed`] for **every** input
543/// they cannot parse. Panicking on hostile bytes is a defect, not a caller
544/// error (ARCHITECTURE §Failure model); every parser is fuzzed in CI from the
545/// first codec onward.
546///
547/// Dimension limits are enforced at header parse, before any pixel buffer is
548/// allocated, so a header claiming enormous dimensions costs nothing.
549pub trait Decoder: Send + fmt::Debug {
550    /// The shape of the image, known after the header is parsed.
551    fn descriptor(&self) -> ImageDescriptor;
552
553    /// What this decoder can produce without a full decode.
554    fn capability(&self) -> DecodeCapability {
555        DecodeCapability::Sequential
556    }
557
558    /// How the stored image must be turned to display upright, as its
559    /// metadata declares.
560    ///
561    /// Reported, never applied: rows come out in stored order whatever this
562    /// says. Applying it is the pipeline's `auto_orient` decision (SPEC
563    /// §Safety and limits), which a decoder that rotated its own output would
564    /// take away from the caller. Known after the header is parsed, like
565    /// [`Decoder::descriptor`].
566    fn orientation(&self) -> Orientation {
567        Orientation::Normal
568    }
569
570    /// The source's animation, if it has more than one frame.
571    ///
572    /// Reported, like [`Decoder::orientation`]: rows are always the first
573    /// frame's. Known once the decoder is constructed; a format that must
574    /// read further to count its frames does so up front.
575    fn animation(&self) -> Option<Animation> {
576        None
577    }
578
579    /// How many pages the source document holds, at least 1.
580    ///
581    /// A page is a separate image in one file, as in a multi-page TIFF: a
582    /// stack of scans or faxes, not the frames of an animation, which
583    /// [`Decoder::animation`] reports. Rows are always the page the decoder
584    /// was opened on. Known once the decoder is constructed, without decoding
585    /// any page.
586    fn pages(&self) -> u32 {
587        1
588    }
589
590    /// The embedded ICC colour profile, if the file carries one.
591    ///
592    /// Reported, never applied, like [`Decoder::orientation`]: samples come
593    /// out as stored, and converting them is the pipeline's decision. Known
594    /// after the header is parsed; a profile stored after the image data is
595    /// not seen.
596    fn icc_profile(&self) -> Option<&[u8]> {
597        None
598    }
599
600    /// The embedded EXIF block, if the file carries one: the TIFF-structured
601    /// bytes from the byte-order mark (`II` or `MM`), without the `Exif\0\0`
602    /// identifier a JPEG wraps it in.
603    ///
604    /// Reported for an encoder to copy, like [`Decoder::icc_profile`]; its
605    /// orientation is what [`Decoder::orientation`] reports.
606    fn exif(&self) -> Option<&[u8]> {
607        None
608    }
609
610    /// The embedded XMP packet, if the file carries one: the XML as stored,
611    /// from `<?xpacket` or `<x:xmpmeta`.
612    ///
613    /// Reported for an encoder to copy, like [`Decoder::exif`].
614    fn xmp(&self) -> Option<&[u8]> {
615        None
616    }
617
618    /// What the header said beyond the pixel layout. Known after the header
619    /// is parsed, like [`Decoder::descriptor`].
620    fn source_info(&self) -> SourceInfo {
621        SourceInfo::default()
622    }
623
624    /// Decode the next row, top to bottom, into `out`.
625    ///
626    /// `out` is exactly [`ImageDescriptor::row_bytes`] long. Each call advances
627    /// the cursor by one row; the caller reads `descriptor().height` rows.
628    ///
629    /// # Errors
630    ///
631    /// Returns [`PixelsError::Malformed`] on invalid or truncated input,
632    /// [`PixelsError::Io`] on source failure, or
633    /// [`PixelsError::InvalidArgument`] if `out` is the wrong length or every
634    /// row has already been read.
635    fn read_row(&mut self, out: &mut [u8]) -> Result<()>;
636
637    /// Decode an arbitrary region into `out`.
638    ///
639    /// Only meaningful when [`Decoder::capability`] is
640    /// [`DecodeCapability::Regions`]; the default implementation reports that
641    /// this decoder is sequential.
642    ///
643    /// # Errors
644    ///
645    /// Returns [`PixelsError::Unsupported`] for sequential decoders, and
646    /// otherwise as [`Decoder::read_row`].
647    fn read_region(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
648        let _ = (region, out);
649        Err(PixelsError::unsupported(
650            "this decoder is sequential; region decode requires DecodeCapability::Regions",
651        ))
652    }
653
654    /// What this decoder would produce if asked for `target` or larger, when
655    /// the format lets it reach that size more cheaply than a full decode.
656    ///
657    /// JPEG is the motivating case — the low-frequency corner of a DCT block
658    /// is a smaller version of that block, so 1/8, 1/4 and 1/2 come almost
659    /// free — but nothing here is JPEG-specific: a pyramidal TIFF or a WebP
660    /// with scaled decode fits the same shape.
661    ///
662    /// **Pure**: nothing is committed. The planner asks before it knows
663    /// whether the reduction is legal for the pipeline as a whole.
664    ///
665    /// The returned descriptor is never smaller than `target` in either axis.
666    fn reduced_descriptor(&self, target: (u32, u32)) -> Option<ImageDescriptor> {
667        let _ = target;
668        None
669    }
670
671    /// Commit to producing `descriptor`, which
672    /// [`Decoder::reduced_descriptor`] must have returned.
673    ///
674    /// # Errors
675    ///
676    /// Returns [`PixelsError::Unsupported`] if this decoder has one
677    /// resolution, or [`PixelsError::InvalidArgument`] if any row has already
678    /// been read — the resolution is fixed from the first row onward.
679    fn reduce_to(&mut self, descriptor: ImageDescriptor) -> Result<()> {
680        let _ = descriptor;
681        Err(PixelsError::unsupported(
682            "this decoder has only one resolution",
683        ))
684    }
685}
686
687/// Encodes rows of pixels into a byte stream.
688///
689/// The sink is passed to each call rather than held by the encoder, which
690/// keeps [`Encoder`] object-safe and lets the caller retain ownership of the
691/// destination. Encoders write incrementally as rows arrive; they must not
692/// buffer the whole image unless the format leaves no choice (ADR-0005).
693pub trait Encoder: Send {
694    /// Embed `profile` as the image's ICC colour profile, or write none.
695    ///
696    /// Called before [`Encoder::write_header`], whose header usually holds
697    /// it. The default ignores it, which is right for a format with nowhere
698    /// to put one (raw pixels, GIF): the pixels are written as given either
699    /// way.
700    ///
701    /// # Errors
702    ///
703    /// Returns [`PixelsError::InvalidArgument`] if the header has already
704    /// been written.
705    fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
706        let _ = profile;
707        Ok(())
708    }
709
710    /// Embed `exif` (TIFF-structured bytes, as [`Decoder::exif`] gives
711    /// them) as the image's EXIF block, or write none.
712    ///
713    /// Called before [`Encoder::write_header`]. The default ignores it, which
714    /// is right for a format with nowhere to put one.
715    ///
716    /// # Errors
717    ///
718    /// Returns [`PixelsError::InvalidArgument`] if the header has already
719    /// been written.
720    fn set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
721        let _ = exif;
722        Ok(())
723    }
724
725    /// Embed `xmp`, an XMP packet as [`Decoder::xmp`] gives it, or write
726    /// none.
727    ///
728    /// Called before [`Encoder::write_header`]. The default ignores it, which
729    /// is right for a format with nowhere to put one.
730    ///
731    /// # Errors
732    ///
733    /// Returns [`PixelsError::InvalidArgument`] if the header has already
734    /// been written.
735    fn set_xmp(&mut self, xmp: Option<&[u8]>) -> Result<()> {
736        let _ = xmp;
737        Ok(())
738    }
739
740    /// Record `density` as the image's resolution, or write the format's
741    /// default.
742    ///
743    /// Called before [`Encoder::write_header`]. The default ignores it, for
744    /// a format with no resolution field.
745    ///
746    /// # Errors
747    ///
748    /// Returns [`PixelsError::InvalidArgument`] if the header has already
749    /// been written.
750    fn set_density(&mut self, density: Option<Density>) -> Result<()> {
751        let _ = density;
752        Ok(())
753    }
754
755    /// Begin an image, writing any container header.
756    ///
757    /// Must be called exactly once, before any [`Encoder::write_row`].
758    ///
759    /// # Errors
760    ///
761    /// Returns [`PixelsError::Io`] on sink failure, or
762    /// [`PixelsError::Unsupported`] if the encoder cannot represent `desc`
763    /// (for example an alpha channel in a format without one).
764    fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()>;
765
766    /// Write the next row, top to bottom.
767    ///
768    /// `row` is exactly [`ImageDescriptor::row_bytes`] long.
769    ///
770    /// # Errors
771    ///
772    /// Returns [`PixelsError::Io`] on sink failure, or
773    /// [`PixelsError::InvalidArgument`] if `row` is the wrong length or more
774    /// rows are written than the header declared.
775    fn write_row(&mut self, row: &[u8], sink: &mut dyn Sink) -> Result<()>;
776
777    /// Finish the image, writing any trailer and flushing the sink.
778    ///
779    /// # Errors
780    ///
781    /// Returns [`PixelsError::Io`] on sink failure, or
782    /// [`PixelsError::Malformed`] if fewer rows were written than the header
783    /// declared — a partial image is never silently emitted
784    /// (ARCHITECTURE §Failure model).
785    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()>;
786}
787
788#[cfg(test)]
789#[allow(
790    clippy::unwrap_used,
791    clippy::indexing_slicing,
792    reason = "tests operate on known-good values and assert shapes directly"
793)]
794mod tests {
795    use super::*;
796    use crate::ErrorCode;
797
798    #[test]
799    fn quality_is_validated_at_the_boundary() {
800        assert_eq!(EncodeOptions::with_quality(1).unwrap().quality, 1);
801        assert_eq!(EncodeOptions::with_quality(100).unwrap().quality, 100);
802        assert_eq!(
803            EncodeOptions::with_quality(0).unwrap_err().code(),
804            ErrorCode::InvalidArgument
805        );
806        assert_eq!(
807            EncodeOptions::with_quality(101).unwrap_err().code(),
808            ErrorCode::InvalidArgument
809        );
810        assert_eq!(EncodeOptions::default().quality, 80);
811    }
812
813    #[test]
814    fn format_names_are_stable_and_unique() {
815        let all = [
816            Format::Raw,
817            Format::Png,
818            Format::Jpeg,
819            Format::Gif,
820            Format::Tiff,
821            Format::WebP,
822            Format::Avif,
823            Format::Jxl,
824        ];
825        let mut seen = std::collections::HashSet::new();
826        for f in all {
827            assert!(seen.insert(f.as_str()), "duplicate name {f}");
828        }
829        assert_eq!(Format::WebP.as_str(), "webp");
830    }
831
832    #[test]
833    fn metadata_mirrors_the_descriptor() {
834        let desc = ImageDescriptor::new(7, 5, PixelFormat::Rgba8).unwrap();
835        let meta = Metadata::new(&desc, Format::Png);
836        assert_eq!((meta.width, meta.height), (7, 5));
837        assert_eq!(meta.pixel, PixelFormat::Rgba8);
838        assert_eq!(meta.format, Format::Png);
839    }
840
841    #[test]
842    fn sequential_decoders_decline_region_decode() {
843        #[derive(Debug)]
844        struct Seq;
845        impl Decoder for Seq {
846            fn descriptor(&self) -> ImageDescriptor {
847                ImageDescriptor::new(1, 1, PixelFormat::Gray8).unwrap_or(ImageDescriptor {
848                    width: 1,
849                    height: 1,
850                    pixel: PixelFormat::Gray8,
851                    color: crate::ColorModel::Srgb,
852                })
853            }
854            fn read_row(&mut self, _: &mut [u8]) -> Result<()> {
855                Ok(())
856            }
857        }
858        let mut buf = crate::TileBuf::zeroed(Region::from_size(1, 1), PixelFormat::Gray8).unwrap();
859        let mut tile = buf.as_tile_mut().unwrap();
860        let err = Seq
861            .read_region(Region::from_size(1, 1), &mut tile)
862            .unwrap_err();
863        assert_eq!(err.code(), ErrorCode::Unsupported);
864        assert_eq!(Seq.capability(), DecodeCapability::Sequential);
865    }
866}