Skip to main content

otf_pixels_core/
codec.rs

1//! Container formats and the codec traits every format plugs in behind.
2//!
3//! Ownership per format — implemented from scratch versus wrapped — is
4//! recorded in ADR-0004 and is invisible here on purpose: everything sits
5//! behind [`Decoder`] and [`Encoder`], so a format can be rewritten without an
6//! 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    /// The embedded ICC colour profile, if the file carries one.
268    ///
269    /// Reported, never applied, like [`Decoder::orientation`]: samples come
270    /// out as stored, and converting them is the pipeline's decision. Known
271    /// after the header is parsed; a profile stored after the image data is
272    /// not seen.
273    fn icc_profile(&self) -> Option<&[u8]> {
274        None
275    }
276
277    /// Decode the next row, top to bottom, into `out`.
278    ///
279    /// `out` is exactly [`ImageDescriptor::row_bytes`] long. Each call advances
280    /// the cursor by one row; the caller reads `descriptor().height` rows.
281    ///
282    /// # Errors
283    ///
284    /// Returns [`PixelsError::Malformed`] on invalid or truncated input,
285    /// [`PixelsError::Io`] on source failure, or
286    /// [`PixelsError::InvalidArgument`] if `out` is the wrong length or every
287    /// row has already been read.
288    fn read_row(&mut self, out: &mut [u8]) -> Result<()>;
289
290    /// Decode an arbitrary region into `out`.
291    ///
292    /// Only meaningful when [`Decoder::capability`] is
293    /// [`DecodeCapability::Regions`]; the default implementation reports that
294    /// this decoder is sequential.
295    ///
296    /// # Errors
297    ///
298    /// Returns [`PixelsError::Unsupported`] for sequential decoders, and
299    /// otherwise as [`Decoder::read_row`].
300    fn read_region(&mut self, region: Region, out: &mut TileMut<'_>) -> Result<()> {
301        let _ = (region, out);
302        Err(PixelsError::unsupported(
303            "this decoder is sequential; region decode requires DecodeCapability::Regions",
304        ))
305    }
306
307    /// What this decoder would produce if asked for `target` or larger, when
308    /// the format lets it reach that size more cheaply than a full decode.
309    ///
310    /// JPEG is the motivating case — the low-frequency corner of a DCT block
311    /// is a smaller version of that block, so 1/8, 1/4 and 1/2 come almost
312    /// free — but nothing here is JPEG-specific: a pyramidal TIFF or a WebP
313    /// with scaled decode fits the same shape.
314    ///
315    /// **Pure**: nothing is committed. The planner asks before it knows
316    /// whether the reduction is legal for the pipeline as a whole.
317    ///
318    /// The returned descriptor is never smaller than `target` in either axis.
319    fn reduced_descriptor(&self, target: (u32, u32)) -> Option<ImageDescriptor> {
320        let _ = target;
321        None
322    }
323
324    /// Commit to producing `descriptor`, which
325    /// [`Decoder::reduced_descriptor`] must have returned.
326    ///
327    /// # Errors
328    ///
329    /// Returns [`PixelsError::Unsupported`] if this decoder has one
330    /// resolution, or [`PixelsError::InvalidArgument`] if any row has already
331    /// been read — the resolution is fixed from the first row onward.
332    fn reduce_to(&mut self, descriptor: ImageDescriptor) -> Result<()> {
333        let _ = descriptor;
334        Err(PixelsError::unsupported(
335            "this decoder has only one resolution",
336        ))
337    }
338}
339
340/// Encodes rows of pixels into a byte stream.
341///
342/// The sink is passed to each call rather than held by the encoder, which
343/// keeps [`Encoder`] object-safe and lets the caller retain ownership of the
344/// destination. Encoders write incrementally as rows arrive; they must not
345/// buffer the whole image unless the format leaves no choice (ADR-0005).
346pub trait Encoder: Send {
347    /// Embed `profile` as the image's ICC colour profile, or write none.
348    ///
349    /// Called before [`Encoder::write_header`], whose header usually holds
350    /// it. The default ignores it, which is right for a format with nowhere
351    /// to put one (raw pixels, GIF): the pixels are written as given either
352    /// way.
353    ///
354    /// # Errors
355    ///
356    /// Returns [`PixelsError::InvalidArgument`] if the header has already
357    /// been written.
358    fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
359        let _ = profile;
360        Ok(())
361    }
362
363    /// Begin an image, writing any container header.
364    ///
365    /// Must be called exactly once, before any [`Encoder::write_row`].
366    ///
367    /// # Errors
368    ///
369    /// Returns [`PixelsError::Io`] on sink failure, or
370    /// [`PixelsError::Unsupported`] if the encoder cannot represent `desc`
371    /// (for example an alpha channel in a format without one).
372    fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()>;
373
374    /// Write the next row, top to bottom.
375    ///
376    /// `row` is exactly [`ImageDescriptor::row_bytes`] long.
377    ///
378    /// # Errors
379    ///
380    /// Returns [`PixelsError::Io`] on sink failure, or
381    /// [`PixelsError::InvalidArgument`] if `row` is the wrong length or more
382    /// rows are written than the header declared.
383    fn write_row(&mut self, row: &[u8], sink: &mut dyn Sink) -> Result<()>;
384
385    /// Finish the image, writing any trailer and flushing the sink.
386    ///
387    /// # Errors
388    ///
389    /// Returns [`PixelsError::Io`] on sink failure, or
390    /// [`PixelsError::Malformed`] if fewer rows were written than the header
391    /// declared — a partial image is never silently emitted
392    /// (ARCHITECTURE §Failure model).
393    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()>;
394}
395
396#[cfg(test)]
397#[allow(
398    clippy::unwrap_used,
399    clippy::indexing_slicing,
400    reason = "tests operate on known-good values and assert shapes directly"
401)]
402mod tests {
403    use super::*;
404    use crate::ErrorCode;
405
406    #[test]
407    fn quality_is_validated_at_the_boundary() {
408        assert_eq!(EncodeOptions::with_quality(1).unwrap().quality, 1);
409        assert_eq!(EncodeOptions::with_quality(100).unwrap().quality, 100);
410        assert_eq!(
411            EncodeOptions::with_quality(0).unwrap_err().code(),
412            ErrorCode::InvalidArgument
413        );
414        assert_eq!(
415            EncodeOptions::with_quality(101).unwrap_err().code(),
416            ErrorCode::InvalidArgument
417        );
418        assert_eq!(EncodeOptions::default().quality, 80);
419    }
420
421    #[test]
422    fn format_names_are_stable_and_unique() {
423        let all = [
424            Format::Raw,
425            Format::Png,
426            Format::Jpeg,
427            Format::Gif,
428            Format::Tiff,
429            Format::WebP,
430            Format::Avif,
431        ];
432        let mut seen = std::collections::HashSet::new();
433        for f in all {
434            assert!(seen.insert(f.as_str()), "duplicate name {f}");
435        }
436        assert_eq!(Format::WebP.as_str(), "webp");
437    }
438
439    #[test]
440    fn metadata_mirrors_the_descriptor() {
441        let desc = ImageDescriptor::new(7, 5, PixelFormat::Rgba8).unwrap();
442        let meta = Metadata::new(&desc, Format::Png);
443        assert_eq!((meta.width, meta.height), (7, 5));
444        assert_eq!(meta.pixel, PixelFormat::Rgba8);
445        assert_eq!(meta.format, Format::Png);
446    }
447
448    #[test]
449    fn sequential_decoders_decline_region_decode() {
450        #[derive(Debug)]
451        struct Seq;
452        impl Decoder for Seq {
453            fn descriptor(&self) -> ImageDescriptor {
454                ImageDescriptor::new(1, 1, PixelFormat::Gray8).unwrap_or(ImageDescriptor {
455                    width: 1,
456                    height: 1,
457                    pixel: PixelFormat::Gray8,
458                    color: crate::ColorModel::Srgb,
459                })
460            }
461            fn read_row(&mut self, _: &mut [u8]) -> Result<()> {
462                Ok(())
463            }
464        }
465        let mut buf = crate::TileBuf::zeroed(Region::from_size(1, 1), PixelFormat::Gray8).unwrap();
466        let mut tile = buf.as_tile_mut().unwrap();
467        let err = Seq
468            .read_region(Region::from_size(1, 1), &mut tile)
469            .unwrap_err();
470        assert_eq!(err.code(), ErrorCode::Unsupported);
471        assert_eq!(Seq.capability(), DecodeCapability::Sequential);
472    }
473}