pnglitcher 0.2.0

PNG glitcher
Documentation
//! Common types shared between the encoder and decoder
use crate::text_metadata::{EncodableTextChunk, ITXtChunk, TEXtChunk, ZTXtChunk};
use crate::{chunk, encoder};
use io::Write;
use std::{borrow::Cow, fmt, io};

/// Describes how a pixel is encoded.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ColorType {
    /// 1 grayscale sample.
    Grayscale = 0,
    /// 1 red sample, 1 green sample, 1 blue sample.
    Rgb = 2,
    /// 1 sample for the palette index.
    Indexed = 3,
    /// 1 grayscale sample, then 1 alpha sample.
    GrayscaleAlpha = 4,
    /// 1 red sample, 1 green sample, 1 blue sample, and finally, 1 alpha sample.
    Rgba = 6,
}

impl ColorType {
    /// Returns the number of samples used per pixel encoded in this way.
    pub fn samples(self) -> usize {
        self.samples_u8().into()
    }

    pub(crate) fn samples_u8(self) -> u8 {
        use self::ColorType::*;
        match self {
            Grayscale | Indexed => 1,
            Rgb => 3,
            GrayscaleAlpha => 2,
            Rgba => 4,
        }
    }

    /// u8 -> Self. Temporary solution until Rust provides a canonical one.
    pub fn from_u8(n: u8) -> Option<ColorType> {
        match n {
            0 => Some(ColorType::Grayscale),
            2 => Some(ColorType::Rgb),
            3 => Some(ColorType::Indexed),
            4 => Some(ColorType::GrayscaleAlpha),
            6 => Some(ColorType::Rgba),
            _ => None,
        }
    }

    pub(crate) fn raw_row_length_from_width(self, depth: BitDepth, width: u32) -> usize {
        let samples = width as usize * self.samples();
        1 + match depth {
            BitDepth::Sixteen => samples * 2,
            BitDepth::Eight => samples,
            subbyte => {
                let samples_per_byte = 8 / subbyte as usize;
                let whole = samples / samples_per_byte;
                let fract = usize::from(samples % samples_per_byte > 0);
                whole + fract
            }
        }
    }

    pub(crate) fn is_combination_invalid(self, bit_depth: BitDepth) -> bool {
        // Section 11.2.2 of the PNG standard disallows several combinations
        // of bit depth and color type
        ((bit_depth == BitDepth::One || bit_depth == BitDepth::Two || bit_depth == BitDepth::Four)
            && (self == ColorType::Rgb
                || self == ColorType::GrayscaleAlpha
                || self == ColorType::Rgba))
            || (bit_depth == BitDepth::Sixteen && self == ColorType::Indexed)
    }
}

/// Bit depth of the PNG file.
/// Specifies the number of bits per sample.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum BitDepth {
    One = 1,
    Two = 2,
    Four = 4,
    Eight = 8,
    Sixteen = 16,
}

/// Internal count of bytes per pixel.
/// This is used for filtering which never uses sub-byte units. This essentially reduces the number
/// of possible byte chunk lengths to a very small set of values appropriate to be defined as an
/// enum.
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub(crate) enum BytesPerPixel {
    One = 1,
    Two = 2,
    Three = 3,
    Four = 4,
    Six = 6,
    Eight = 8,
}

impl BitDepth {
    /// u8 -> Self. Temporary solution until Rust provides a canonical one.
    pub fn from_u8(n: u8) -> Option<BitDepth> {
        match n {
            1 => Some(BitDepth::One),
            2 => Some(BitDepth::Two),
            4 => Some(BitDepth::Four),
            8 => Some(BitDepth::Eight),
            16 => Some(BitDepth::Sixteen),
            _ => None,
        }
    }
}

/// Pixel dimensions information
#[derive(Clone, Copy, Debug)]
pub struct PixelDimensions {
    /// Pixels per unit, X axis
    pub xppu: u32,
    /// Pixels per unit, Y axis
    pub yppu: u32,
    /// Either *Meter* or *Unspecified*
    pub unit: Unit,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
/// Physical unit of the pixel dimensions
pub enum Unit {
    Unspecified = 0,
    Meter = 1,
}

impl Unit {
    /// u8 -> Self. Temporary solution until Rust provides a canonical one.
    pub fn from_u8(n: u8) -> Option<Unit> {
        match n {
            0 => Some(Unit::Unspecified),
            1 => Some(Unit::Meter),
            _ => None,
        }
    }
}

/// How to reset buffer of an animated png (APNG) at the end of a frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum DisposeOp {
    /// Leave the buffer unchanged.
    None = 0,
    /// Clear buffer with the background color.
    Background = 1,
    /// Reset the buffer to the state before the current frame.
    Previous = 2,
}

