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}
38
39impl Format {
40    /// A short, stable, lowercase name for this format.
41    #[must_use]
42    pub const fn as_str(self) -> &'static str {
43        match self {
44            Self::Raw => "raw",
45            Self::Png => "png",
46            Self::Jpeg => "jpeg",
47            Self::Gif => "gif",
48            Self::Tiff => "tiff",
49            Self::WebP => "webp",
50            Self::Avif => "avif",
51        }
52    }
53}
54
55impl fmt::Display for Format {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.write_str(self.as_str())
58    }
59}
60
61/// What a decoder can produce without a full decode.
62///
63/// The scheduler uses this to decide whether it may request arbitrary regions
64/// from a source or must pull rows in order (ARCHITECTURE §Layer 2).
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66#[non_exhaustive]
67pub enum DecodeCapability {
68    /// Emits rows in top-to-bottom order as bytes arrive.
69    ///
70    /// Out-of-order region requests are not supported; the engine reads
71    /// forward. This covers non-interlaced PNG, baseline JPEG, GIF frames,
72    /// strip TIFF and raw.
73    Sequential,
74    /// Can produce an arbitrary region without decoding the whole image.
75    ///
76    /// This covers tiled TIFF, and is what makes the giant-image thumbnail
77    /// path constant-memory.
78    ///
79    /// A JPEG decoding at a reduced `M/8` scale is *not* this, though an
80    /// earlier version of this comment said it was: scaled decode lowers the
81    /// resolution but still emits rows in order, and every coefficient is
82    /// still entropy-decoded. It is a decoder configuration, not a capability,
83    /// and claiming otherwise would be a lie the scheduler acts on.
84    Regions,
85}
86
87/// How an animated image plays: its frames and their timing.
88///
89/// Reported by [`Decoder::animation`] for a file with more than one frame.
90/// The pipeline processes the first frame; this describes the source, so a
91/// caller can see it is animated and decide what to do (pass it through,
92/// refuse it, or take the still).
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94#[non_exhaustive]
95pub struct Animation {
96    /// Frames in the animation, always two or more.
97    pub frame_count: u32,
98    /// How many times it plays; 0 means forever, as in GIF and WebP.
99    pub loop_count: u32,
100    /// How long each frame shows, in milliseconds, in order.
101    pub frame_durations_ms: Vec<u32>,
102}
103
104impl Animation {
105    /// An animation of the given frame durations, playing `loop_count`
106    /// times (0 for forever). `None` for fewer than two frames, which is a
107    /// still image.
108    #[must_use]
109    pub fn new(frame_durations_ms: Vec<u32>, loop_count: u32) -> Option<Self> {
110        let frame_count = u32::try_from(frame_durations_ms.len()).ok()?;
111        (frame_count >= 2).then_some(Self {
112            frame_count,
113            loop_count,
114            frame_durations_ms,
115        })
116    }
117
118    /// The total time one play-through takes, in milliseconds.
119    #[must_use]
120    pub fn duration_ms(&self) -> u64 {
121        self.frame_durations_ms.iter().map(|&d| u64::from(d)).sum()
122    }
123}
124
125/// Header-only facts about an image.
126///
127/// Answering this must not decode pixels (SPEC §Guarantees 3).
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129#[non_exhaustive]
130pub struct Metadata {
131    /// Width in pixels.
132    pub width: u32,
133    /// Height in pixels.
134    pub height: u32,
135    /// The container format the image was read from.
136    pub format: Format,
137    /// The pixel format the decoder will produce.
138    pub pixel: PixelFormat,
139}
140
141impl Metadata {
142    /// Build metadata from a decoded header descriptor and its container.
143    #[must_use]
144    pub const fn new(desc: &ImageDescriptor, format: Format) -> Self {
145        Self {
146            width: desc.width,
147            height: desc.height,
148            format,
149            pixel: desc.pixel,
150        }
151    }
152}
153
154/// Encoder tuning shared across formats.
155///
156/// Fields that a format has no notion of are ignored by that format rather
157/// than rejected, so the same options value can be handed to any encoder.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159#[non_exhaustive]
160pub struct EncodeOptions {
161    /// Lossy quality, 1–100. Ignored by lossless formats, and by a format
162    /// with both modes when `lossless` is set.
163    pub quality: u8,
164    /// For a format that has both a lossy and a lossless mode (WebP), choose
165    /// lossless. Off by default: such a format encodes lossy at `quality`,
166    /// as `cwebp` and sharp do. Formats with one mode ignore it.
167    pub lossless: bool,
168}
169
170impl EncodeOptions {
171    /// The default quality used when none is given.
172    pub const DEFAULT_QUALITY: u8 = 80;
173
174    /// Options with the given quality.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`PixelsError::InvalidArgument`] unless `quality` is in 1..=100.
179    pub fn with_quality(quality: u8) -> Result<Self> {
180        if !(1..=100).contains(&quality) {
181            return Err(PixelsError::invalid_argument(
182                "quality",
183                format!("must be in 1..=100, got {quality}"),
184            ));
185        }
186        Ok(Self {
187            quality,
188            ..Self::default()
189        })
190    }
191
192    /// These options with `lossless` replaced.
193    #[must_use]
194    pub const fn with_lossless(mut self, lossless: bool) -> Self {
195        self.lossless = lossless;
196        self
197    }
198}
199
200impl Default for EncodeOptions {
201    fn default() -> Self {
202        Self {
203            quality: Self::DEFAULT_QUALITY,
204            lossless: false,
205        }
206    }
207}
208
209/// Format sniffing.
210///
211/// Detection is by **magic bytes only** — extensions and MIME types are
212/// ignored (SPEC §Formats), because they are attacker-controlled hints rather
213/// than facts about the bytes.
214pub trait Codec {
215    /// The format this codec handles.
216    fn format(&self) -> Format;
217
218    /// The number of leading bytes [`Codec::probe`] needs to decide.
219    fn magic_len(&self) -> usize;
220
221    /// Whether `prefix` looks like this format.
222    ///
223    /// `prefix` may be shorter than [`Codec::magic_len`] if the source ended
224    /// early; an implementation must then return `false`, never index blindly.
225    fn probe(&self, prefix: &[u8]) -> bool;
226}
227
228/// Decodes a byte stream into rows of pixels.
229///
230/// Implementations must return [`PixelsError::Malformed`] for **every** input
231/// they cannot parse. Panicking on hostile bytes is a defect, not a caller
232/// error (ARCHITECTURE §Failure model); every parser is fuzzed in CI from the
233/// first codec onward.
234///
235/// Dimension limits are enforced at header parse, before any pixel buffer is
236/// allocated, so a header claiming enormous dimensions costs nothing.
237pub trait Decoder: Send + fmt::Debug {
238    /// The shape of the image, known after the header is parsed.
239    fn descriptor(&self) -> ImageDescriptor;
240
241    /// What this decoder can produce without a full decode.
242    fn capability(&self) -> DecodeCapability {
243        DecodeCapability::Sequential
244    }
245
246    /// How the stored image must be turned to display upright, as its
247    /// metadata declares.
248    ///
249    /// Reported, never applied: rows come out in stored order whatever this
250    /// says. Applying it is the pipeline's `auto_orient` decision (SPEC
251    /// §Safety and limits), which a decoder that rotated its own output would
252    /// take away from the caller. Known after the header is parsed, like
253    /// [`Decoder::descriptor`].
254    fn orientation(&self) -> Orientation {
255        Orientation::Normal
256    }
257
258    /// The source's animation, if it has more than one frame.
259    ///
260    /// Reported, like [`Decoder::orientation`]: rows are always the first
261    /// frame's. Known once the decoder is constructed; a format that must
262    /// read further to count its frames does so up front.
263    fn animation(&self) -> Option<Animation> {
264        None
265    }
266
267    /// How many pages the source document holds, at least 1.
268    ///
269    /// A page is a separate image in one file, as in a multi-page TIFF: a
270    /// stack of scans or faxes, not the frames of an animation, which
271    /// [`Decoder::animation`] reports. Rows are always the page the decoder
272    /// was opened on. Known once the decoder is constructed, without decoding
273    /// any page.
274    fn pages(&self) -> u32 {
275        1
276    }
277
278    /// The embedded ICC colour profile, if the file carries one.
279    ///
280    /// Reported, never applied, like [`Decoder::orientation`]: samples come
281    /// out as stored, and converting them is the pipeline's decision. Known
282    /// after the header is parsed; a profile stored after the image data is
283    /// not seen.
284    fn icc_profile(&self) -> Option<&[u8]> {
285        None
286    }
287
288    /// Decode the next row, top to bottom, into `out`.
289    ///
290    /// `out` is exactly [`ImageDescriptor::row_bytes`] long. Each call advances
291    /// the cursor by one row; the caller reads `descriptor().height` rows.
292    ///
293    /// # Errors
294    ///
295    /// Returns [`PixelsError::Malformed`] on invalid or truncated input,
296    /// [`PixelsError::Io`] on source failure, or
297    /// [`PixelsError::InvalidArgument`] if `out` is the wrong length or every
298    /// row has already been read.
299    fn read_row(&mut self, out: &mut [u8]) -> Result<()>;
300
301    /// Decode an arbitrary region into `out`.
302    ///
303    /// Only meaningful when [`Decoder::capability`] is
304    /// [`DecodeCapability::Regions`]; the default implementation reports that
305    /// this decoder is sequential.
306    ///
307    /// # Errors
308    ///
309    /// Returns [`PixelsError::Unsupported`] for sequential decoders, and
310    /// otherwise as [`Decoder::read_row`].
311    fn read_region(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
312        let _ = (region, out);
313        Err(PixelsError::unsupported(
314            "this decoder is sequential; region decode requires DecodeCapability::Regions",
315        ))
316    }
317
318    /// What this decoder would produce if asked for `target` or larger, when
319    /// the format lets it reach that size more cheaply than a full decode.
320    ///
321    /// JPEG is the motivating case — the low-frequency corner of a DCT block
322    /// is a smaller version of that block, so 1/8, 1/4 and 1/2 come almost
323    /// free — but nothing here is JPEG-specific: a pyramidal TIFF or a WebP
324    /// with scaled decode fits the same shape.
325    ///
326    /// **Pure**: nothing is committed. The planner asks before it knows
327    /// whether the reduction is legal for the pipeline as a whole.
328    ///
329    /// The returned descriptor is never smaller than `target` in either axis.
330    fn reduced_descriptor(&self, target: (u32, u32)) -> Option<ImageDescriptor> {
331        let _ = target;
332        None
333    }
334
335    /// Commit to producing `descriptor`, which
336    /// [`Decoder::reduced_descriptor`] must have returned.
337    ///
338    /// # Errors
339    ///
340    /// Returns [`PixelsError::Unsupported`] if this decoder has one
341    /// resolution, or [`PixelsError::InvalidArgument`] if any row has already
342    /// been read — the resolution is fixed from the first row onward.
343    fn reduce_to(&mut self, descriptor: ImageDescriptor) -> Result<()> {
344        let _ = descriptor;
345        Err(PixelsError::unsupported(
346            "this decoder has only one resolution",
347        ))
348    }
349}
350
351/// Encodes rows of pixels into a byte stream.
352///
353/// The sink is passed to each call rather than held by the encoder, which
354/// keeps [`Encoder`] object-safe and lets the caller retain ownership of the
355/// destination. Encoders write incrementally as rows arrive; they must not
356/// buffer the whole image unless the format leaves no choice (ADR-0005).
357pub trait Encoder: Send {
358    /// Embed `profile` as the image's ICC colour profile, or write none.
359    ///
360    /// Called before [`Encoder::write_header`], whose header usually holds
361    /// it. The default ignores it, which is right for a format with nowhere
362    /// to put one (raw pixels, GIF): the pixels are written as given either
363    /// way.
364    ///
365    /// # Errors
366    ///
367    /// Returns [`PixelsError::InvalidArgument`] if the header has already
368    /// been written.
369    fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
370        let _ = profile;
371        Ok(())
372    }
373
374    /// Begin an image, writing any container header.
375    ///
376    /// Must be called exactly once, before any [`Encoder::write_row`].
377    ///
378    /// # Errors
379    ///
380    /// Returns [`PixelsError::Io`] on sink failure, or
381    /// [`PixelsError::Unsupported`] if the encoder cannot represent `desc`
382    /// (for example an alpha channel in a format without one).
383    fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()>;
384
385    /// Write the next row, top to bottom.
386    ///
387    /// `row` is exactly [`ImageDescriptor::row_bytes`] long.
388    ///
389    /// # Errors
390    ///
391    /// Returns [`PixelsError::Io`] on sink failure, or
392    /// [`PixelsError::InvalidArgument`] if `row` is the wrong length or more
393    /// rows are written than the header declared.
394    fn write_row(&mut self, row: &[u8], sink: &mut dyn Sink) -> Result<()>;
395
396    /// Finish the image, writing any trailer and flushing the sink.
397    ///
398    /// # Errors
399    ///
400    /// Returns [`PixelsError::Io`] on sink failure, or
401    /// [`PixelsError::Malformed`] if fewer rows were written than the header
402    /// declared — a partial image is never silently emitted
403    /// (ARCHITECTURE §Failure model).
404    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()>;
405}
406
407#[cfg(test)]
408#[allow(
409    clippy::unwrap_used,
410    clippy::indexing_slicing,
411    reason = "tests operate on known-good values and assert shapes directly"
412)]
413mod tests {
414    use super::*;
415    use crate::ErrorCode;
416
417    #[test]
418    fn quality_is_validated_at_the_boundary() {
419        assert_eq!(EncodeOptions::with_quality(1).unwrap().quality, 1);
420        assert_eq!(EncodeOptions::with_quality(100).unwrap().quality, 100);
421        assert_eq!(
422            EncodeOptions::with_quality(0).unwrap_err().code(),
423            ErrorCode::InvalidArgument
424        );
425        assert_eq!(
426            EncodeOptions::with_quality(101).unwrap_err().code(),
427            ErrorCode::InvalidArgument
428        );
429        assert_eq!(EncodeOptions::default().quality, 80);
430    }
431
432    #[test]
433    fn format_names_are_stable_and_unique() {
434        let all = [
435            Format::Raw,
436            Format::Png,
437            Format::Jpeg,
438            Format::Gif,
439            Format::Tiff,
440            Format::WebP,
441            Format::Avif,
442        ];
443        let mut seen = std::collections::HashSet::new();
444        for f in all {
445            assert!(seen.insert(f.as_str()), "duplicate name {f}");
446        }
447        assert_eq!(Format::WebP.as_str(), "webp");
448    }
449
450    #[test]
451    fn metadata_mirrors_the_descriptor() {
452        let desc = ImageDescriptor::new(7, 5, PixelFormat::Rgba8).unwrap();
453        let meta = Metadata::new(&desc, Format::Png);
454        assert_eq!((meta.width, meta.height), (7, 5));
455        assert_eq!(meta.pixel, PixelFormat::Rgba8);
456        assert_eq!(meta.format, Format::Png);
457    }
458
459    #[test]
460    fn sequential_decoders_decline_region_decode() {
461        #[derive(Debug)]
462        struct Seq;
463        impl Decoder for Seq {
464            fn descriptor(&self) -> ImageDescriptor {
465                ImageDescriptor::new(1, 1, PixelFormat::Gray8).unwrap_or(ImageDescriptor {
466                    width: 1,
467                    height: 1,
468                    pixel: PixelFormat::Gray8,
469                    color: crate::ColorModel::Srgb,
470                })
471            }
472            fn read_row(&mut self, _: &mut [u8]) -> Result<()> {
473                Ok(())
474            }
475        }
476        let mut buf = crate::TileBuf::zeroed(Region::from_size(1, 1), PixelFormat::Gray8).unwrap();
477        let mut tile = buf.as_tile_mut().unwrap();
478        let err = Seq
479            .read_region(Region::from_size(1, 1), &mut tile)
480            .unwrap_err();
481        assert_eq!(err.code(), ErrorCode::Unsupported);
482        assert_eq!(Seq.capability(), DecodeCapability::Sequential);
483    }
484}