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}