impl DisposeOp {
    /// u8 -> Self. Using enum_primitive or transmute is probably the right thing but this will do for now.
    pub fn from_u8(n: u8) -> Option<DisposeOp> {
        match n {
            0 => Some(DisposeOp::None),
            1 => Some(DisposeOp::Background),
            2 => Some(DisposeOp::Previous),
            _ => None,
        }
    }
}

impl fmt::Display for DisposeOp {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let name = match *self {
            DisposeOp::None => "DISPOSE_OP_NONE",
            DisposeOp::Background => "DISPOSE_OP_BACKGROUND",
            DisposeOp::Previous => "DISPOSE_OP_PREVIOUS",
        };
        write!(f, "{}", name)
    }
}

/// How pixels are written into the buffer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum BlendOp {
    /// Pixels overwrite the value at their position.
    Source = 0,
    /// The new pixels are blended into the current state based on alpha.
    Over = 1,
}

impl BlendOp {
    /// u8 -> Self. Using enum_primitive or transmute is probably the right thing but this will do for now.
    pub fn from_u8(n: u8) -> Option<BlendOp> {
        match n {
            0 => Some(BlendOp::Source),
            1 => Some(BlendOp::Over),
            _ => None,
        }
    }
}

impl fmt::Display for BlendOp {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let name = match *self {
            BlendOp::Source => "BLEND_OP_SOURCE",
            BlendOp::Over => "BLEND_OP_OVER",
        };
        write!(f, "{}", name)
    }
}

/// Frame control information
#[derive(Clone, Copy, Debug)]
pub struct FrameControl {
    /// Sequence number of the animation chunk, starting from 0
    pub sequence_number: u32,
    /// Width of the following frame
    pub width: u32,
    /// Height of the following frame
    pub height: u32,
    /// X position at which to render the following frame
    pub x_offset: u32,
    /// Y position at which to render the following frame
    pub y_offset: u32,
    /// Frame delay fraction numerator
    pub delay_num: u16,
    /// Frame delay fraction denominator
    pub delay_den: u16,
    /// Type of frame area disposal to be done after rendering this frame
    pub dispose_op: DisposeOp,
    /// Type of frame area rendering for this frame
    pub blend_op: BlendOp,
}

impl Default for FrameControl {
    fn default() -> FrameControl {
        FrameControl {
            sequence_number: 0,
            width: 0,
            height: 0,
            x_offset: 0,
            y_offset: 0,
            delay_num: 1,
            delay_den: 30,
            dispose_op: DisposeOp::None,
            blend_op: BlendOp::Source,
        }
    }
}

impl FrameControl {
    pub fn set_seq_num(&mut self, s: u32) {
        self.sequence_number = s;
    }

    pub fn inc_seq_num(&mut self, i: u32) {
        self.sequence_number += i;
    }

    pub fn encode<W: Write>(self, w: &mut W) -> encoder::Result<()> {
        let mut data = [0u8; 26];
        data[..4].copy_from_slice(&self.sequence_number.to_be_bytes());
        data[4..8].copy_from_slice(&self.width.to_be_bytes());
        data[8..12].copy_from_slice(&self.height.to_be_bytes());
        data[12..16].copy_from_slice(&self.x_offset.to_be_bytes());
        data[16..20].copy_from_slice(&self.y_offset.to_be_bytes());
        data[20..22].copy_from_slice(&self.delay_num.to_be_bytes());
        data[22..24].copy_from_slice(&self.delay_den.to_be_bytes());
        data[24] = self.dispose_op as u8;
        data[25] = self.blend_op as u8;

        encoder::write_chunk(w, chunk::fcTL, &data)
    }
}

/// Animation control information
#[derive(Clone, Copy, Debug)]
pub struct AnimationControl {
    /// Number of frames
    pub num_frames: u32,
    /// Number of times to loop this APNG.  0 indicates infinite looping.
    pub num_plays: u32,
}

