use crate::framebuffer::FrameBuffer; use crate::handlers::LineHandler; use rusttype::LayoutIter; use rusttype::{point, FontCollection, Scale}; use serenity::async_trait; use serenity::http::AttachmentType; use serenity::model::channel::Message; use serenity::prelude::*; use std::borrow::Cow; const COLOUR: (u8, u8, u8) = (255, 0, 0); const SCALE: Scale = Scale { x: 48.0, y: 48.0 }; const PADDING_X: u32 = 0; const PADDING_Y: u32 = 0; const MAX_WIDTH: i32 = 800; const Y_GAP: i32 = 5; // Wrap text when width exceeds MAX_WIDTH struct WrappingLayoutIter<'a> { iter: LayoutIter<'a, 'a>, offset_x: i32, offset_y: i32, cur_x: i32, cur_y: i32, } impl<'a> WrappingLayoutIter<'a> { fn new(iter: LayoutIter<'a, 'a>, offset_x: i32, offset_y: i32) -> Self { Self { iter, offset_x, offset_y, cur_x: 0, cur_y: 0, } } } impl<'a> Iterator for WrappingLayoutIter<'a> { type Item = Vec<(i32, i32, f32)>; fn next(&mut self) -> Option<Self::Item> { let (glyph, bounding_box) = loop { let glyph = self.iter.next()?; if let Some(bb) = glyph.pixel_bounding_box() { break (glyph, bb); } }; if bounding_box.max.x + self.cur_x > MAX_WIDTH { self.cur_x = -bounding_box.min.x; self.cur_y += bounding_box.max.y - bounding_box.min.y + Y_GAP; } let mut buf = Vec::new(); glyph.draw(|x, y, a| { let x = x as i32 + bounding_box.min.x + self.offset_x + self.cur_x; let y = y as i32 + bounding_box.min.y + self.offset_y + self.cur_y; buf.push((x, y, a)); }); Some(buf) } } #[derive(Default)] pub struct SusHandler; impl SusHandler { async fn render_font(&self, ctx: &Context, msg: &Message, s: &str) { let font_data = include_bytes!("../amongus.ttf"); let collection = FontCollection::from_bytes(font_data as &[u8]); let font = collection.into_font().unwrap(); let start = point(0.0, 0.0); // Find image dimensions let mut min_x = 0; let mut max_x = 0; let mut max_y = 0; let mut min_y = 0; for arr in WrappingLayoutIter::new(font.layout(s, SCALE, start), 0, 0) { for (x, y, _) in arr { if x < min_x { min_x = x; } if x > max_x { max_x = x; } if y < min_y { min_y = y; } if y > max_y { max_y = y; } } } let offset_x = -min_x; let offset_y = -min_y; max_x += offset_x; max_y += offset_y; let mut framebuffer = FrameBuffer::new(max_x as u32 + PADDING_X * 2, max_y as u32 + PADDING_Y * 2); for arr in WrappingLayoutIter::new( font.layout(s, SCALE, start), offset_x + PADDING_X as i32, offset_y + PADDING_Y as i32, ) { for (x, y, a) in arr { framebuffer.set_pixel( x as u32, y as u32, COLOUR.0, COLOUR.1, COLOUR.2, (a * 255.0) as u8, ); } } let buf = framebuffer.as_png_vec(); msg.channel_id .send_message(&ctx, |e| { e.add_file(AttachmentType::Bytes { data: Cow::Borrowed(&buf), filename: "output.png".to_string(), }); e }) .await .unwrap(); } } #[async_trait] impl LineHandler for SusHandler { async fn line(&self, ctx: &Context, msg: &Message, line: &str) { if line.starts_with("!SUS") { let s = line.split(' ').collect::<Vec<_>>()[1..] .join(" ") .to_uppercase(); if s.is_empty() { return; } self.render_font(ctx, msg, &s).await; } } }