use std::{net::TcpListener, process, sync::Arc}; use http::responses::{Body, Response}; use resvg::{ tiny_skia::{Color, Pixmap, Transform}, usvg, }; mod constants; mod handlers; mod http; mod icons; mod macros; mod threads; fn main() { println!("luciders starting..."); let port = match std::env::var("PORT") { Ok(port) => port, Err(_) => "7878".to_string(), }; let listener = TcpListener::bind(format!("[::]:{}", port)) .unwrap_or_else(|_| fatal("Failed to bind to address")); let icons_rc = Arc::new(match icons::Icons::build() { Ok(icons) => icons, Err(e) => fatal(e), }); let mut handlers = handlers::Handlers::new(); handlers.add_handler("/", |req| { if req.method != "GET" { return Response::new(req, 405, Body::Static("Method Not Allowed")); } Response::new(req, 200, Body::Static("OK - luciders is running")) }); handlers.add_handler("/info", |req| { if req.method != "GET" { return Response::new(req, 405, Body::Static("Method Not Allowed")); } Response::new(req, 200, Body::Static(constants::SERVER_INFO)) }); handlers.add_handler("/icons/[icon].svg", { let icons = Arc::clone(&icons_rc); move |req| { if req.method != "GET" { return Response::new(req, 405, Body::Static("Method Not Allowed")); } let icon_filename = match req.url.get_path_segment(1) { Some(icon) => icon, None => return Response::new(req, 404, Body::Static("Icon Segment Not Found")), }; let icon_name = &icon_filename[..icon_filename.len() - icons::ICON_FILE_EXTENSION_LEN]; if !icons.has_iconname(icon_name) { return Response::new(req, 404, Body::Static("Icon Not Found")); } let icon = match icons.get_icon(icon_name) { Some(icon) => icon, None => { error!("Failed to read icon"); return Response::new(req, 500, Body::Static("Failed to read icon")); } }; return Response::new(req, 200, Body::Bytes(icon, "image/svg+xml")); } }); handlers.add_handler("/icons/[icon].png", { let icons = Arc::clone(&icons_rc); move |req| { if req.method != "GET" { return Response::new(req, 405, Body::Static("Method Not Allowed")); } let icon_filename = match req.url.get_path_segment(1) { Some(icon) => icon, None => return Response::new(req, 404, Body::Static("Icon Segment Not Found")), }; let icon_name = &icon_filename[..icon_filename.len() - icons::ICON_FILE_EXTENSION_LEN]; if !icons.has_iconname(icon_name) { return Response::new(req, 404, Body::Static("Icon Not Found")); } let mut icon_string = match icons.get_icon_string(icon_name) { Some(icon) => icon, None => { error!("Failed to read icon"); return Response::new(req, 500, Body::Static("Failed to read icon")) }, }; // Options handling let mut scale = 1; match req.url.query.get("scale") { Some(scale_str) => { match scale_str.parse::() { Ok(scale_val) => { if scale_val <= 0 || scale_val > 100 { return Response::new(req, 400, Body::Static("Invalid scale value. Scale value must be an integer > 0 and <= 100")); } scale = scale_val; } Err(_) => return Response::new(req, 400, Body::Static("Invalid scale value. Scale value must be an integer > 0 and <= 100")) } } None => {} } let mut padding = 0; match req.url.query.get("padding") { Some(padding_str) => { match padding_str.parse::() { Ok(padding_val) => { if padding_val > 100 { return Response::new(req, 400, Body::Static("Invalid padding value. Padding value must be an integer >= 0 and <= 100")); } padding = padding_val; } Err(_) => return Response::new(req, 400, Body::Static("Invalid padding value. Padding value must be an integer >= 0 and <= 100")) } } None => {} } match req.url.query.get("discord_compatibility") { Some(_) => { padding = 8; } None => {} } let mut background = Color::TRANSPARENT; match req.url.query.get("background") { Some(background_str) => { match u32::from_str_radix(&background_str, 16) { Ok(background_val) => { if background_val > 0xFFFFFF { return Response::new(req, 400, Body::Static("Invalid background value. Background value must be a valid hex color #000000 - #FFFFFF")); } let red = ((background_val >> 16) & 0xFF) as u8; let green = ((background_val >> 8) & 0xFF) as u8; let blue = (background_val & 0xFF) as u8; background = Color::from_rgba8(red, green, blue, 255); } Err(_) => return Response::new(req, 400, Body::Static("Invalid background value. Background value must be a valid hex color")) } } None => {} } let mut svg_stroke = "currentColor"; // thx horizell for this code match (req.url.query.get("stroke_color"), req.url.query.get("stroke_colour"), req.url.query.get("stroke")) { (Some(stroke_str), _, _) | (_, Some(stroke_str), _) | (_, _, Some(stroke_str)) => svg_stroke = stroke_str, _ => {} } icon_string = icon_string.replace("currentColor", &svg_stroke); let mut backwards_compatibility = false; match req.url.query.get("backwards_compatibility") { Some(_) => { backwards_compatibility = true; // Icons were 80x80 plus 8px of padding; Icons start at 24x24 now so: 80 - 8 = 72, 72 / 24 = 3 therefore scale is 3x. scale = 3; padding = 8; } None => {} } // Rendering let tree = match usvg::Tree::from_str(&icon_string, &usvg::Options::default()) { Ok(tree) => tree, Err(e) => { error!(&format!("Failed to load icon into tree: {}", e)); return Response::new(req, 500, Body::Static("Failed to load icon into tree")) } }; let length = if backwards_compatibility { 80 } else { 24 * scale + padding }; let mut pixmap = match Pixmap::new(length, length) { Some(pixmap) => pixmap, None => { error!("Failed to create pixmap"); return Response::new(req, 500, Body::Static("Failed to create pixmap")) }, }; let mut pixmap_mut = pixmap.as_mut(); pixmap_mut.fill(background); resvg::render( &tree, Transform::from_scale(scale as f32, scale as f32) .post_concat(Transform::from_translate(length as f32 / 2.0 - 12.0 * scale as f32, length as f32 / 2.0 - 12.0 * scale as f32)), &mut pixmap_mut, ); let png = match pixmap.encode_png() { Ok(png) => png, Err(e) => { error!(&format!("Failed to encode PNG: {}", e)); return Response::new(req, 500, Body::Static("Failed to encode PNG")) }, }; return Response::new(req, 200, Body::Bytes(png, "image/png")); } }); handlers .bind(listener) .unwrap_or_else(|_| fatal("Failed to bind handlers")); println!("luciders started on port {}", port); println!("Serving {} icons", icons_rc.icons_len()) } fn fatal(msg: &'static str) -> ! { eprintln!("[FATAL] {}", msg); process::exit(1); }