impl AnimationControl {
    pub fn encode<W: Write>(self, w: &mut W) -> encoder::Result<()> {
        let mut data = [0; 8];
        data[..4].copy_from_slice(&self.num_frames.to_be_bytes());
        data[4..].copy_from_slice(&self.num_plays.to_be_bytes());
        encoder::write_chunk(w, chunk::acTL, &data)
    }
}

/// The type and strength of applied compression.
#[derive(Debug, Clone, Copy)]
pub enum Compression {
    /// Default level
    Default,
    /// Fast minimal compression
    Fast,
    /// Higher compression level
    ///
    /// Best in this context isn't actually the highest possible level
    /// the encoder can do, but is meant to emulate the `Best` setting in the `Flate2`
    /// library.
    Best,
    Huffman,
    Rle,
}

/// An unsigned integer scaled version of a floating point value,
/// equivalent to an integer quotient with fixed denominator (100_000)).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScaledFloat(u32);

impl ScaledFloat {
    const SCALING: f32 = 100_000.0;

    /// Gets whether the value is within the clamped range of this type.
    pub fn in_range(value: f32) -> bool {
        value >= 0.0 && (value * Self::SCALING).floor() <= std::u32::MAX as f32
    }

    /// Gets whether the value can be exactly converted in round-trip.
    #[allow(clippy::float_cmp)] // Stupid tool, the exact float compare is _the entire point_.
    pub fn exact(value: f32) -> bool {
        let there = Self::forward(value);
        let back = Self::reverse(there);
        value == back
    }

    fn forward(value: f32) -> u32 {
        (value.max(0.0) * Self::SCALING).floor() as u32
    }

    fn reverse(encoded: u32) -> f32 {
        encoded as f32 / Self::SCALING
    }

    /// Slightly inaccurate scaling and quantization.
    /// Clamps the value into the representable range if it is negative or too large.
    pub fn new(value: f32) -> Self {
        Self {
            0: Self::forward(value),
        }
    }

    /// Fully accurate construction from a value scaled as per specification.
    pub fn from_scaled(val: u32) -> Self {
        Self { 0: val }
    }

    /// Get the accurate encoded value.
    pub fn into_scaled(self) -> u32 {
        self.0
    }

    /// Get the unscaled value as a floating point.
    pub fn into_value(self) -> f32 {
        Self::reverse(self.0) as f32
    }

    pub(crate) fn encode_gama<W: Write>(self, w: &mut W) -> encoder::Result<()> {
        encoder::write_chunk(w, chunk::gAMA, &self.into_scaled().to_be_bytes())
    }
}

/// Chromaticities of the color space primaries
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SourceChromaticities {
    pub white: (ScaledFloat, ScaledFloat),
    pub red: (ScaledFloat, ScaledFloat),
    pub green: (ScaledFloat, ScaledFloat),
    pub blue: (ScaledFloat, ScaledFloat),
}

impl SourceChromaticities {
    pub fn new(white: (f32, f32), red: (f32, f32), green: (f32, f32), blue: (f32, f32)) -> Self {
        SourceChromaticities {
            white: (ScaledFloat::new(white.0), ScaledFloat::new(white.1)),
            red: (ScaledFloat::new(red.0), ScaledFloat::new(red.1)),
            green: (ScaledFloat::new(green.0), ScaledFloat::new(green.1)),
            blue: (ScaledFloat::new(blue.0), ScaledFloat::new(blue.1)),
        }
    }

    #[rustfmt::skip]
    pub fn to_be_bytes(self) -> [u8; 32] {
        let white_x = self.white.0.into_scaled().to_be_bytes();
        let white_y = self.white.1.into_scaled().to_be_bytes();
        let red_x   = self.red.0.into_scaled().to_be_bytes();
        let red_y   = self.red.1.into_scaled().to_be_bytes();
        let green_x = self.green.0.into_scaled().to_be_bytes();
        let green_y = self.green.1.into_scaled().to_be_bytes();
        let blue_x  = self.blue.0.into_scaled().to_be_bytes();
        let blue_y  = self.blue.1.into_scaled().to_be_bytes();
        [
            white_x[0], white_x[1], white_x[2], white_x[3],
            white_y[0], white_y[1], white_y[2], white_y[3],
            red_x[0],   red_x[1],   red_x[2],   red_x[3],
            red_y[0],   red_y[1],   red_y[2],   red_y[3],
            green_x[0], green_x[1], green_x[2], green_x[3],
            green_y[0], green_y[1], green_y[2], green_y[3],
            blue_x[0],  blue_x[1],  blue_x[2],  blue_x[3],
            blue_y[0],  blue_y[1],  blue_y[2],  blue_y[3],
        ]
    }

