mbed-image 0.1.7

Embed and transform assets into your Rust crate.
Documentation
use std::{
    borrow::Cow::Borrowed,
    io::{Cursor, Write},
};

#[cfg(feature = "avif")]
use image::codecs::avif::AvifEncoder;
use image::{
    DynamicImage, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageReader, RgbaImage,
    codecs::{
        bmp::BmpEncoder, farbfeld::FarbfeldEncoder, gif::GifEncoder, hdr::HdrEncoder,
        ico::IcoEncoder, jpeg::JpegEncoder, openexr::OpenExrEncoder, png::PngEncoder,
        pnm::PnmEncoder, qoi::QoiEncoder, tga::TgaEncoder, tiff::TiffEncoder, webp::WebPEncoder,
    },
    error::ImageFormatHint,
    imageops::FilterType,
};
use mbed_macros::{Artifact, Bytes, LocalFile, Writer};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde::{Deserialize, Serialize};
use tanager::Parse;

#[proc_macro]
pub fn include(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
    mbed_macros::execute_fn(item.into(), |deps, input: Input| {
        deps.track_file(&input.path);

        let reader = ImageReader::open(&input.path)?;

        let format = input
            .format
            .or_else(|| reader.format().and_then(|x| x.try_into().ok()))
            .ok_or_else(|| ImageError::Unsupported(ImageFormatHint::Unknown.into()))?;

        let mut decoder = reader.into_decoder()?;
        let orientation = decoder.orientation()?;

        let mut image = DynamicImage::from_decoder(decoder)?;

        image.apply_orientation(orientation);

        let image = match input.resize {
            Some(Resize::Exact {
                keep_aspect_ratio: true,
                width,
                height,
            }) => image.resize(width, height, FilterType::Lanczos3),

            Some(Resize::Exact {
                keep_aspect_ratio: false,
                width,
                height,
            }) => image.resize_exact(width, height, FilterType::Lanczos3),

            Some(Resize::Scale(x)) => image.resize(
                (image.width() as f64 * x) as u32,
                (image.height() as f64 * x) as u32,
                FilterType::Lanczos3,
            ),

            None => image,
        };

        let (width, height) = (image.width(), image.height());

        let bytes = encode(&image.into_rgba8(), format, input.compression)?;

        let mut writer = Writer::new()?;
        writer.write_all(&bytes)?;
        let artifact = writer.finish()?;

        Result::Ok(Artifact {
            mime: Borrowed(format.mime()),
            id: artifact.id,

            value: Image {
                format,
                width,
                height,
                bytes: artifact.value,
            },
        })
    })
    .into()
}

