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}