    pub fn encode<W: Write>(self, w: &mut W) -> encoder::Result<()> {
        encoder::write_chunk(w, chunk::cHRM, &self.to_be_bytes())
    }
}

/// The rendering intent for an sRGB image.
///
/// Presence of this data also indicates that the image conforms to the sRGB color space.
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SrgbRenderingIntent {
    /// For images preferring good adaptation to the output device gamut at the expense of colorimetric accuracy, such as photographs.
    Perceptual = 0,
    /// For images requiring colour appearance matching (relative to the output device white point), such as logos.
    RelativeColorimetric = 1,
    /// For images preferring preservation of saturation at the expense of hue and lightness, such as charts and graphs.
    Saturation = 2,
    /// For images requiring preservation of absolute colorimetry, such as previews of images destined for a different output device (proofs).
    AbsoluteColorimetric = 3,
}

impl SrgbRenderingIntent {
    pub(crate) fn into_raw(self) -> u8 {
        self as u8
    }

    pub fn encode<W: Write>(self, w: &mut W) -> encoder::Result<()> {
        encoder::write_chunk(w, chunk::sRGB, &[self.into_raw()])
    }
}

/// PNG info struct
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Info<'a> {
    pub width: u32,
    pub height: u32,
    pub bit_depth: BitDepth,
    /// How colors are stored in the image.
    pub color_type: ColorType,
    pub interlaced: bool,
    /// The image's `tRNS` chunk, if present; contains the alpha channel of the image's palette, 1 byte per entry.
    pub trns: Option<Cow<'a, [u8]>>,
    pub pixel_dims: Option<PixelDimensions>,
    /// Gamma of the source system.
    pub source_gamma: Option<ScaledFloat>,
    /// The image's `PLTE` chunk, if present; contains the RGB channels (in that order) of the image's palettes, 3 bytes per entry (1 per channel).
    pub palette: Option<Cow<'a, [u8]>>,
    pub frame_control: Option<FrameControl>,
    pub animation_control: Option<AnimationControl>,
    pub compression: Compression,
    /// Chromaticities of the source system.
    pub source_chromaticities: Option<SourceChromaticities>,
    /// The rendering intent of an SRGB image.
    ///
    /// Presence of this value also indicates that the image conforms to the SRGB color space.
    pub srgb: Option<SrgbRenderingIntent>,
    /// The ICC profile for the image.
    pub icc_profile: Option<Cow<'a, [u8]>>,
    /// tEXt field
    pub uncompressed_latin1_text: Vec<TEXtChunk>,
    /// zTXt field
    pub compressed_latin1_text: Vec<ZTXtChunk>,
    /// iTXt field
    pub utf8_text: Vec<ITXtChunk>,
}

impl Default for Info<'_> {
    fn default() -> Info<'static> {
        Info {
            width: 0,
            height: 0,
            bit_depth: BitDepth::Eight,
            color_type: ColorType::Grayscale,
            interlaced: false,
            palette: None,
            trns: None,
            pixel_dims: None,
            source_gamma: None,
            frame_control: None,
            animation_control: None,
            // Default to `deflate::Compression::Fast` and `filter::FilterType::Sub`
            // to maintain backward compatible output.
            compression: Compression::Fast,
            source_chromaticities: None,
            srgb: None,
            icc_profile: None,
            uncompressed_latin1_text: Vec::new(),
            compressed_latin1_text: Vec::new(),
            utf8_text: Vec::new(),
        }
    }
}