fn encode(image: &RgbaImage, format: Format, compression: Compression) -> Result<Vec<u8>> {
    macro_rules! encode {
        ($image:expr, $encoder:expr) => {{
            $encoder.write_image(
                $image.as_raw(),
                $image.width(),
                $image.height(),
                image::ExtendedColorType::Rgba8,
            )?;
        }};
    }

    let mut bytes = Vec::new();

    match format {
        Format::Png => encode!(
            image,
            PngEncoder::new_with_quality(
                Cursor::new(&mut bytes),
                match compression {
                    Compression::Best => image::codecs::png::CompressionType::Best,
                    Compression::Fast => image::codecs::png::CompressionType::Fast,
                    Compression::Uncompressed => image::codecs::png::CompressionType::Uncompressed,
                    Compression::Balanced => image::codecs::png::CompressionType::Default,
                },
                image::codecs::png::FilterType::Adaptive,
            )
        ),

        Format::Jpeg => encode!(
            image,
            JpegEncoder::new_with_quality(
                Cursor::new(&mut bytes),
                match compression {
                    Compression::Best => 95,
                    Compression::Fast => 65,
                    Compression::Uncompressed => 100,
                    Compression::Balanced => 80,
                },
            )
        ),

        Format::Gif => encode!(
            image,
            GifEncoder::new_with_speed(
                Cursor::new(&mut bytes),
                match compression {
                    Compression::Best => 1,
                    Compression::Fast => 25,
                    Compression::Uncompressed => 30,
                    Compression::Balanced => 10,
                }
            )
        ),

        Format::WebP => encode!(image, WebPEncoder::new_lossless(Cursor::new(&mut bytes))),
        Format::Pnm => encode!(image, PnmEncoder::new(Cursor::new(&mut bytes))),
        Format::Tiff => encode!(image, TiffEncoder::new(Cursor::new(&mut bytes))),
        Format::Tga => encode!(image, TgaEncoder::new(Cursor::new(&mut bytes))),
        Format::Bmp => encode!(image, BmpEncoder::new(&mut Cursor::new(&mut bytes))),
        Format::Ico => encode!(image, IcoEncoder::new(Cursor::new(&mut bytes))),
        Format::Hdr => encode!(image, HdrEncoder::new(Cursor::new(&mut bytes))),
        Format::OpenExr => encode!(image, OpenExrEncoder::new(Cursor::new(&mut bytes))),
        Format::Farbfeld => encode!(image, FarbfeldEncoder::new(Cursor::new(&mut bytes))),

        #[cfg(feature = "avif")]
        Format::Avif => {
            let (speed, quality) = match compression {
                Compression::Best => (1, 95),
                Compression::Fast => (10, 65),
                Compression::Uncompressed => (1, 100),
                Compression::Balanced => (6, 80),
            };

            encode!(
                image,
                AvifEncoder::new_with_speed_quality(Cursor::new(&mut bytes), speed, quality)
            )
        }

        Format::Qoi => encode!(image, QoiEncoder::new(Cursor::new(&mut bytes))),
    };

    Ok(bytes)
}

#[derive(Debug, Hash, Parse)]
struct Input {
    #[tanager(default)]
    format: Option<Format>,

    path: LocalFile,

    #[tanager(default = Compression::Balanced)]
    compression: Compression,

    #[tanager(default)]
    resize: Option<Resize>,
}

impl std::fmt::Display for Input {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "mbed::image::include {{ path: {} }}",
            std::env::current_dir()
                .ok()
                .and_then(|cwd| self.path.strip_prefix(cwd).ok())
                .unwrap_or(self.path.as_path())
                .to_string_lossy()
        )
    }
}

#[derive(Debug, Parse)]
enum Resize {
    Exact {
        #[tanager(default = true)]
        keep_aspect_ratio: bool,

        width: u32,
        height: u32,
    },

    Scale(f64),
}

impl std::hash::Hash for Resize {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        ::core::mem::discriminant(self).hash(state);

        match self {
            Self::Exact {
                keep_aspect_ratio,
                width,
                height,
            } => {
                keep_aspect_ratio.hash(state);
                width.hash(state);
                height.hash(state);
            }

            Resize::Scale(x) => {
                x.to_bits().hash(state);
            }
        }
    }
}

type Result<T> = ::core::result::Result<T, Error>;

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error(transparent)]
    Image(#[from] image::ImageError),

    #[error(transparent)]
    Io(#[from] std::io::Error),
}

#[derive(Debug, Serialize, Deserialize)]
struct Image {
    format: Format,
    width: u32,
    height: u32,
    bytes: Bytes,
}

impl ToTokens for Image {
    #[inline]
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let Self {
            format,
            width,
            height,
            bytes,
        } = self;

        quote! {{
            const IMAGE: mbed::image::__macro::Image = mbed::image::__macro::image(#format, #width, #height, #bytes);

            &IMAGE
        }}
        .to_tokens(tokens);
    }
}

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Parse, Serialize, Deserialize,
)]
enum Format {
    Png,
    Jpeg,
    Gif,
    WebP,
    Pnm,
    Tiff,
    Tga,
    Bmp,
    Ico,
    Hdr,
    OpenExr,
    Farbfeld,
    #[cfg(feature = "avif")]
    Avif,
    Qoi,
}

