text_image/
lib.rs

1#![feature(iter_array_chunks)]
2
3use ab_glyph::{Font, FontRef, PxScale, ScaleFont};
4use image::{GenericImageView, GrayImage, Luma, Rgb};
5use imageproc::drawing::{draw_text_mut, text_size};
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::parse::{Parse, ParseStream, Result};
9use syn::{parse_macro_input, Ident, Lit, LitByteStr, Token};
10
11#[derive(Debug)]
12struct TextImageOptions {
13    text: String,
14    font: String,
15    font_size: f32,
16    inverse: bool,
17    line_spacing: i32,
18    // 2, 4, or 8
19    gray_depth: i32,
20}
21
22impl Parse for TextImageOptions {
23    fn parse(input: ParseStream) -> Result<Self> {
24        let mut opts = TextImageOptions {
25            text: "".to_string(),
26            font: "".to_string(),
27            font_size: 16.0,
28            inverse: false,
29            line_spacing: 0,
30            gray_depth: 1,
31        };
32
33        loop {
34            let name: Ident = input.parse()?;
35
36            match &*name.to_string() {
37                "text" => {
38                    input.parse::<Token![=]>()?;
39                    let text: Lit = input.parse()?;
40
41                    let text = if let Lit::Str(text) = &text {
42                        text.value()
43                    } else {
44                        return Err(syn::Error::new_spanned(text, "expected a string literal"));
45                    };
46
47                    opts.text = text;
48                }
49                "font" => {
50                    input.parse::<Token![=]>()?;
51                    let font: Lit = input.parse()?;
52
53                    let font = if let Lit::Str(font) = &font {
54                        font.value()
55                    } else {
56                        return Err(syn::Error::new_spanned(font, "expected a string literal"));
57                    };
58
59                    opts.font = font;
60                }
61                "font_size" => {
62                    input.parse::<Token![=]>()?;
63                    let font_size: Lit = input.parse()?;
64
65                    let font_size = if let Lit::Float(font_size) = &font_size {
66                        font_size.base10_parse()?
67                    } else {
68                        return Err(syn::Error::new_spanned(
69                            font_size,
70                            "expected a float literal",
71                        ));
72                    };
73
74                    opts.font_size = font_size;
75                }
76                "line_spacing" => {
77                    input.parse::<Token![=]>()?;
78                    let line_spacing: Lit = input.parse()?;
79
80                    let line_spacing = if let Lit::Int(line_spacing) = &line_spacing {
81                        line_spacing.base10_parse()?
82                    } else {
83                        return Err(syn::Error::new_spanned(
84                            line_spacing,
85                            "expected a integer literal",
86                        ));
87                    };
88
89                    opts.line_spacing = line_spacing;
90                }
91                "inverse" => {
92                    opts.inverse = true;
93                }
94                "Gray2" => {
95                    opts.gray_depth = 2;
96                }
97                "Gray4" => {
98                    opts.gray_depth = 4;
99                }
100                "Gray8" => {
101                    opts.gray_depth = 8;
102                }
103                _ => {
104                    return Err(syn::Error::new_spanned(
105                        name,
106                        "expected `text`, `font`, `font_size` or `inverse`",
107                    ));
108                }
109            }
110
111            let _ = input.parse::<Token![,]>();
112            if input.is_empty() {
113                break;
114            }
115        }
116
117        // check required
118        if opts.text.is_empty() {
119            return Err(syn::Error::new_spanned(
120                "text",
121                "required option `text` is missing",
122            ));
123        }
124        if opts.font.is_empty() {
125            return Err(syn::Error::new_spanned(
126                "font",
127                "required option `font` is missing",
128            ));
129        }
130
131        Ok(opts)
132    }
133}
134
135/// Generate a text image.
136///
137/// Usage:
138///
139/// ```rust
140/// use text_image::text_image;
141///
142/// use embedded_graphics::{image::ImageRaw, pixelcolor::Gray8};
143///
144/// fn main() {
145///   let (w, h, raw) = text_image!(
146///     text = "Hello, world!哈哈这样也行",
147///     font = "LXGWWenKaiScreen.ttf",
148///     font_size = 48.0,
149///     inverse,
150///     Gray4,
151///   );
152///   let raw_image = ImageRaw::<Gray8>::new(raw, w);
153/// }
154///
155/// ````
156#[proc_macro]
157pub fn text_image(input: TokenStream) -> TokenStream {
158    let opts = parse_macro_input!(input as TextImageOptions);
159    println!("text_image: {:#?}", opts);
160
161    let font_raw = std::fs::read(opts.font).expect("Can not read font file");
162    let font = FontRef::try_from_slice(&font_raw).expect("Can not load font");
163
164    let scale = PxScale {
165        x: opts.font_size,
166        y: opts.font_size,
167    };
168
169    // let metric = font.v_metrics(scale);
170    let sfont = font.as_scaled(scale);
171    let line_height = (sfont.ascent() - sfont.descent() + sfont.line_gap())
172        .abs()
173        .ceil() as i32;
174
175    let mut h = 0;
176    let mut w = 0;
177    let mut lines = 0;
178
179    for line in opts.text.lines() {
180        let (lw, _lh) = text_size(scale, &font, line);
181        // println!("lh => {}", _lh);
182        w = w.max(lw);
183        h += line_height;
184        lines += 1;
185    }
186    w += 1;
187    h += opts.line_spacing as i32 * (lines - 1);
188
189    // align to byte
190    if w % 8 != 0 {
191        w = (w / 8 + 1) * 8;
192    }
193    println!("text_image: result size {}x{}, {} lines", w, h, lines);
194
195    let mut image: image::ImageBuffer<Luma<u8>, Vec<u8>> = GrayImage::new(w as _, h as _);
196
197    let mut luma = 0xFF;
198    if opts.inverse {
199        image.fill(0xFF);
200        luma = 0x00;
201    }
202
203    for (i, line) in opts.text.lines().enumerate() {
204        // 1 px offset for blending
205        draw_text_mut(
206            &mut image,
207            Luma([luma]),
208            1,
209            (line_height + opts.line_spacing) * (i as i32) - 1,
210            scale,
211            &font,
212            &line,
213        );
214    }
215
216    let raw = image.into_raw();
217
218    // convert depth
219    let raw: Vec<u8> = match opts.gray_depth {
220        8 => raw,
221        4 => raw
222            .chunks(2)
223            .map(|ch| (ch[1] >> 4) | (ch[0] & 0xF0))
224            .collect(),
225        2 => {
226            let mut ret = Vec::with_capacity(raw.len() / 4);
227            for ch in raw.chunks(4) {
228                ret.push(
229                    (ch[3] >> 6) | ((ch[2] >> 4) & 0x0C) | ((ch[1] >> 2) & 0x30) | (ch[0] & 0xC0),
230                );
231            }
232            ret
233        }
234        1 => {
235            let mut ret = Vec::with_capacity(raw.len() / 8);
236            for ch in raw.chunks(8) {
237                ret.push(
238                    (ch[7] >> 7)
239                        | ((ch[6] >> 6) & 0x02)
240                        | ((ch[5] >> 5) & 0x04)
241                        | ((ch[4] >> 4) & 0x08)
242                        | ((ch[3] >> 3) & 0x10)
243                        | ((ch[2] >> 2) & 0x20)
244                        | ((ch[1] >> 1) & 0x40)
245                        | (ch[0] & 0x80),
246                );
247            }
248            ret
249        }
250        _ => unreachable!(),
251    };
252
253    // convert from 8-bit grayscale to 1-bit compressed bytes
254
255    let raw_bytes = Lit::ByteStr(LitByteStr::new(&raw, proc_macro2::Span::call_site()));
256
257    let w = w as u32;
258    let h = h as u32;
259
260    // TODO: binary support https://github.com/image-rs/image/issues/640
261
262    let expanded = quote! {
263        (#w, #h, #raw_bytes)
264    };
265
266    TokenStream::from(expanded)
267}
268
269#[derive(Debug)]
270struct ImageOptions {
271    image: String,
272    /// index of the channel to use
273    channel: u8,
274    gray_depth: i32,
275}
276
277impl Parse for ImageOptions {
278    fn parse(input: ParseStream) -> Result<Self> {
279        let mut opts = ImageOptions {
280            image: "".to_string(),
281            channel: 0,
282            gray_depth: 1,
283        };
284
285        let name: Lit = input.parse()?;
286
287        let image = if let Lit::Str(image) = &name {
288            image.value()
289        } else {
290            return Err(syn::Error::new_spanned(
291                "image",
292                "expected a string literal",
293            ));
294        };
295        opts.image = image;
296
297        while let Ok(_) = input.parse::<Token![,]>() {
298            if input.is_empty() {
299                break;
300            }
301
302            let name: Ident = input.parse()?;
303
304            match &*name.to_string() {
305                "channel" => {
306                    input.parse::<Token![=]>()?;
307                    let channel: Lit = input.parse()?;
308
309                    let channel = if let Lit::Int(channel) = &channel {
310                        channel.base10_parse()?
311                    } else {
312                        return Err(syn::Error::new_spanned(
313                            channel,
314                            "expected a integer literal",
315                        ));
316                    };
317
318                    opts.channel = channel;
319                }
320                "Gray2" => {
321                    opts.gray_depth = 2;
322                }
323                "Gray4" => {
324                    opts.gray_depth = 4;
325                }
326                "Gray8" => {
327                    opts.gray_depth = 8;
328                }
329                _ => {
330                    return Err(syn::Error::new_spanned(
331                        name,
332                        "expected `palette` or `channel`",
333                    ));
334                }
335            }
336        }
337
338        Ok(opts)
339    }
340}
341
342struct BWR;
343
344impl BWR {
345    fn map_palette(&self, c: &Rgb<u8>) -> u8 {
346        let palette = vec![0x000000, 0xFFFFFF, 0xFF0000];
347        let mut min = 0;
348        let mut min_dist = 0x7FFF_FFFF;
349        for (i, p) in palette.iter().enumerate() {
350            let dist = (c.0[0] as i32 - (p >> 16) as i32).pow(2)
351                + (c.0[1] as i32 - ((p >> 8) & 0xFF) as i32).pow(2)
352                + (c.0[2] as i32 - (p & 0xFF) as i32).pow(2);
353            if dist < min_dist {
354                min_dist = dist;
355                min = i;
356            }
357        }
358        min as u8
359    }
360}
361
362impl image::imageops::colorops::ColorMap for BWR {
363    type Color = Rgb<u8>;
364
365    fn index_of(&self, color: &Self::Color) -> usize {
366        let palette = vec![0x000000, 0xFFFFFF, 0xFF0000];
367        let mut min = 0;
368        let mut min_dist = 0x7FFF_FFFF;
369        for (i, p) in palette.iter().enumerate() {
370            let dist = (color.0[0] as i32 - (p >> 16) as i32).pow(2)
371                + (color.0[1] as i32 - ((p >> 8) & 0xFF) as i32).pow(2)
372                + (color.0[2] as i32 - (p & 0xFF) as i32).pow(2);
373            if dist < min_dist {
374                min_dist = dist;
375                min = i;
376            }
377        }
378        min
379    }
380    fn map_color(&self, color: &mut Self::Color) {
381        let idx = self.index_of(color);
382        let palette = [
383            Rgb([0x00, 0x00, 0x00]),
384            Rgb([0xFF, 0xFF, 0xFF]),
385            Rgb([0xFF, 0x00, 0x00]),
386        ];
387        *color = palette[idx];
388    }
389}
390
391#[proc_macro]
392pub fn monochrome_image(input: TokenStream) -> TokenStream {
393    let opts = parse_macro_input!(input as ImageOptions);
394    println!("text_image: {:#?}", opts);
395
396    let im = image::open(&opts.image).expect("Can not read image file");
397    let (mut w, h) = im.dimensions();
398
399    let mut im = im.to_rgb8();
400
401    // Floyd-Steinberg dithering
402    image::imageops::colorops::dither(&mut im, &BWR);
403
404    let mut ret = vec![];
405
406    // convert each 8 pixel to a compressed byte
407    for (y, row) in im.enumerate_rows() {
408        let mut n = 0u8;
409        for (x, (_, _, px)) in row.enumerate() {
410            let ix = BWR.map_palette(px);
411            if ix == opts.channel {
412                n |= 1 << (7 - x % 8);
413            }
414            if x % 8 == 7 {
415                ret.push(n);
416                n = 0;
417            }
418        }
419        if w % 8 != 0 {
420            ret.push(n);
421        }
422    }
423
424    w = (w / 8 + if w % 8 != 0 { 1 } else { 0 }) * 8;
425
426    let raw_bytes = Lit::ByteStr(LitByteStr::new(&ret, proc_macro2::Span::call_site()));
427
428    let expanded = quote! {
429        (#w, #h, #raw_bytes)
430    };
431
432    TokenStream::from(expanded)
433}
434
435struct BWYR;
436
437impl BWYR {
438    fn map_palette(&self, c: &Rgb<u8>) -> u8 {
439        let palette = vec![0x000000, 0xFFFFFF, 0xFF0000, 0xFFFF00];
440        let mut min = 0;
441        let mut min_dist = 0x7FFF_FFFF;
442        for (i, p) in palette.iter().enumerate() {
443            let dist = (c.0[0] as i32 - (p >> 16) as i32).pow(2)
444                + (c.0[1] as i32 - ((p >> 8) & 0xFF) as i32).pow(2)
445                + (c.0[2] as i32 - (p & 0xFF) as i32).pow(2);
446            if dist < min_dist {
447                min_dist = dist;
448                min = i;
449            }
450        }
451        min as u8
452    }
453}
454
455impl image::imageops::colorops::ColorMap for BWYR {
456    type Color = Rgb<u8>;
457
458    fn index_of(&self, color: &Self::Color) -> usize {
459        let palette = vec![0x000000, 0xFFFFFF, 0xFFFF00, 0xFF0000];
460        let mut min = 0;
461        let mut min_dist = 0x7FFF_FFFF;
462        for (i, p) in palette.iter().enumerate() {
463            let dist = (color.0[0] as i32 - (p >> 16) as i32).abs()
464                + (color.0[1] as i32 - ((p >> 8) & 0xFF) as i32).abs()
465                + (color.0[2] as i32 - (p & 0xFF) as i32).abs();
466            if dist < min_dist {
467                min_dist = dist;
468                min = i;
469            }
470        }
471        min
472    }
473    fn map_color(&self, color: &mut Self::Color) {
474        let idx = self.index_of(color);
475        let palette = [
476            Rgb([0x00, 0x00, 0x00]),
477            Rgb([0xFF, 0xFF, 0xFF]),
478            Rgb([0xFF, 0x00, 0x00]),
479            Rgb([0xFF, 0xFF, 0x00]),
480        ];
481        *color = palette[idx];
482    }
483}
484
485// for BWRY palette
486#[proc_macro]
487pub fn quadcolor_image(input: TokenStream) -> TokenStream {
488    let opts = parse_macro_input!(input as ImageOptions);
489    println!("text_image: {:#?}", opts);
490
491    let im = image::open(&opts.image).expect("Can not read image file");
492    let (w, h) = im.dimensions();
493
494    let mut im = im.to_rgb8();
495
496    // Floyd-Steinberg dithering
497    image::imageops::colorops::dither(&mut im, &BWYR);
498
499    let mut ret = vec![];
500
501    for pixels in im.pixels().array_chunks::<4>() {
502        let mut n = 0u8;
503        for pix in pixels {
504            let ix = BWYR.map_palette(pix);
505            if ix != 0 && ix != 1 && ix != 2 {
506                println!("ix => {}", ix);
507            }
508            n = (n << 2) | (ix & 0b11);
509        }
510        ret.push(n);
511    }
512
513    let raw_bytes = Lit::ByteStr(LitByteStr::new(&ret, proc_macro2::Span::call_site()));
514
515    let expanded = quote! {
516        (#w, #h, #raw_bytes)
517    };
518
519    TokenStream::from(expanded)
520}
521
522/// Load a image and compress it to grayscale image of specified depth.
523///
524/// ```
525/// let (w, h, img_raw) = text_image::gray_image!("pattern128x128.png", Gray4);
526/// let image: ImageRaw<Gray4, LittleEndian> = ImageRaw::new(img_raw, w);
527/// image.draw(&mut fb).unwrap();
528/// ```
529#[proc_macro]
530pub fn gray_image(input: TokenStream) -> TokenStream {
531    let opts = parse_macro_input!(input as ImageOptions);
532    println!("text_image: {:#?}", opts);
533
534    let im = image::open(&opts.image).expect("Can not read image file");
535    let (w, h) = im.dimensions();
536
537    let im = im.to_luma8();
538
539    let mut ret = vec![];
540
541    let steps_per_pixel = 8 / opts.gray_depth;
542    let shift_per_pixel = match opts.gray_depth {
543        8 => 0,
544        4 => 4,
545        2 => 6,
546        1 => 7,
547        _ => unreachable!(),
548    };
549
550    let mut c = 0;
551    let mut n = 0u8;
552    for pixel in im.pixels() {
553        let val = pixel.0[0];
554
555        n = (n << opts.gray_depth) | (val >> shift_per_pixel);
556        c += 1;
557
558        if c == steps_per_pixel {
559            ret.push(n);
560            n = 0;
561            c = 0;
562        }
563    }
564
565    let raw_bytes = Lit::ByteStr(LitByteStr::new(&ret, proc_macro2::Span::call_site()));
566
567    let expanded = quote! {
568        (#w, #h, #raw_bytes)
569    };
570
571    TokenStream::from(expanded)
572}