impl Info<'_> {
    /// A utility constructor for a default info with width and height.
    pub fn with_size(width: u32, height: u32) -> Self {
        Info {
            width,
            height,
            ..Default::default()
        }
    }

    /// Size of the image, width then height.
    pub fn size(&self) -> (u32, u32) {
        (self.width, self.height)
    }

    /// Returns true if the image is an APNG image.
    pub fn is_animated(&self) -> bool {
        self.frame_control.is_some() && self.animation_control.is_some()
    }

    /// Returns the frame control information of the image.
    pub fn animation_control(&self) -> Option<&AnimationControl> {
        self.animation_control.as_ref()
    }

    /// Returns the frame control information of the current frame
    pub fn frame_control(&self) -> Option<&FrameControl> {
        self.frame_control.as_ref()
    }

    /// Returns the number of bits per pixel.
    pub fn bits_per_pixel(&self) -> usize {
        self.color_type.samples() * self.bit_depth as usize
    }

    /// Returns the number of bytes per pixel.
    pub fn bytes_per_pixel(&self) -> usize {
        // If adjusting this for expansion or other transformation passes, remember to keep the old
        // implementation for bpp_in_prediction, which is internal to the png specification.
        self.color_type.samples() * ((self.bit_depth as usize + 7) >> 3)
    }

    /// Return the number of bytes for this pixel used in prediction.
    ///
    /// Some filters use prediction, over the raw bytes of a scanline. Where a previous pixel is
    /// require for such forms the specification instead references previous bytes. That is, for
    /// a gray pixel of bit depth 2, the pixel used in prediction is actually 4 pixels prior. This
    /// has the consequence that the number of possible values is rather small. To make this fact
    /// more obvious in the type system and the optimizer we use an explicit enum here.
    pub(crate) fn bpp_in_prediction(&self) -> BytesPerPixel {
        match self.bytes_per_pixel() {
            1 => BytesPerPixel::One,
            2 => BytesPerPixel::Two,
            3 => BytesPerPixel::Three,
            4 => BytesPerPixel::Four,
            6 => BytesPerPixel::Six,   // Only rgb×16bit
            8 => BytesPerPixel::Eight, // Only rgba×16bit
            _ => unreachable!("Not a possible byte rounded pixel width"),
        }
    }

    /// Returns the number of bytes needed for one deinterlaced image.
    pub fn raw_bytes(&self) -> usize {
        self.height as usize * self.raw_row_length()
    }

    /// Returns the number of bytes needed for one deinterlaced row.
    pub fn raw_row_length(&self) -> usize {
        self.raw_row_length_from_width(self.width)
    }

    /// Returns the number of bytes needed for one deinterlaced row of width `width`.
    pub fn raw_row_length_from_width(&self, width: u32) -> usize {
        self.color_type
            .raw_row_length_from_width(self.bit_depth, width)
    }

    /// Encode this header to the writer.
    ///
    /// Note that this does _not_ include the PNG signature, it starts with the IHDR chunk and then
    /// includes other chunks that were added to the header.
    pub fn encode<W: Write>(&self, mut w: W) -> encoder::Result<()> {
        // Encode the IHDR chunk
        let mut data = [0; 13];
        data[..4].copy_from_slice(&self.width.to_be_bytes());
        data[4..8].copy_from_slice(&self.height.to_be_bytes());
        data[8] = self.bit_depth as u8;
        data[9] = self.color_type as u8;
        data[12] = self.interlaced as u8;
        encoder::write_chunk(&mut w, chunk::IHDR, &data)?;

        if let Some(p) = &self.palette {
            encoder::write_chunk(&mut w, chunk::PLTE, p)?;
        };

        if let Some(t) = &self.trns {
            encoder::write_chunk(&mut w, chunk::tRNS, t)?;
        }

        // If specified, the sRGB information overrides the source gamma and chromaticities.
        if let Some(srgb) = &self.srgb {
            let gamma = crate::srgb::substitute_gamma();
            let chromaticities = crate::srgb::substitute_chromaticities();
            srgb.encode(&mut w)?;
            gamma.encode_gama(&mut w)?;
            chromaticities.encode(&mut w)?;
        } else {
            if let Some(gma) = self.source_gamma {
                gma.encode_gama(&mut w)?
            }
            if let Some(chrms) = self.source_chromaticities {
                chrms.encode(&mut w)?;
            }
        }
        if let Some(actl) = self.animation_control {
            actl.encode(&mut w)?;
        }

        for text_chunk in &self.uncompressed_latin1_text {
            text_chunk.encode(&mut w)?;
        }

        for text_chunk in &self.compressed_latin1_text {
            text_chunk.encode(&mut w)?;
        }

        for text_chunk in &self.utf8_text {
            text_chunk.encode(&mut w)?;
        }

        Ok(())
    }
}

