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}