//! Simple badge generator use base64::display::Base64Display; use once_cell::sync::Lazy; use rusttype::{point, Font, Point, PositionedGlyph, Scale}; use serde::Deserialize; const FONT_DATA: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/DejaVuSans.ttf")); const FONT_SIZE: f32 = 11.; const SCALE: Scale = Scale { x: FONT_SIZE, y: FONT_SIZE, }; /// Badge style name. /// /// Default style is "flat". /// /// Matches style names from shields.io. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum BadgeStyle { Flat, FlatSquare, } impl Default for BadgeStyle { fn default() -> Self { Self::Flat } } #[derive(Debug, Clone)] pub struct BadgeOptions { /// Subject will be displayed on the left side of badge pub subject: String, /// Status will be displayed on the right side of badge pub status: String, /// HTML color of badge pub color: String, /// Style of badge. pub style: BadgeStyle, } impl Default for BadgeOptions { fn default() -> BadgeOptions { BadgeOptions { subject: "build".to_owned(), status: "passing".to_owned(), color: "#4c1".to_owned(), style: BadgeStyle::Flat, } } } struct BadgeStaticData { font: Font<'static>, scale: Scale, offset: Point, } static DATA: Lazy = Lazy::new(|| { let font = Font::try_from_bytes(FONT_DATA).expect("failed to parse font collection"); let v_metrics = font.v_metrics(SCALE); let offset = point(0.0, v_metrics.ascent); BadgeStaticData { font, scale: SCALE, offset, } }); pub struct Badge { options: BadgeOptions, } impl Badge { pub fn new(options: BadgeOptions) -> Badge { Badge { options } } pub fn to_svg_data_uri(&self) -> String { format!( "data:image/svg+xml;base64,{}", Base64Display::with_config(self.to_svg().as_bytes(), base64::STANDARD) ) } pub fn to_svg(&self) -> String { match self.options.style { BadgeStyle::Flat => self.to_flat_svg(), BadgeStyle::FlatSquare => self.to_flat_square_svg(), } } pub fn to_flat_svg(&self) -> String { let left_width = self.calculate_width(&self.options.subject) + 6; let right_width = self.calculate_width(&self.options.status) + 6; let total_width = left_width + right_width; let left_center = left_width / 2; let right_center = left_width + (right_width / 2); let color = &self.options.color; let subject = &self.options.subject; let status = &self.options.status; let svg = format!( r###" {subject} {subject} {status} {status} "### ); svg } pub fn to_flat_square_svg(&self) -> String { let left_width = self.calculate_width(&self.options.subject) + 6; let right_width = self.calculate_width(&self.options.status) + 6; let total_width = left_width + right_width; let left_center = left_width / 2; let right_center = left_width + (right_width / 2); let color = &self.options.color; let subject = &self.options.subject; let status = &self.options.status; let svg = format!( r###" {subject} {status} "###, ); svg } fn calculate_width(&self, text: &str) -> u32 { let glyphs: Vec = DATA.font.layout(text, DATA.scale, DATA.offset).collect(); let width = glyphs .iter() .rev() .filter_map(|g| { g.pixel_bounding_box() .map(|b| b.min.x as f32 + g.unpositioned().h_metrics().advance_width) }) .next() .unwrap_or(0.0); (width + ((text.len() as f32 - 1f32) * 1.3)).ceil() as u32 } } #[cfg(test)] mod tests { use super::*; fn options() -> BadgeOptions { BadgeOptions::default() } #[test] fn test_calculate_width() { let badge = Badge::new(options()); assert_eq!(badge.calculate_width("build"), 29); assert_eq!(badge.calculate_width("passing"), 44); } #[test] #[ignore] fn test_to_svg() { use std::fs::File; use std::io::Write; let mut file = File::create("test.svg").unwrap(); let options = BadgeOptions { subject: "build".to_owned(), status: "passing".to_owned(), ..BadgeOptions::default() }; let badge = Badge::new(options); file.write_all(badge.to_svg().as_bytes()).unwrap(); } #[test] fn deserialize_badge_style() { #[derive(Debug, Deserialize)] struct Foo { style: BadgeStyle, } let style = serde_urlencoded::from_str::("style=flat").unwrap(); assert_eq!(style.style, BadgeStyle::Flat); let style = serde_urlencoded::from_str::("style=flat-square").unwrap(); assert_eq!(style.style, BadgeStyle::FlatSquare); } }