From be94409e986a6b2003e36ac27c5b733ebe66a909 Mon Sep 17 00:00:00 2001 From: Reverier-Xu Date: Mon, 28 Aug 2023 23:38:10 +0900 Subject: [PATCH] :tada: v0.1.0 --- .gitignore | 9 ++ .vscode/settings.json | 3 + Cargo.toml | 19 ++++ README.md | 33 ++++++ samples/ncuz.svg | 77 ++++++++++++++ samples/wxgc.svg | 95 ++++++++++++++++++ src/lib.rs | 193 +++++++++++++++++++++++++++++++++++ src/model.rs | 227 ++++++++++++++++++++++++++++++++++++++++++ src/resource.rs | 70 +++++++++++++ 9 files changed, 726 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 samples/ncuz.svg create mode 100644 samples/wxgc.svg create mode 100644 src/lib.rs create mode 100644 src/model.rs create mode 100644 src/resource.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ce42ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target + + +# Added by cargo +# +# already existing elements were commented out + +#/target +/Cargo.lock diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..691b084 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "svg.preview.background": "black" +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e21d00e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "biosvg" +version = "0.1.0" +edition = "2021" +authors = ["Reverier-Xu "] +description = "Captcha based on SVG." +homepage = "https://github.com/XDSEC/biosvg" +documentation = "https://docs.rs/biosvg" +repository = "https://github.com/XDSEC/biosvg" +readme = "README.md" +license = "MPL-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +once_cell = "1.18" +rand = "0.8" +regex = "1.9" +thiserror = "1.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1a9178 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# BioSvg + +Captcha based on SVG. + +## Original idea + +[SVG绘制原理与验证码](https://blog.woooo.tech/posts/svg_1/) + +## Usage + +`cargo add biosvg` + +```rust +let (answer, svg) = BiosvgBuilder::new() + .length(4) + .difficulty(6) + .colors(vec![ + "#0078D6".to_string(), + "#aa3333".to_string(), + "#f08012".to_string(), + "#33aa00".to_string(), + "#aa33aa".to_string(), + ]) + .build() + .unwrap(); +println!("answer: {}", answer); +println!("svg: {}", svg); +``` + +## Example + +![ncuz](samples/ncuz.svg) +![wxgc](samples/wxgc.svg) diff --git a/samples/ncuz.svg b/samples/ncuz.svg new file mode 100644 index 0000000..6e8a4e4 --- /dev/null +++ b/samples/ncuz.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/wxgc.svg b/samples/wxgc.svg new file mode 100644 index 0000000..39eb6ed --- /dev/null +++ b/samples/wxgc.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eab45fc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,193 @@ +//! Captcha based on SVG. +//! +//! ## Original idea +//! +//! [SVG绘制原理与验证码](https://blog.woooo.tech/posts/svg_1/) +//! +//! ## Usage +//! +//! `cargo add biosvg` +//! +//! ```rust +//! let (answer, svg) = BiosvgBuilder::new() +//! .length(4) +//! .difficulty(6) +//! .colors(vec![ +//! "#0078D6".to_string(), +//! "#aa3333".to_string(), +//! "#f08012".to_string(), +//! "#33aa00".to_string(), +//! "#aa33aa".to_string(), +//! ]) +//! .build() +//! .unwrap(); +//! println!("answer: {}", answer); +//! println!("svg: {}", svg); +//! ``` + +mod model; +mod resource; +use model::Command; +use rand::seq::SliceRandom; +use rand::{thread_rng, Rng}; + +use resource::{FONT_PATHS, FONT_TABLE}; + +/// BiosvgBuilder is a builder for generating svg captcha with random text +#[derive(Debug, Clone, Default)] +pub struct BiosvgBuilder { + length: usize, + difficulty: u16, + colors: Vec, +} + +impl BiosvgBuilder { + /// constructor + pub fn new() -> BiosvgBuilder { + BiosvgBuilder::default() + } + + /// set length of captcha text + pub fn length(mut self, length: usize) -> BiosvgBuilder { + self.length = length; + self + } + + /// set difficulty of captcha, `difficulty` number of noise lines will be added + pub fn difficulty(mut self, difficulty: u16) -> BiosvgBuilder { + self.difficulty = difficulty; + self + } + + /// set colors of captcha text and noise lines, each color will be used randomly, + /// please add at least 4 colors. + /// the result of captcha will have a transparent background, + /// so you should add colors that looks good on your website background + pub fn colors(mut self, colors: Vec) -> BiosvgBuilder { + self.colors = colors; + self + } + + /// build and generate svg captcha + pub fn build(self) -> Result<(String, String), model::PathError> { + // generate random text with length + let mut answer = String::new(); + let mut rng = thread_rng(); + for _ in 0..self.length { + let index = rng.gen_range(0..FONT_TABLE.len()); + answer.push(String::from(FONT_TABLE).chars().nth(index).unwrap()); + } + + // split colors + let mut char_colors = Vec::new(); + let mut line_colors = Vec::new(); + for color in &self.colors { + let give_char = rng.gen_range(0..=1); + if give_char == 1 { + char_colors.push(color.clone()); + } else { + line_colors.push(color.clone()); + } + } + let mut font_paths = Vec::new(); + for ch in answer.chars() { + FONT_PATHS.get(ch.to_string().as_str()).map(|path| { + let random_angle = rng.gen_range(-0.2..0.2 * std::f64::consts::PI); + // let random_angle = random_angle + std::f64::consts::PI * 1.0; + let random_offset = rng.gen_range(0.0..0.1 * path.width); + let random_color = char_colors.choose(&mut rng).unwrap(); + let random_scale_x = rng.gen_range(0.8..1.2); + let random_scale_y = rng.gen_range(0.8..1.2); + let path = path + .with_color(&random_color) + .scale(random_scale_x, random_scale_y) + .rotate(random_angle) + .offset(0.0, random_offset); + + font_paths.push(path.clone()) + }); + } + let mut width = 0.0; + let mut height = 0.0; + for path in &font_paths { + width += path.width; + // height = max height of all paths + if path.height > height { + height = path.height; + } + } + width += 1.5 * height; + let mut start_point = height * 0.55; + let mut paths = Vec::new(); + for path in font_paths { + let offset_x = start_point + path.width / 2.0; + let offset_y = (height * 1.5) / 2.0; + let mut random_splited_path = path.offset(offset_x, offset_y).random_split(); + paths.append(random_splited_path.as_mut()); + start_point += path.width + height * 0.4 / self.length as f64; + } + for _ in 1..self.difficulty { + let start_x = rng.gen_range(0.0..width); + let end_x = rng.gen_range(start_x..start_x + height); + let start_y = rng.gen_range(0.0..height); + let end_y = rng.gen_range(start_y..start_y + height); + let color = line_colors.choose(&mut rng).unwrap(); + let start_command = Command { + x: start_x, + y: start_y, + command_type: model::CommandType::Move, + }; + let end_command = Command { + x: end_x, + y: end_y, + command_type: model::CommandType::LineTo, + }; + paths.push(model::Path { + commands: vec![start_command, end_command], + width, + height: height / 1.5, + color: color.clone(), + }); + } + paths.shuffle(&mut rng); + let svg_content = paths + .iter() + .map(|path| path.to_string()) + .collect::>() + .join(""); + Ok(( + answer, + format!( + r#"{}"#, + width, + height * 1.5, + width, + height * 1.5, + svg_content + ), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let (answer, svg) = BiosvgBuilder::new() + .length(4) + .difficulty(6) + .colors(vec![ + "#0078D6".to_string(), + "#aa3333".to_string(), + "#f08012".to_string(), + "#33aa00".to_string(), + "#aa33aa".to_string(), + ]) + .build() + .unwrap(); + println!("answer: {}", answer); + println!("svg: {}", svg); + } +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..0fa4d6d --- /dev/null +++ b/src/model.rs @@ -0,0 +1,227 @@ +use rand::Rng; +use thiserror::Error; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CommandType { + Move, + LineTo, +} + +#[derive(Debug, Clone)] +pub struct Command { + pub x: f64, + pub y: f64, + pub command_type: CommandType, +} + +#[derive(Debug, Clone)] +pub struct Path { + pub commands: Vec, + pub width: f64, + pub height: f64, + pub color: String, +} + +#[derive(Error, Debug)] +pub enum PathError { + #[error("invalid path or unsupported command")] + ParseError, + #[error("regex error")] + RegexError(#[from] regex::Error), + #[error("unknown path error")] + Unknown, +} + +impl Command { + pub fn new(x: f64, y: f64, command_type: CommandType) -> Command { + Command { x, y, command_type } + } + + pub fn offset(&self, x: f64, y: f64) -> Command { + Command { + x: self.x + x, + y: self.y + y, + command_type: self.command_type, + } + } + + pub fn scale(&self, x: f64, y: f64) -> Command { + Command { + x: self.x * x, + y: self.y * y, + command_type: self.command_type, + } + } + + /// Rotate the command aim point around the origin (0, 0). + pub fn rotate(&self, angle: f64) -> Command { + let x = self.x * angle.cos() - self.y * angle.sin(); + let y = self.x * angle.sin() + self.y * angle.cos(); + Command { + x, + y, + command_type: self.command_type, + } + } + + pub fn to_string(&self) -> String { + match self.command_type { + CommandType::Move => format!("M {} {} ", self.x, self.y), + CommandType::LineTo => format!("L {} {} ", self.x, self.y), + } + } +} + +impl Path { + pub fn parse(path: &str) -> Result { + let mut commands = Vec::new(); + let rx = regex::Regex::new(r"([ML])\s?(-?\d{1,}\.?\d{1,}?)\s(-?\d{1,}\.?\d{1,}?)")?; + let mut max_x = 0.0; + let mut min_x = 0.0; + let mut max_y = 0.0; + let mut min_y = 0.0; + for cap in rx.captures_iter(path) { + let command_type = match &cap[1] { + "M" => CommandType::Move, + "L" => CommandType::LineTo, + _ => return Err(PathError::ParseError), + }; + let x = cap[2].parse::().map_err(|_| PathError::ParseError)?; + let y = cap[3].parse::().map_err(|_| PathError::ParseError)?; + if x > max_x { + max_x = x; + } else if x < min_x { + min_x = x; + } + if y > max_y { + max_y = y; + } else if y < min_y { + min_y = y; + } + commands.push(Command::new(x, y, command_type)); + } + // offset the original point to the center of the path + let offset_x = (max_x + min_x) / 2.0; + let offset_y = (max_y + min_y) / 2.0; + + for command in &mut commands { + command.x -= offset_x; + command.y -= offset_y; + } + + let path = Path { + commands, + width: max_x - min_x, + height: max_y - min_y, + color: String::from("black"), + }; + + Ok(path) + } + + pub fn scale(&self, x: f64, y: f64) -> Path { + let mut commands = Vec::new(); + for command in &self.commands { + commands.push(command.scale(x, y)); + } + Path { + commands, + width: self.width * x, + height: self.height * y, + color: self.color.clone(), + } + } + + /// Rotate the path around the origin (0, 0). + pub fn rotate(&self, angle: f64) -> Path { + let mut commands = Vec::new(); + for command in &self.commands { + commands.push(command.rotate(angle)); + } + Path { + commands, + width: self.width, + height: self.height, + color: self.color.clone(), + } + } + + pub fn offset(&self, x: f64, y: f64) -> Path { + let mut commands = Vec::new(); + for command in &self.commands { + commands.push(command.offset(x, y)); + } + Path { + commands, + width: self.width, + height: self.height, + color: self.color.clone(), + } + } + + pub fn with_color(&self, color: &str) -> Path { + Path { + commands: self.commands.clone(), + width: self.width, + height: self.height, + color: String::from(color), + } + } + + pub fn random_split(&self) -> Vec { + let mut rng = rand::thread_rng(); + let mut paths = Vec::new(); + let mut commands = Vec::new(); + let mut break_limit = rng.gen_range(2..=4); + let mut start_cmd = self.commands[0].clone(); + for command in &self.commands { + if commands.len() >= break_limit || command.command_type == CommandType::Move { + if command.command_type == CommandType::LineTo { + commands.push(command.clone()); + } + + if commands.len() > 1 { + paths.push(Path { + commands: commands.clone(), + width: self.width, + height: self.height, + color: self.color.clone(), + }); + } + commands = Vec::new(); + start_cmd = command.clone(); + start_cmd.command_type = CommandType::Move; + break_limit = rng.gen_range(2..=4); + } else { + if commands.len() == 0 { + commands.push(start_cmd.clone()); + } + commands.push(command.clone()); + } + } + + if commands.len() > 1 { + paths.push(Path { + commands: commands.clone(), + width: self.width, + height: self.height, + color: self.color.clone(), + }); + } + paths + } + + pub fn to_string(&self) -> String { + let mut commands = String::new(); + for command in &self.commands { + commands.push_str(&command.to_string()); + } + // the stroke-width should be calculated by the path size + format!( + "", + commands.trim(), + self.color, + self.height / 20.0 + ) + } +} diff --git a/src/resource.rs b/src/resource.rs new file mode 100644 index 0000000..c4c6c32 --- /dev/null +++ b/src/resource.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; + +use once_cell::sync::Lazy; + +use super::model::Path; + +/// SVG font tables +pub static FONT_PATHS: Lazy> = Lazy::new(|| { + let mut m = HashMap::new(); + m.insert("n", Path::parse("M -266.4 220.5 L -86.5 220.5 L -168.5 220.5 L -168.5 -243 L -266.4 -243 L -171.5 -243 L -171.5 -113 L -118.5 -183 L -71.5 -220 L -14.5 -249 L 39.5 -252 L 99.5 -243 L 155.5 -217 L 193.5 -170 L 206.5 -129 L 206.5 -79 L 206.5 214.2 L 117.5 214.2 L 310.5 214.2 ").expect("invalid path")); + m.insert("9", Path::parse("M -130.5 344.9 L -64.5 341.7 L 4.5 316.5 L 67.5 285 L 127.5 244 L 177.5 178 L 218.5 109 L 237.5 30 L 250.5 -49 L 244.5 -121 L 228.5 -203 L 177.5 -276 L 111.5 -329 L 55.5 -348 L -11.5 -348 L -86.5 -332 L -156 -285 L -190.6 -238 L -206.4 -169 L -209.5 -102 L -187.5 -27 L -156 14 L -115.5 46 L -42.5 74 L 26.5 80 L 225.5 80 ").expect("invalid path")); + m.insert("Y", Path::parse("M -94.425 313.5 L 100.575 313.5 L -6.425 313.5 L -6.425 24 L -217.525 -342 L -302.575 -342 L -113.425 -342 L -217.525 -342 L -6.425 27 L 207.575 -345 L 116.57499999999999 -345 L 302.575 -345 ").expect("invalid path")); + m.insert("g", Path::parse("M -132 70.9 L -195.1 99.2 L -239.2 143.3 L -248.6 196.9 L -242.4 259.9 L -179.4 313.5 L -104 329.5 L -16 335.5 L 76 332.5 L 151 310.5 L 199 275.5 L 227 231.5 L 199 114.9 L 139 83.4 L 63 64.5 L 3 64.5 L -57 67.5 L -119 70.9 L -167 61.5 L -217.1 42.5 L -226.6 7.5 L -207.7 -26.5 L -157 -58.5 L -116 -36.5 L -47 -20.5 L 22 -20.5 L 95 -39.5 L 161 -83.5 L 183 -137.5 L 186 -215.5 L 167 -263.5 L 139 -297.5 L 183 -329.5 L 277 -329.5 L 277 -212.5 L 277 -329.5 L 189 -329.5 L 139 -297.5 L 85 -322.5 L -9 -335.5 L -97 -322.5 L -185.6 -272.5 L -210.9 -200.5 L -207.7 -133.5 L -179.4 -80.5 L -157 -55.5 ").expect("invalid path")); + m.insert("r", Path::parse("M 31.5 219 L -189 219 L -88.5 219 L -88.5 -244.5 L -179.5 -244.5 L -88.5 -244.5 L -88.5 -99.5 L -44.5 -149.5 L -3.5 -190.5 L 47.5 -221.5 L 91.5 -244.5 L 144.5 -250.5 L 220.5 -250.5 L 220.5 -127.5 ").expect("invalid path")); + m.insert("s", Path::parse("M -167 219 L -167 55.5 L -167 124.5 L -126 162.3 L -79 196.9 L -16 222.1 L 44 228.5 L 120 219 L 186 190.6 L 220 149.5 L 230 105.5 L 220 55.5 L 176 20.5 L 123 1.5 L 28 -14.5 L -60 -29.5 L -117 -51.5 L -154.4 -83.5 L -167 -121.5 L -167 -162.5 L -145 -203.5 L -88 -240.5 L -35 -250.5 L 47 -250.5 L 101 -234.5 L 142 -209.5 L 180 -174.5 L 208 -149.5 L 208 -83.5 L 208 -244.5 ").expect("invalid path")); + m.insert("w", Path::parse("M -412.5 -246 L -220.5 -246 L -318 -246 L -151.5 217.5 L 0.5 -246 L 151.5 220.6 L 318.5 -249 L 412.5 -249 L 217.5 -249 ").expect("invalid path")); + m.insert("u", Path::parse("M -286.55 -246 L -179 -246 L -179 79 L -176 120 L -151 160.8 L -123 192.3 L -82 217.5 L -22 227 L 38 217.5 L 104 192.3 L 151 145 L 195 91 L 195 217.5 L 296 217.5 L 195 217.5 L 195 -249 L 95 -249 ").expect("invalid path")); + m.insert("C", Path::parse("M 296 -348 L 296 -150 L 296 -244 L 237 -282 L 183 -317 L 126 -342 L 60 -348 L -3 -345 L -72 -323 L -141 -279 L -195 -206 L -229.7 -131 L -239.1 -52 L -236 49 L -214 134 L -173 209 L -97 278.7 L -6 316.5 L 136 319.6 L 211 297.6 L 262 266.1 L 299 231 ").expect("invalid path")); + m.insert("j", Path::parse("M -139 355.5 L -101 387.5 L -53.5 409.5 L 9.45 409.5 L 59.9 387.5 L 101 349.5 L 113 270.9 L 113 -245.5 L 15.8 -245.5 M 113 -352.5 L 132 -365.5 L 139 -393.5 L 123 -409.5 L 91.4 -406.5 L 85 -378.5 L 88.2 -362.5 L 113 -352.5 ").expect("invalid path")); + m.insert("c", Path::parse("M 228.5 -249 L 228.5 -88 L 228.5 -151 L 162.5 -208 L 67.5 -249 L -4.5 -249 L -67.5 -230 L -121.5 -189 L -159.2 -126 L -175 -53 L -175 32 L -152.9 104 L -102.5 176.6 L -30.5 217.5 L 23.5 223.8 L 83.5 223.8 L 171.5 195.4 L 225.5 151.4 ").expect("invalid path")); + m.insert("A", Path::parse("M -351.225 311.9 L -161.775 311.9 L -262.975 311.9 L -4.775 -346.5 L 162.225 72.5 L -168.775 72.5 L 165.225 72.5 L 260.225 315 L 159.225 315 L 351.225 315 ").expect("invalid path")); + m.insert("q", Path::parse("M 71 334 L 282 334 L 178 334 L 178 -325 L 178 -205 L 140 -249 L 90 -290 L 27 -322 L -39 -334 L -102 -318 L -169 -274 L -206.4 -218 L -225.3 -161 L -231.6 -95 L -225.3 -29 L -203.2 31 L -150 91.1 L -74 128.9 L 14 138.4 L 178 138.4 ").expect("invalid path")); + m.insert("M", Path::parse("M -401.7 313.5 L -206.5 313.5 L -297.5 313.5 L -297.5 -342 L -398.5 -342 L -275.5 -342 L 20.5 307.2 L 322.5 -345 L 439.5 -345 L 335.5 -345 L 335.5 313.5 L 250.5 313.5 L 439.5 313.5 ").expect("invalid path")); + m.insert("v", Path::parse("M -291.15 -247.5 L -92.85 -247.5 L -193.45 -247.5 L 5.157 219.1 L 210.15 -247.5 L 291.15 -247.5 L 109.15 -247.5 ").expect("invalid path")); + m.insert("z", Path::parse("M -168.5 -123.5 L -168.5 -245.5 L 222 -245.5 L -181 214 L 222 214 L 222 87.5 ").expect("invalid path")); + m.insert("P", Path::parse("M -236 312 L -22 312 L -132 312 L -132 -343.5 L -232.9 -343.5 L 117 -343.5 L 167 -333.5 L 218 -305.5 L 255 -267.5 L 277 -210.5 L 277 -138.5 L 265 -59.5 L 230 -2.5 L 177 28.5 L 114 37.5 L -129 37.5 ").expect("invalid path")); + m.insert("t", Path::parse("M -68 -345 L -68 -150 L -162.1 -150 L 125 -150 L -68 -150 L -68 200 L -55 250.5 L -30 288.3 L 11 313.5 L 52 319.8 L 103 313.5 L 153 291.4 L 181 266.2 ").expect("invalid path")); + m.insert("4", Path::parse("M -58 -356 L -61 -293 L -71 -217 L -80 -154 L -105 -85 L -137 -19 L -174.8 54 L -241 154 L 263 154 L 156 154 L 156 330.8 L 156 -151 ").expect("invalid path")); + m.insert("7", Path::parse("M -209.35 -204.5 L -209.35 -343.5 L 212.5 -343.5 L 140.5 -210.5 L 80.5 -103.5 L 33.5 9.5 L 1.5 88.5 L -17.5 154.5 L -29.5 220.5 L -32.5 308.9 ").expect("invalid path")); + m.insert("H", Path::parse("M -304 313.5 L -109 313.5 L -200 313.5 L -200 -342 L -307.2 -342 L -109 -342 L -200 -342 L -200 -20 L 244 -20 L 244 -345 L 153 -345 L 345 -345 L 247 -345 L 247 313.5 L 345 313.5 L 159 313.5 ").expect("invalid path")); + m.insert("V", Path::parse("M -348.075 -345 L -158.925 -345 L -263.025 -345 L -4.925 313.5 L 250.075 -342 L 153.075 -342 L 348.075 -342 ").expect("invalid path")); + m.insert("L", Path::parse("M -217.2 -346.5 L 6 -346.5 L -113 -346.5 L -113 311.9 L -214 311.9 L 255 311.9 L 255 173.5 ").expect("invalid path")); + m.insert("Q", Path::parse("M -53 285.05 L -129 259.75 L -201 199.65 L -258.1 111.65 L -277 26.657 L -286.5 -64.350 L -261.2 -187.350 L -211 -275.35 L -123 -348.35 L -22 -376.35 L 79 -376.35 L 173 -348.35 L 233 -304.35 L 284 -250.350 L 322 -168.350 L 337 -86.350 L 340 10.657 L 318 95.65 L 274 174.65 L 214 237.75 L 148 275.55 L 79 291.25 L 6 294.45 L -44 285.05 L -53 272.45 L -25 215.65 L 32 193.65 L 73 206.65 L 98 240.85 L 114 310.25 L 136 351.15 L 173 376.35 L 218 370.05 L 252 351.15 ").expect("invalid path")); + m.insert("T", Path::parse("M -85 312 L 123 312 L 13 312 L 13 -343.5 L -245.5 -343.5 L -245.5 -195.5 L -245.5 -343.5 L 277 -343.5 L 277 -195.5 ").expect("invalid path")); + m.insert("m", Path::parse("M -407.7 219 L -218.5 219 L -306.5 219 L -306.5 -247.5 L -404.5 -247.5 L -310.5 -247.5 L -310.5 -127.5 L -256.5 -190.5 L -187.5 -240.5 L -121.5 -250.5 L -58.5 -240.5 L -23.5 -209.5 L 14.5 -149.5 L 20.5 -95.5 L 20.5 215.9 L -67.5 215.9 L 108.5 215.9 L 23.5 215.9 L 23.5 -102.5 L 36.5 -133.5 L 80.5 -190.5 L 137.5 -237.5 L 197.5 -250.5 L 256.5 -244.5 L 301.5 -218.5 L 338.5 -171.5 L 354.5 -114.5 L 354.5 -55.5 L 354.5 215.9 L 263.5 215.9 L 445.5 215.9 ").expect("invalid path")); + m.insert("x", Path::parse("M -239.6 216 L -44.5 216 L -154.5 216 L 179.5 -247.5 L 251.5 -247.5 L 65.5 -247.5 M -41.5 -247.5 L -233.3 -247.5 L -151.5 -247.5 L 179.5 216 L 258.5 216 L 69.5 216 ").expect("invalid path")); + m.insert("y", Path::parse("M -256.5 279.5 L -228.1 304.5 L -190.4 332.5 L -153 332.5 L -112 332.5 L -68 307.5 L -27 257 L 11 168.8 L 207 -328.5 L 288 -328.5 L 106 -328.5 M -281.7 -332.5 L -102 -332.5 L -193.5 -332.5 L 24 131 ").expect("invalid path")); + m.insert("B", Path::parse("M -245.5 -345 L 88 -345 L 142 -329 L 186 -301 L 221 -253 L 227 -206 L 227 -156 L 211 -115 L 180 -80 L 148 -52 L 114 -36 L 79 -27 L -135 -27 L 85 -27 L 123 -27 L 180 -17 L 230 11 L 262 58 L 277 112 L 277 172 L 262 222 L 230 269.4 L 189 297.8 L 142 307.2 L 110 313.5 L -229.8 313.5 L -138 313.5 L -138 -342 ").expect("invalid path")); + m.insert("E", Path::parse("M 256.5 163.5 L 256.5 308.9 L -225 308.9 L -121.5 308.9 L -121.5 -21.5 L 159.5 -21.5 L 159.5 -103.5 L 159.5 59.5 L 159.5 -21.5 L -124.5 -21.5 L -124.5 -343.5 L -215.5 -343.5 L 250.5 -343.5 L 250.5 -204.5 ").expect("invalid path")); + m.insert("R", Path::parse("M -270.8 308.9 L -72 308.9 L -173 308.9 L -173 -343.5 L -270.8 -343.5 L 79 -343.5 L 139 -330.5 L 186 -305.5 L 227 -254.5 L 240 -198.5 L 236 -147.5 L 221 -84.5 L 192 -50.5 L 151 -18.5 L 98 -6.5 L -173 -6.5 L 6 -6.5 L 236 312 L 126 312 L 318 312 ").expect("invalid path")); + m.insert("S", Path::parse("M -198.2 315 L -198.2 122.5 L -198.2 217.5 L -157 248.9 L -85 292.9 L 0 318.1 L 98 321.3 L 180 299.3 L 243 252 L 271 195.5 L 277 116.5 L 243 56.5 L 177 18.5 L 95 -6.5 L -34 -44.5 L -135 -81.5 L -179.4 -119.5 L -204.5 -170.5 L -198.2 -242.5 L -164 -299.5 L -110 -333.5 L -34 -346.5 L 63 -346.5 L 129 -324.5 L 180 -292.5 L 221 -261.5 L 252 -229.5 L 252 -151.5 L 252 -346.5 ").expect("invalid path")); + m.insert("F", Path::parse("M -212.7 313.5 L 1.5 313.5 L -108.5 313.5 L -108.5 -20 L 177.5 -20 L 177.5 61 L 177.5 -102 L 177.5 -24 L -108.5 -24 L -108.5 -345 L -206.4 -345 L 250.5 -345 L 250.5 -203 ").expect("invalid path")); + m.insert("2", Path::parse("M -194 -254 L -146.5 -298 L -83.5 -329 L -17.5 -348 L 51.5 -348 L 114.5 -339 L 168.5 -314 L 215.5 -273 L 247.5 -200 L 243.5 -147 L 221.5 -96 L 158.5 -30 L 70.5 24 L -20.5 71 L -93.5 118 L -140.5 156 L -178.2 213 L -187.6 250.4 L -187.6 316.5 L 243.5 316.5 L 243.5 175 ").expect("invalid path")); + m.insert("6", Path::parse("M -176.6 -91 L 72.5 -91 L 135.5 -69 L 188.5 -38 L 232.5 13 L 254.5 63 L 258.5 129 L 251.5 195 L 210.5 255 L 169.5 299.1 L 103.5 330.6 L 28.5 333.8 L -34.5 324.4 L -97.5 289.7 L -157.5 230 L -195.5 145 L -201.8 63 L -195.5 -28 L -182.9 -79 L -160.9 -151 L -113.5 -224 L -47.5 -284 L 25.5 -331 L 103.5 -359 L 182.5 -356 ").expect("invalid path")); + m.insert("b", Path::parse("M -256.65 -345 L -155.85 -345 L -155.85 313.5 L -155.85 181 L -115.35 228 L -61.35 282 L -17.35 307.2 L 55.650 323 L 121.65 313.5 L 184.65 275.7 L 234.65 206 L 256.65 131 L 256.65 36 L 237.65 -27 L 203.65 -83 L 146.65 -121 L 80.65 -143 L 14.650 -146 L -152.75 -146 ").expect("invalid path")); + m.insert("e", Path::parse("M -179.6 -19 L 239.5 -19 L 239.5 -88 L 217.5 -148 L 185.5 -189 L 132.5 -230 L 62.5 -249 L -9.5 -252 L -81.5 -224 L -141.9 -170 L -170.2 -107 L -182.8 -38 L -182.8 38 L -167 98 L -116.5 170.1 L -53.5 211.1 L -3.5 226.8 L 72.5 226.8 L 154.5 201.6 L 217.5 157.5 ").expect("invalid path")); + m.insert("3", Path::parse("M -192.3 226.5 L -142 270.9 L -79 305.6 L -10 321.3 L 44 321.3 L 110 311.9 L 176 283.5 L 217 242.5 L 242 188.5 L 249 119.5 L 233 56.5 L 186 6.5 L 132 -15.5 L 63 -28.5 L -38 -28.5 L 85 -28.5 L 135 -44.5 L 183 -75.5 L 217 -122.5 L 230 -179.5 L 223 -229.5 L 192 -286.5 L 157 -321.5 L 97 -343.5 L 31 -346.5 L -35 -343.5 L -114 -315.5 L -179.7 -267.5 ").expect("invalid path")); + m.insert("D", Path::parse("M -275.7 311.9 L 39.5 311.9 L 114.5 292.9 L 181.5 258.3 L 247.5 192.5 L 291.5 126.5 L 313.5 50.5 L 313.5 -59.5 L 294.5 -141.5 L 247.5 -223.5 L 199.5 -280.5 L 124.5 -324.5 L 45.5 -346.5 L -275.7 -346.5 L -174.5 -346.5 L -174.5 308.7 ").expect("invalid path")); + m.insert("5", Path::parse("M -173.2 340.4 L -103.5 334.1 L -15.5 324.6 L 69.5 299.4 L 148.5 261.6 L 204.5 208.5 L 239.5 151.5 L 242.5 88.5 L 239.5 34.5 L 214.5 -18.5 L 176.5 -56.5 L 116.5 -84.5 L 34.5 -100.5 L -59.5 -91.5 L -125.5 -66.5 L -170 -37.5 L -170 -343.5 L 214.5 -343.5 ").expect("invalid path")); + m.insert("8", Path::parse("M 25 324.3 L 104 318 L 186 289.6 L 249 236.5 L 274 176.5 L 265 119.5 L 233 72.5 L 186 37.5 L 132 15.5 L 38 -19.5 L -44 -47.5 L -120 -82.5 L -161 -126.5 L -185.8 -192.5 L -176.4 -252.5 L -126 -312.5 L -50 -340.5 L 28 -349.5 L 88 -343.5 L 170 -318.5 L 224 -267.5 L 243 -214.5 L 243 -154.5 L 198 -97.5 L 139 -63.5 L 60 -34.5 L -54 3.5 L -129 34.5 L -179.5 75.5 L -207.9 113.5 L -214.1 191.5 L -185.8 248.5 L -139 289.6 L -79 314.9 L -19 324.3 L 25 324.3 ").expect("invalid path")); + m.insert("J", Path::parse("M -200.1 233.5 L -156 283.6 L -99 315.1 L -46 318.3 L 11 312 L 68 283.6 L 105 239.5 L 115 189.5 L 121 138.5 L 121 -343.5 L 17 -343.5 L 219 -343.5 ").expect("invalid path")); + m.insert("K", Path::parse("M -286.7 -343.5 L -88.5 -343.5 L -185.5 -343.5 L -185.5 312 L -283.5 312 L -94.5 312 L -185.5 312 L -185.5 75.5 L 207.5 -343.5 L 296.5 -343.5 L 85.5 -343.5 L 207.5 -343.5 L -47.5 -72.5 L 236.5 312 L 324.5 312 L 125.5 312 ").expect("invalid path")); + m.insert("N", Path::parse("M -308.5 310.4 L -119.5 310.4 L -207.5 310.4 L -207.5 -342 L -311.7 -342 L -195.5 -342 L 233.5 313.5 L 349.5 313.5 L 248.5 313.5 L 248.5 -345 L 160.5 -345 L 349.5 -345 ").expect("invalid path")); + m.insert("W", Path::parse("M -483.575 -346.5 L -297.425 -346.5 L -395.425 -346.5 L -190.425 318.1 L -1.425 -346.5 L 190.575 318.1 L 401.575 -343.5 L 303.575 -343.5 L 483.575 -343.5 ").expect("invalid path")); + m.insert("Z", Path::parse("M -203 -201.5 L -203 -346.5 L 266 -346.5 L -212.5 311.9 L 251 311.9 L 251 176.5 ").expect("invalid path")); + m.insert("a", Path::parse("M -179.4 -193.5 L -126 -234.5 L -53 -253.5 L 32 -247.5 L 91 -228.5 L 142 -171.5 L 158 -121.5 L 158 95.5 L 117 149.5 L 47 203.1 L -22 228.3 L -113 222 L -182.5 184.2 L -214 133.5 L -214 77.5 L -185.7 17.5 L -113 -26.5 L -28 -36.5 L 158 -36.5 L 158 218.9 L 255 218.9 ").expect("invalid path")); + m.insert("d", Path::parse("M 72.5 -345 L 185.5 -345 L 185.5 191 L 119.5 250.5 L 62.5 297.8 L -12.5 319.8 L -56.5 316.6 L -145.5 285.1 L -201.7 219 L -233.3 131 L -227 30 L -195.5 -55 L -119.5 -121 L -44.5 -146 L 31.5 -150 L 185.5 -150 L 185.5 313.5 L 280.5 313.5 ").expect("invalid path")); + m.insert("U", Path::parse("M -327.5 -345 L -138.5 -345 L -223.5 -345 L -223.5 137 L -207.5 187 L -176.5 235 L -129.5 275.7 L -75.5 307.2 L -15.5 319.8 L 53.5 319.8 L 119.5 294.6 L 176.5 256.8 L 220.5 213 L 242.5 165 L 248.5 118 L 248.5 -342 L 154.5 -342 L 349.5 -342 ").expect("invalid path")); + m.insert("h", Path::parse("M -270.6 -345 L -176 -345 L -176 310.4 L -270.6 310.4 L -88 310.4 L -176 310.4 L -176 -30 L -119 -90 L -66 -137 L 7 -159 L 63 -159 L 123 -140 L 161 -112 L 196 -61 L 202 2 L 202 313.5 L 114 313.5 L 299 313.5 ").expect("invalid path")); + m.insert("X", Path::parse("M -300.7 310.4 L -109 310.4 L -215.6 310.4 L 216 -342 L 298 -342 L 106 -342 M -297.55 -345 L -102 -345 L -200 -345 L 229 310.4 L 307 310.4 L 121 310.4 ").expect("invalid path")); + m.insert("k", Path::parse("M -249.1 -346.5 L -145 -346.5 L -145 311.9 L -246 311.9 L -54 311.9 L -142 311.9 L -142 176.5 L 154 -148.5 L 239 -148.5 L 44 -148.5 L 154 -148.5 L -16 34.5 L 182 315 L 268 315 L 78 315 ").expect("invalid path")); + m.insert("f", Path::parse("M -162.2 314.9 L 49 314.9 L -65 314.9 L -65 -145.5 L -162.2 -145.5 L 118 -145.5 L -68 -145.5 L -68 -201.5 L -58 -258.5 L -20 -315.5 L 30 -343.5 L 90 -349.5 L 156 -327.5 L 200 -289.5 ").expect("invalid path")); + m.insert("p", Path::parse("M -244.1 335.5 L -24 335.5 L -150 335.5 L -150 -323.5 L -237.8 -323.5 L -150 -323.5 L -150 -203.5 L -87 -263.5 L 2 -326.5 L 93 -335.5 L 181 -300.5 L 241 -228.5 L 263 -134.5 L 260 -42.5 L 241 32.5 L 194 86.4 L 131 121.1 L 71 133.7 L 21 140 L -143 140 ").expect("invalid path")); + m.insert("G", Path::parse("M 153 -19.5 L 351 -19.5 L 247 -19.5 L 247 321.1 L 247 214.5 L 188 264.4 L 125 302.3 L 52 324.3 L -14 324.3 L -67 318 L -143 289.6 L -200 236.5 L -259.6 154.5 L -288 37.5 L -291.1 -63.5 L -259.6 -173.5 L -197 -261.5 L -115 -327.5 L -33 -349.5 L 59 -343.5 L 147 -312.5 L 210 -267.5 L 241 -220.5 L 241 -151.5 L 241 -343.5 ").expect("invalid path")); + m.insert("i", Path::parse("M -85.2 294.5 L 123 294.5 L 16 294.5 L 16 -172 L -85.2 -172 M 9 -269 L 38 -285 L 38 -310 L 19 -326 L -6 -320 L -16 -295 L -10 -282 L 9 -269 ").expect("invalid path")); + m +}); + +/// all available characters +pub static FONT_TABLE: &'static str = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz";