Skip to main content

mbed_image/
lib.rs

1use std::{
2    borrow::Cow::Borrowed,
3    io::{Cursor, Write},
4};
5
6#[cfg(feature = "avif")]
7use image::codecs::avif::AvifEncoder;
8use image::{
9    DynamicImage, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageReader, RgbaImage,
10    codecs::{
11        bmp::BmpEncoder, farbfeld::FarbfeldEncoder, gif::GifEncoder, hdr::HdrEncoder,
12        ico::IcoEncoder, jpeg::JpegEncoder, openexr::OpenExrEncoder, png::PngEncoder,
13        pnm::PnmEncoder, qoi::QoiEncoder, tga::TgaEncoder, tiff::TiffEncoder, webp::WebPEncoder,
14    },
15    error::ImageFormatHint,
16    imageops::FilterType,
17};
18use mbed_macros::{Artifact, Bytes, LocalFile, Writer};
19use proc_macro2::TokenStream;
20use quote::{ToTokens, quote};
21use serde::{Deserialize, Serialize};
22use tanager::Parse;
23
24#[proc_macro]
25pub fn include(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
26    mbed_macros::execute_fn(item.into(), |deps, input: Input| {
27        deps.track_file(&input.path);
28
29        let reader = ImageReader::open(&input.path)?;
30
31        let format = input
32            .format
33            .or_else(|| reader.format().and_then(|x| x.try_into().ok()))
34            .ok_or_else(|| ImageError::Unsupported(ImageFormatHint::Unknown.into()))?;
35
36        let mut decoder = reader.into_decoder()?;
37        let orientation = decoder.orientation()?;
38
39        let mut image = DynamicImage::from_decoder(decoder)?;
40
41        image.apply_orientation(orientation);
42
43        let image = match input.resize {
44            Some(Resize::Exact {
45                keep_aspect_ratio: true,
46                width,
47                height,
48            }) => image.resize(width, height, FilterType::Lanczos3),
49
50            Some(Resize::Exact {
51                keep_aspect_ratio: false,
52                width,
53                height,
54            }) => image.resize_exact(width, height, FilterType::Lanczos3),
55
56            Some(Resize::Scale(x)) => image.resize(
57                (image.width() as f64 * x) as u32,
58                (image.height() as f64 * x) as u32,
59                FilterType::Lanczos3,
60            ),
61
62            None => image,
63        };
64
65        let (width, height) = (image.width(), image.height());
66
67        let bytes = encode(&image.into_rgba8(), format, input.compression)?;
68
69        let mut writer = Writer::new()?;
70        writer.write_all(&bytes)?;
71        let artifact = writer.finish()?;
72
73        Result::Ok(Artifact {
74            mime: Borrowed(format.mime()),
75            id: artifact.id,
76
77            value: Image {
78                format,
79                width,
80                height,
81                bytes: artifact.value,
82            },
83        })
84    })
85    .into()
86}
87
88fn encode(image: &RgbaImage, format: Format, compression: Compression) -> Result<Vec<u8>> {
89    macro_rules! encode {
90        ($image:expr, $encoder:expr) => {{
91            $encoder.write_image(
92                $image.as_raw(),
93                $image.width(),
94                $image.height(),
95                image::ExtendedColorType::Rgba8,
96            )?;
97        }};
98    }
99
100    let mut bytes = Vec::new();
101
102    match format {
103        Format::Png => encode!(
104            image,
105            PngEncoder::new_with_quality(
106                Cursor::new(&mut bytes),
107                match compression {
108                    Compression::Best => image::codecs::png::CompressionType::Best,
109                    Compression::Fast => image::codecs::png::CompressionType::Fast,
110                    Compression::Uncompressed => image::codecs::png::CompressionType::Uncompressed,
111                    Compression::Balanced => image::codecs::png::CompressionType::Default,
112                },
113                image::codecs::png::FilterType::Adaptive,
114            )
115        ),
116
117        Format::Jpeg => encode!(
118            image,
119            JpegEncoder::new_with_quality(
120                Cursor::new(&mut bytes),
121                match compression {
122                    Compression::Best => 95,
123                    Compression::Fast => 65,
124                    Compression::Uncompressed => 100,
125                    Compression::Balanced => 80,
126                },
127            )
128        ),
129
130        Format::Gif => encode!(
131            image,
132            GifEncoder::new_with_speed(
133                Cursor::new(&mut bytes),
134                match compression {
135                    Compression::Best => 1,
136                    Compression::Fast => 25,
137                    Compression::Uncompressed => 30,
138                    Compression::Balanced => 10,
139                }
140            )
141        ),
142
143        Format::WebP => encode!(image, WebPEncoder::new_lossless(Cursor::new(&mut bytes))),
144        Format::Pnm => encode!(image, PnmEncoder::new(Cursor::new(&mut bytes))),
145        Format::Tiff => encode!(image, TiffEncoder::new(Cursor::new(&mut bytes))),
146        Format::Tga => encode!(image, TgaEncoder::new(Cursor::new(&mut bytes))),
147        Format::Bmp => encode!(image, BmpEncoder::new(&mut Cursor::new(&mut bytes))),
148        Format::Ico => encode!(image, IcoEncoder::new(Cursor::new(&mut bytes))),
149        Format::Hdr => encode!(image, HdrEncoder::new(Cursor::new(&mut bytes))),
150        Format::OpenExr => encode!(image, OpenExrEncoder::new(Cursor::new(&mut bytes))),
151        Format::Farbfeld => encode!(image, FarbfeldEncoder::new(Cursor::new(&mut bytes))),
152
153        #[cfg(feature = "avif")]
154        Format::Avif => {
155            let (speed, quality) = match compression {
156                Compression::Best => (1, 95),
157                Compression::Fast => (10, 65),
158                Compression::Uncompressed => (1, 100),
159                Compression::Balanced => (6, 80),
160            };
161
162            encode!(
163                image,
164                AvifEncoder::new_with_speed_quality(Cursor::new(&mut bytes), speed, quality)
165            )
166        }
167
168        Format::Qoi => encode!(image, QoiEncoder::new(Cursor::new(&mut bytes))),
169    };
170
171    Ok(bytes)
172}
173
174#[derive(Debug, Hash, Parse)]
175struct Input {
176    #[tanager(default)]
177    format: Option<Format>,
178
179    path: LocalFile,
180
181    #[tanager(default = Compression::Balanced)]
182    compression: Compression,
183
184    #[tanager(default)]
185    resize: Option<Resize>,
186}
187
188impl std::fmt::Display for Input {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        write!(
191            f,
192            "mbed::image::include {{ path: {} }}",
193            std::env::current_dir()
194                .ok()
195                .and_then(|cwd| self.path.strip_prefix(cwd).ok())
196                .unwrap_or(self.path.as_path())
197                .to_string_lossy()
198        )
199    }
200}
201
202#[derive(Debug, Parse)]
203enum Resize {
204    Exact {
205        #[tanager(default = true)]
206        keep_aspect_ratio: bool,
207
208        width: u32,
209        height: u32,
210    },
211
212    Scale(f64),
213}
214
215impl std::hash::Hash for Resize {
216    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
217        ::core::mem::discriminant(self).hash(state);
218
219        match self {
220            Self::Exact {
221                keep_aspect_ratio,
222                width,
223                height,
224            } => {
225                keep_aspect_ratio.hash(state);
226                width.hash(state);
227                height.hash(state);
228            }
229
230            Resize::Scale(x) => {
231                x.to_bits().hash(state);
232            }
233        }
234    }
235}
236
237type Result<T> = ::core::result::Result<T, Error>;
238
239#[derive(Debug, thiserror::Error)]
240enum Error {
241    #[error(transparent)]
242    Image(#[from] image::ImageError),
243
244    #[error(transparent)]
245    Io(#[from] std::io::Error),
246}
247
248#[derive(Debug, Serialize, Deserialize)]
249struct Image {
250    format: Format,
251    width: u32,
252    height: u32,
253    bytes: Bytes,
254}
255
256impl ToTokens for Image {
257    #[inline]
258    fn to_tokens(&self, tokens: &mut TokenStream) {
259        let Self {
260            format,
261            width,
262            height,
263            bytes,
264        } = self;
265
266        quote! {{
267            const IMAGE: mbed::image::__macro::Image = mbed::image::__macro::image(#format, #width, #height, #bytes);
268
269            &IMAGE
270        }}
271        .to_tokens(tokens);
272    }
273}
274
275#[derive(
276    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Parse, Serialize, Deserialize,
277)]
278enum Format {
279    Png,
280    Jpeg,
281    Gif,
282    WebP,
283    Pnm,
284    Tiff,
285    Tga,
286    Bmp,
287    Ico,
288    Hdr,
289    OpenExr,
290    Farbfeld,
291    #[cfg(feature = "avif")]
292    Avif,
293    Qoi,
294}
295
296impl Format {
297    #[inline]
298    const fn mime(&self) -> &'static str {
299        match self {
300            Self::Png => "image/png",
301            Self::Jpeg => "image/jpeg",
302            Self::Gif => "image/gif",
303            Self::WebP => "image/webp",
304            Self::Pnm => "image/x-portable-anymap",
305            Self::Tiff => "image/tiff",
306            Self::Tga => "image/x-tga",
307            Self::Bmp => "image/bmp",
308            Self::Ico => "image/vnd.microsoft.icon",
309            Self::Hdr => "image/vnd.radiance",
310            Self::OpenExr => "image/x-exr",
311            Self::Farbfeld => "image/farbfeld",
312            #[cfg(feature = "avif")]
313            Self::Avif => "image/avif",
314            Self::Qoi => "image/qoi",
315        }
316    }
317}
318
319impl ToTokens for Format {
320    #[inline]
321    fn to_tokens(&self, tokens: &mut TokenStream) {
322        match self {
323            Self::Png => quote! { mbed::image::Format::Png }.to_tokens(tokens),
324            Self::Jpeg => quote! { mbed::image::Format::Jpeg }.to_tokens(tokens),
325            Self::Gif => quote! { mbed::image::Format::Gif }.to_tokens(tokens),
326            Self::WebP => quote! { mbed::image::Format::WebP }.to_tokens(tokens),
327            Self::Pnm => quote! { mbed::image::Format::Pnm }.to_tokens(tokens),
328            Self::Tiff => quote! { mbed::image::Format::Tiff }.to_tokens(tokens),
329            Self::Tga => quote! { mbed::image::Format::Tga }.to_tokens(tokens),
330            Self::Bmp => quote! { mbed::image::Format::Bmp }.to_tokens(tokens),
331            Self::Ico => quote! { mbed::image::Format::Ico }.to_tokens(tokens),
332            Self::Hdr => quote! { mbed::image::Format::Hdr }.to_tokens(tokens),
333            Self::OpenExr => quote! { mbed::image::Format::OpenExr }.to_tokens(tokens),
334            Self::Farbfeld => quote! { mbed::image::Format::Farbfeld }.to_tokens(tokens),
335            #[cfg(feature = "avif")]
336            Self::Avif => quote! { mbed::image::Format::Avif }.to_tokens(tokens),
337            Self::Qoi => quote! { mbed::image::Format::Qoi }.to_tokens(tokens),
338        }
339    }
340}
341
342impl TryFrom<ImageFormat> for Format {
343    type Error = ImageError;
344
345    fn try_from(value: ImageFormat) -> std::result::Result<Self, Self::Error> {
346        match value {
347            ImageFormat::Png => Ok(Format::Png),
348            ImageFormat::Jpeg => Ok(Format::Jpeg),
349            ImageFormat::Gif => Ok(Format::Gif),
350            ImageFormat::WebP => Ok(Format::WebP),
351            ImageFormat::Pnm => Ok(Format::Pnm),
352            ImageFormat::Tiff => Ok(Format::Tiff),
353            ImageFormat::Tga => Ok(Format::Tga),
354            ImageFormat::Bmp => Ok(Format::Bmp),
355            ImageFormat::Ico => Ok(Format::Ico),
356            ImageFormat::Hdr => Ok(Format::Hdr),
357            ImageFormat::OpenExr => Ok(Format::OpenExr),
358            ImageFormat::Farbfeld => Ok(Format::Farbfeld),
359            #[cfg(feature = "avif")]
360            ImageFormat::Avif => Ok(Format::Avif),
361            ImageFormat::Qoi => Ok(Format::Qoi),
362
363            _ => Err(ImageError::Unsupported(ImageFormatHint::Unknown.into())),
364        }
365    }
366}
367
368impl From<Format> for ImageFormat {
369    fn from(value: Format) -> Self {
370        match value {
371            Format::Png => Self::Png,
372            Format::Jpeg => Self::Jpeg,
373            Format::Gif => Self::Gif,
374            Format::WebP => Self::WebP,
375            Format::Pnm => Self::Pnm,
376            Format::Tiff => Self::Tiff,
377            Format::Tga => Self::Tga,
378            Format::Bmp => Self::Bmp,
379            Format::Ico => Self::Ico,
380            Format::Hdr => Self::Hdr,
381            Format::OpenExr => Self::OpenExr,
382            Format::Farbfeld => Self::Farbfeld,
383            #[cfg(feature = "avif")]
384            Format::Avif => Self::Avif,
385            Format::Qoi => Self::Qoi,
386        }
387    }
388}
389
390#[derive(
391    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Parse, Serialize, Deserialize,
392)]
393enum Compression {
394    Best,
395    Fast,
396    Uncompressed,
397    Balanced,
398}