fakejpeg 0.2.0

Rust port of Alun Jones' fakejpeg library
Documentation
// SPDX-FileCopyrightText: Alun Jones
// SPDX-FileCopyrightText: Gergely Nagy
// SPDX-FileContributor: Gergely Nagy
//
// SPDX-License-Identifier: MIT

pub mod config;

use crate::{Config, Result, Template, masked_rng::MaskedRng, template::chunk::Marker};
use rand::Rng;

/// Generate randomized images based on a [`Template`] and a [`Config`].
///
/// An `ImageGenerator` is cheap to construct and throw away.
///
/// # Examples
///
/// ```
/// use fakejpeg::{Config, ImageGenerator, Template};
/// use std::io::{self, Write};
///
/// # static EXAMPLE_IMAGE: &'static [u8] = include_bytes!("../../benches/data/64x64.jpg");
/// #
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let images = [
///       // images come here...
/// #      EXAMPLE_IMAGE,
///     ];
///     let template = Template::from_inputs(&images)?;
///     let generator = ImageGenerator::from(&template);
///
///     let image = generator.emit(Config::default())?;
///     io::stdout().write_all(&image)?;
///
///     Ok(())
/// }
/// ```
#[derive(Debug)]
pub struct ImageGenerator<'t> {
    template: &'t Template,
}

impl ImageGenerator<'_> {
    /// Emit a randomized image from the template.
    ///
    /// Some properties of the generated image can be configured, see
    /// [`Config`].
    ///
    /// # Errors
    ///
    /// Returns [`Error::EmptyTemplate`](crate::Error::EmptyTemplate) if the
    /// associated template is empty.
    #[allow(clippy::cast_possible_truncation)]
    #[allow(clippy::cast_precision_loss)]
    #[allow(clippy::cast_sign_loss)]
    pub fn emit<T: Rng>(&self, mut config: Config<T>) -> Result<Vec<u8>> {
        let template = self.template.choose(&mut config.rng)?;

        let mut bytes = Vec::with_capacity(template.minimum_size);
        let mut masked_rng: MaskedRng<T, 0x6d6d_6d6d_6d6d_6d6d> = config.rng.into();

        for chunk in &template.chunks {
            bytes.extend(&chunk.data);

            if chunk.remaining > 0 {
                let size = masked_rng.random_range(
                    chunk.remaining
                        ..=(chunk.remaining as f64 * config.options.size_variance) as usize,
                );
                let mut garbage = vec![0; size];
                masked_rng.fill_bytes(&mut garbage);
                bytes.append(&mut garbage);
            }
        }

        if let Some(comment) = &config.options.comment {
            bytes.extend(Marker::COM.to_be_bytes());
            bytes.extend(((comment.len() + 2) as u16).to_be_bytes());
            bytes.extend(comment.as_bytes());
        }

        // Always append an EOI marker: the template does not have one, to make
        // it easier to shove things before the marker.
        bytes.extend(Marker::EOI.to_be_bytes());

        Ok(bytes)
    }
}

impl<'t> From<&'t Template> for ImageGenerator<'t> {
    /// Convert a [`Template`] reference to an [`ImageGenerator`].
    fn from(template: &'t Template) -> Self {
        Self { template }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_generated_image() {
        let source = std::fs::read("benches/data/1024x1024.jpg").unwrap();
        let template = Template::try_from(source).unwrap();
        let image = ImageGenerator::from(&template)
            .emit(Config::default())
            .unwrap();

        assert!(
            Template::try_from(image).is_ok(),
            "Generated image could not be parsed back"
        );
    }
}