1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use embedded_graphics::pixelcolor::{Bgr888, RgbColor as _};
use image::{io::Reader as ImageReader, Pixel, RgbaImage};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::path::PathBuf;
use syn::{parse_macro_input, spanned::Spanned as _, Expr, ExprLit, ItemConst, Lit};

fn expand(
	ItemConst {
		attrs,
		vis,
		const_token,
		ident,
		colon_token,
		ty,
		eq_token,
		expr,
		semi_token,
	}: ItemConst,
) -> syn::Result<TokenStream2> {
	let path_lit = match expr.as_ref() {
		Expr::Lit(ExprLit { lit: Lit::Str(path), .. }) => path,
		expr => return Err(syn::Error::new(expr.span(), "Expected path to image")),
	};
	let path: PathBuf = path_lit
		.value()
		.parse()
		.map_err(|err| syn::Error::new(path_lit.span(), format!("Invalid path: {err}")))?;
	let image = ImageReader::open(&path)
		.map_err(|err| syn::Error::new(path_lit.span(), format!("Failed to open image {path:?}: {err}")))?
		.decode()
		.map_err(|err| syn::Error::new(path_lit.span(), format!("Failed to decode image {path:?}: {err}")))?;
	let path = path
		.canonicalize()
		.ok()
		.and_then(|path| path.to_str().map(String::from))
		.unwrap_or_else(|| path_lit.value());

	// convert input image to vec of colors
	let image: RgbaImage = image.into_rgba8();
	let pixels = image.pixels();
	let mut colors: Vec<Bgr888> = Vec::new();
	let mut transparency = Vec::new();
	for pixel in pixels.into_iter() {
		let mut chanel = pixel.channels().iter();
		// `Color::new()` only cuts bits; so we create a Bgr888 and convert it to
		// our target color type
		let color = Bgr888::new(
			chanel.next().unwrap_or(&0).to_owned(),
			chanel.next().unwrap_or(&0).to_owned(),
			chanel.next().unwrap_or(&0).to_owned(),
		);
		let a = chanel.next().map(|value| value == &0).unwrap_or(false);
		transparency.push(a as u8);
		colors.push(color);
	}

	// contruct output;
	let tmap_array = quote!(embedded_sprites::transparency![#(#transparency),*]);
	let color_ty = quote!(<#ty as ::embedded_sprites::private::Image>::Color);
	let colors = colors.into_iter().map(|color| {
		let r = color.r();
		let g = color.g();
		let b = color.b();
		quote!({
			let (r, g, b) = ::embedded_sprites::private::convert_from_bgr888::
				<#color_ty>(#r, #g, #b);
			#color_ty::new(r, g, b)
		})
	});
	let color_array = quote!([#(#colors),*]);
	let width = image.width() as u16;
	let height = image.height() as u16;
	let output = quote! {
		#(#attrs)* #vis #const_token #ident #colon_token #ty #eq_token {
			// include the bytes so that the compiler knows to recompile when the
			// image file changes
			const _: &[u8] = ::core::include_bytes!(#path);

			const COLOR_ARRAY: &[#color_ty] = &#color_array;
			const TRANSPARENTY_MAP: &[u8] = &#tmap_array;
			match ::embedded_sprites::image::Image::<'static, #color_ty>::new(
				COLOR_ARRAY, TRANSPARENTY_MAP, #width, #height
			) {
				::core::result::Result::Ok(img) => img,
				_ => panic!("Failed to construct image")
			}
		}
		#semi_token
	};
	//eprintln!("{output}");
	Ok(output)
}

/// Utility macro to construct a const [`Image`](embedded_graphics::image::Image) at compile time from a image file.
///
/// Every image formats supported by the [image crate](https://crates.io/crates/image) can be used.
/// The image will be automatically be converted to the requested pixelcolor.
/// Current only rgb pixelcolors are supported.
///
/// ```ignore
/// use embedded_sprites::{image::Image, include_image};
/// use embedded_graphics::pixelcolor::Bgr565;
/// #[include_image]
/// const IMAGE: Image<Bgr565> = "img/grass.png";
/// ```
#[proc_macro_attribute]
pub fn include_image(_attr: TokenStream, item: TokenStream) -> TokenStream {
	expand(parse_macro_input!(item))
		.unwrap_or_else(|err| err.into_compile_error())
		.into()
}