impl Format {
    #[inline]
    const fn mime(&self) -> &'static str {
        match self {
            Self::Png => "image/png",
            Self::Jpeg => "image/jpeg",
            Self::Gif => "image/gif",
            Self::WebP => "image/webp",
            Self::Pnm => "image/x-portable-anymap",
            Self::Tiff => "image/tiff",
            Self::Tga => "image/x-tga",
            Self::Bmp => "image/bmp",
            Self::Ico => "image/vnd.microsoft.icon",
            Self::Hdr => "image/vnd.radiance",
            Self::OpenExr => "image/x-exr",
            Self::Farbfeld => "image/farbfeld",
            #[cfg(feature = "avif")]
            Self::Avif => "image/avif",
            Self::Qoi => "image/qoi",
        }
    }
}

impl ToTokens for Format {
    #[inline]
    fn to_tokens(&self, tokens: &mut TokenStream) {
        match self {
            Self::Png => quote! { mbed::image::Format::Png }.to_tokens(tokens),
            Self::Jpeg => quote! { mbed::image::Format::Jpeg }.to_tokens(tokens),
            Self::Gif => quote! { mbed::image::Format::Gif }.to_tokens(tokens),
            Self::WebP => quote! { mbed::image::Format::WebP }.to_tokens(tokens),
            Self::Pnm => quote! { mbed::image::Format::Pnm }.to_tokens(tokens),
            Self::Tiff => quote! { mbed::image::Format::Tiff }.to_tokens(tokens),
            Self::Tga => quote! { mbed::image::Format::Tga }.to_tokens(tokens),
            Self::Bmp => quote! { mbed::image::Format::Bmp }.to_tokens(tokens),
            Self::Ico => quote! { mbed::image::Format::Ico }.to_tokens(tokens),
            Self::Hdr => quote! { mbed::image::Format::Hdr }.to_tokens(tokens),
            Self::OpenExr => quote! { mbed::image::Format::OpenExr }.to_tokens(tokens),
            Self::Farbfeld => quote! { mbed::image::Format::Farbfeld }.to_tokens(tokens),
            #[cfg(feature = "avif")]
            Self::Avif => quote! { mbed::image::Format::Avif }.to_tokens(tokens),
            Self::Qoi => quote! { mbed::image::Format::Qoi }.to_tokens(tokens),
        }
    }
}

impl TryFrom<ImageFormat> for Format {
    type Error = ImageError;

    fn try_from(value: ImageFormat) -> std::result::Result<Self, Self::Error> {
        match value {
            ImageFormat::Png => Ok(Format::Png),
            ImageFormat::Jpeg => Ok(Format::Jpeg),
            ImageFormat::Gif => Ok(Format::Gif),
            ImageFormat::WebP => Ok(Format::WebP),
            ImageFormat::Pnm => Ok(Format::Pnm),
            ImageFormat::Tiff => Ok(Format::Tiff),
            ImageFormat::Tga => Ok(Format::Tga),
            ImageFormat::Bmp => Ok(Format::Bmp),
            ImageFormat::Ico => Ok(Format::Ico),
            ImageFormat::Hdr => Ok(Format::Hdr),
            ImageFormat::OpenExr => Ok(Format::OpenExr),
            ImageFormat::Farbfeld => Ok(Format::Farbfeld),
            #[cfg(feature = "avif")]
            ImageFormat::Avif => Ok(Format::Avif),
            ImageFormat::Qoi => Ok(Format::Qoi),

            _ => Err(ImageError::Unsupported(ImageFormatHint::Unknown.into())),
        }
    }
}

impl From<Format> for ImageFormat {
    fn from(value: Format) -> Self {
        match value {
            Format::Png => Self::Png,
            Format::Jpeg => Self::Jpeg,
            Format::Gif => Self::Gif,
            Format::WebP => Self::WebP,
            Format::Pnm => Self::Pnm,
            Format::Tiff => Self::Tiff,
            Format::Tga => Self::Tga,
            Format::Bmp => Self::Bmp,
            Format::Ico => Self::Ico,
            Format::Hdr => Self::Hdr,
            Format::OpenExr => Self::OpenExr,
            Format::Farbfeld => Self::Farbfeld,
            #[cfg(feature = "avif")]
            Format::Avif => Self::Avif,
            Format::Qoi => Self::Qoi,
        }
    }
}

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Parse, Serialize, Deserialize,
)]
enum Compression {
    Best,
    Fast,
    Uncompressed,
    Balanced,
}