//! Simple badge generator extern crate base64; extern crate rusttype; use base64::display::Base64Display; use rusttype::{Font, FontCollection, Scale, point, Point, PositionedGlyph}; const FONT_DATA: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/DejaVuSans.ttf")); const FONT_SIZE: f32 = 11.; 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, } impl Default for BadgeOptions { fn default() -> BadgeOptions { BadgeOptions { subject: "build".to_owned(), status: "passing".to_owned(), color: "#4c1".to_owned(), } } } pub struct Badge { options: BadgeOptions, font: Font<'static>, scale: Scale, offset: Point, } impl Badge { pub fn new(options: BadgeOptions) -> Result { let collection = FontCollection::from_bytes(FONT_DATA); // this should never fail in practice let font = try!(collection.into_font().ok_or("Failed to load font data".to_owned())); let scale = Scale { x: FONT_SIZE, y: FONT_SIZE, }; let v_metrics = font.v_metrics(scale); let offset = point(0.0, v_metrics.ascent); Ok(Badge { options: options, font: font, scale: scale, offset: offset, }) } pub fn to_svg_data_uri(&self) -> String { format!("data:image/svg+xml;base64,{}", Base64Display::standard(self.to_svg().as_bytes())) } pub fn to_svg(&self) -> String { let left_width = self.calculate_width(&self.options.subject) + 6; let right_width = self.calculate_width(&self.options.status) + 6; let svg = format!(r###" {} {} {} {} "###, left_width + right_width, left_width + right_width, left_width, left_width, right_width, self.options.color, left_width + right_width, (left_width) / 2, self.options.subject, (left_width) / 2, self.options.subject, left_width + (right_width / 2), self.options.status, left_width + (right_width / 2), self.options.status); svg } fn calculate_width(&self, text: &str) -> u32 { let glyphs: Vec = self.font.layout(text, self.scale, self.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.5)).ceil() as u32 } } #[cfg(test)] mod tests { use super::*; fn options() -> BadgeOptions { BadgeOptions::default() } #[test] fn test_new() { assert!(Badge::new(options()).is_ok()); } #[test] fn test_calculate_width() { let badge = Badge::new(options()).unwrap(); assert_eq!(badge.calculate_width("build"), 31); assert_eq!(badge.calculate_width("passing"), 48); } #[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).unwrap(); file.write_all(badge.to_svg().as_bytes()).unwrap(); } }