embedded_sprites_proc_macro/
lib.rs

1use embedded_graphics::pixelcolor::{Bgr888, RgbColor as _};
2use image::{io::Reader as ImageReader, Pixel, RgbaImage};
3use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2;
5use quote::quote;
6use std::path::PathBuf;
7use syn::{parse_macro_input, spanned::Spanned as _, Expr, ExprLit, ItemConst, Lit};
8
9fn expand(
10	ItemConst {
11		attrs,
12		vis,
13		const_token,
14		ident,
15		colon_token,
16		ty,
17		eq_token,
18		expr,
19		semi_token,
20	}: ItemConst,
21) -> syn::Result<TokenStream2> {
22	let path_lit = match expr.as_ref() {
23		Expr::Lit(ExprLit { lit: Lit::Str(path), .. }) => path,
24		expr => return Err(syn::Error::new(expr.span(), "Expected path to image")),
25	};
26	let path: PathBuf = path_lit
27		.value()
28		.parse()
29		.map_err(|err| syn::Error::new(path_lit.span(), format!("Invalid path: {err}")))?;
30	let image = ImageReader::open(&path)
31		.map_err(|err| syn::Error::new(path_lit.span(), format!("Failed to open image {path:?}: {err}")))?
32		.decode()
33		.map_err(|err| syn::Error::new(path_lit.span(), format!("Failed to decode image {path:?}: {err}")))?;
34	let path = path
35		.canonicalize()
36		.ok()
37		.and_then(|path| path.to_str().map(String::from))
38		.unwrap_or_else(|| path_lit.value());
39
40	// convert input image to vec of colors
41	let image: RgbaImage = image.into_rgba8();
42	let pixels = image.pixels();
43	let mut colors: Vec<Bgr888> = Vec::new();
44	let mut transparency = Vec::new();
45	for pixel in pixels.into_iter() {
46		let mut chanel = pixel.channels().iter();
47		// `Color::new()` only cuts bits; so we create a Bgr888 and convert it to
48		// our target color type
49		let color = Bgr888::new(
50			chanel.next().unwrap_or(&0).to_owned(),
51			chanel.next().unwrap_or(&0).to_owned(),
52			chanel.next().unwrap_or(&0).to_owned(),
53		);
54		let a = chanel.next().map(|value| value == &0).unwrap_or(false);
55		transparency.push(a as u8);
56		colors.push(color);
57	}
58
59	// contruct output;
60	let tmap_array = quote!(embedded_sprites::transparency![#(#transparency),*]);
61	let color_ty = quote!(<#ty as ::embedded_sprites::private::Image>::Color);
62	let colors = colors.into_iter().map(|color| {
63		let r = color.r();
64		let g = color.g();
65		let b = color.b();
66		quote!({
67			let (r, g, b) = ::embedded_sprites::private::convert_from_bgr888::
68				<#color_ty>(#r, #g, #b);
69			#color_ty::new(r, g, b)
70		})
71	});
72	let color_array = quote!([#(#colors),*]);
73	let width = image.width() as u16;
74	let height = image.height() as u16;
75	let output = quote! {
76		#(#attrs)* #vis #const_token #ident #colon_token #ty #eq_token {
77			// include the bytes so that the compiler knows to recompile when the
78			// image file changes
79			const _: &[u8] = ::core::include_bytes!(#path);
80
81			const COLOR_ARRAY: &[#color_ty] = &#color_array;
82			const TRANSPARENTY_MAP: &[u8] = &#tmap_array;
83			match ::embedded_sprites::image::Image::<'static, #color_ty>::new(
84				COLOR_ARRAY, TRANSPARENTY_MAP, #width, #height
85			) {
86				::core::result::Result::Ok(img) => img,
87				_ => panic!("Failed to construct image")
88			}
89		}
90		#semi_token
91	};
92	//eprintln!("{output}");
93	Ok(output)
94}
95
96/// Utility macro to construct a const [`Image`](embedded_graphics::image::Image) at compile time from a image file.
97///
98/// Every image formats supported by the [image crate](https://crates.io/crates/image) can be used.
99/// The image will be automatically be converted to the requested pixelcolor.
100/// Current only rgb pixelcolors are supported.
101///
102/// ```ignore
103/// use embedded_sprites::{image::Image, include_image};
104/// use embedded_graphics::pixelcolor::Bgr565;
105/// #[include_image]
106/// const IMAGE: Image<Bgr565> = "img/grass.png";
107/// ```
108#[proc_macro_attribute]
109pub fn include_image(_attr: TokenStream, item: TokenStream) -> TokenStream {
110	expand(parse_macro_input!(item))
111		.unwrap_or_else(|err| err.into_compile_error())
112		.into()
113}