impl BytesPerPixel {
    pub(crate) fn into_usize(self) -> usize {
        self as usize
    }
}

bitflags! {
    /// Output transformations
    ///
    /// Many flags from libpng are not yet supported. A PR discussing/adding them would be nice.
    ///
    #[doc = "
    ```c
    /// Discard the alpha channel
    const STRIP_ALPHA         = 0x0002; // read only
    /// Expand 1; 2 and 4-bit samples to bytes
    const PACKING             = 0x0004; // read and write
    /// Change order of packed pixels to LSB first
    const PACKSWAP            = 0x0008; // read and write
    /// Invert monochrome images
    const INVERT_MONO         = 0x0020; // read and write
    /// Normalize pixels to the sBIT depth
    const SHIFT               = 0x0040; // read and write
    /// Flip RGB to BGR; RGBA to BGRA
    const BGR                 = 0x0080; // read and write
    /// Flip RGBA to ARGB or GA to AG
    const SWAP_ALPHA          = 0x0100; // read and write
    /// Byte-swap 16-bit samples
    const SWAP_ENDIAN         = 0x0200; // read and write
    /// Change alpha from opacity to transparency
    const INVERT_ALPHA        = 0x0400; // read and write
    const STRIP_FILLER        = 0x0800; // write only
    const STRIP_FILLER_BEFORE = 0x0800; // write only
    const STRIP_FILLER_AFTER  = 0x1000; // write only
    const GRAY_TO_RGB         = 0x2000; // read only
    const EXPAND_16           = 0x4000; // read only
    /// Similar to STRIP_16 but in libpng considering gamma?
    /// Not entirely sure the documentation says it is more
    /// accurate but doesn't say precisely how.
    const SCALE_16            = 0x8000; // read only
    ```
    "]
    pub struct Transformations: u32 {
        /// No transformation
        const IDENTITY            = 0x0000; // read and write */
        /// Strip 16-bit samples to 8 bits
        const STRIP_16            = 0x0001; // read only */
        /// Expand paletted images to RGB; expand grayscale images of
        /// less than 8-bit depth to 8-bit depth; and expand tRNS chunks
        /// to alpha channels.
        const EXPAND              = 0x0010; // read only */
    }
}

impl Transformations {
    /// Transform every input to 8bit grayscale or color.
    ///
    /// This sets `EXPAND` and `STRIP_16` which is similar to the default transformation used by
    /// this library prior to `0.17`.
    pub fn normalize_to_color8() -> Transformations {
        Transformations::EXPAND | Transformations::STRIP_16
    }
}

/// Instantiate the default transformations, the identity transform.
impl Default for Transformations {
    fn default() -> Transformations {
        Transformations::IDENTITY
    }
}

#[derive(Debug)]
pub struct ParameterError {
    inner: ParameterErrorKind,
}

#[derive(Debug)]
pub(crate) enum ParameterErrorKind {
    /// A provided buffer must be have the exact size to hold the image data. Where the buffer can
    /// be allocated by the caller, they must ensure that it has a minimum size as hinted previously.
    /// Even though the size is calculated from image data, this does counts as a parameter error
    /// because they must react to a value produced by this library, which can have been subjected
    /// to limits.
    ImageBufferSize { expected: usize, actual: usize },
    /// A bit like return `None` from an iterator.
    /// We use it to differentiate between failing to seek to the next image in a sequence and the
    /// absence of a next image. This is an error of the caller because they should have checked
    /// the number of images by inspecting the header data returned when opening the image. This
    /// library will perform the checks necessary to ensure that data was accurate or error with a
    /// format error otherwise.
    PolledAfterEndOfImage,
}

impl From<ParameterErrorKind> for ParameterError {
    fn from(inner: ParameterErrorKind) -> Self {
        ParameterError { inner }
    }
}

impl fmt::Display for ParameterError {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        use ParameterErrorKind::*;
        match self.inner {
            ImageBufferSize { expected, actual } => {
                write!(fmt, "wrong data size, expected {} got {}", expected, actual)
            }
            PolledAfterEndOfImage => write!(fmt, "End of image has been reached"),
        }
    }
}