Complete overhaul of render/rasterize/parsing/templates/routes subsystems

This commit is contained in:
2023-07-22 16:06:27 -05:00
parent fb575ffd8b
commit bf54edf3bb
6 changed files with 169 additions and 86 deletions

View File

@@ -3,12 +3,14 @@ use std::net::SocketAddr;
use axum::{Router, routing::get}; use axum::{Router, routing::get};
use dotenvy::dotenv; use dotenvy::dotenv;
use config::Configuration; use config::Configuration;
use crate::routes::root_handler; use crate::routes::{relative_handler, implicit_handler, absolute_handler};
mod config; mod config;
mod svg; mod raster;
mod abbr; mod abbr;
mod routes; mod routes;
mod parse;
mod template;
#[tokio::main] #[tokio::main]
@@ -26,7 +28,12 @@ async fn main() {
.init(); .init();
let app = Router::new() let app = Router::new()
.route("/:path", get(root_handler)); .route("/:path", get(implicit_handler))
.route("/rel/:path", get(relative_handler))
.route("/relative/:path", get(relative_handler))
.route("/absolute/:path", get(absolute_handler))
.route("/abs/:path", get(absolute_handler));
let addr = SocketAddr::from((config.socket_addr(), config.port)); let addr = SocketAddr::from((config.socket_addr(), config.port));
axum::Server::bind(&addr) axum::Server::bind(&addr)
.serve(app.into_make_service_with_connect_info::<SocketAddr>()) .serve(app.into_make_service_with_connect_info::<SocketAddr>())

26
src/parse.rs Normal file
View File

@@ -0,0 +1,26 @@
use chrono::{DateTime, FixedOffset, Utc};
/// Split a path into a tuple of the preceding path and the extension.
/// Can handle paths with multiple dots (period characters).
/// Returns None if there is no extension.
/// Returns None if the preceding path is empty (for example, dotfiles like ".env").
pub fn split_on_extension(path: &str) -> Option<(&str, &str)> {
let split = path.rsplit_once('.');
if split.is_none() { return None; }
// Check that the file is not a dotfile (.env)
if split.unwrap().0.len() == 0 {
return None;
}
Some(split.unwrap())
}
pub fn parse_absolute(raw: String) -> Result<(DateTime<Utc>, FixedOffset), String> {
let datetime_with_offset = DateTime::parse_from_rfc3339(&raw);
if datetime_with_offset.is_err() {
return Err("Failed to parse datetime".to_string());
}
Ok((datetime_with_offset.unwrap().with_timezone(&Utc), *(datetime_with_offset.unwrap().offset())))
}

View File

@@ -16,11 +16,11 @@ impl std::fmt::Display for RenderError {
} }
} }
pub struct Renderer { pub struct Rasterizer {
font_db: fontdb::Database, font_db: fontdb::Database,
} }
impl Renderer { impl Rasterizer {
pub fn new() -> Self { pub fn new() -> Self {
let mut fontdb = fontdb::Database::new(); let mut fontdb = fontdb::Database::new();
fontdb.load_system_fonts(); fontdb.load_system_fonts();

View File

@@ -1,116 +1,119 @@
use std::io::Read;
use std::process::Output;
use std::time::SystemTime; use std::time::SystemTime;
use axum::{http::StatusCode, response::IntoResponse}; use axum::{http::StatusCode, response::IntoResponse};
use axum::body::Full; use axum::body::{Bytes, Full};
use axum::extract::{Path}; use axum::extract::{Path};
use axum::http::{header}; use axum::http::{header};
use axum::response::Response; use axum::response::Response;
use lazy_static::lazy_static; use chrono::{DateTime, FixedOffset, NaiveDateTime, Offset, Utc};
use tera::{Context, Tera};
use timeago::Formatter;
use crate::svg::Renderer;
lazy_static! { use crate::parse::split_on_extension;
static ref TEMPLATES: Tera = { use crate::raster::Rasterizer;
let mut _tera = match Tera::new("templates/**/*.svg") { use crate::template::{OutputForm, render_template, RenderContext};
Ok(t) => {
let names: Vec<&str> = t.get_template_names().collect();
println!("{} templates found ([{}]).", names.len(), names.join(", ")); fn parse_path(path: &str) -> (&str, &str) {
t split_on_extension(path)
}, .or_else(|| Some((path, "svg")))
Err(e) => { .unwrap()
println!("Parsing error(s): {}", e); }
::std::process::exit(1);
enum TimeBannerError {
ParseError(String),
RenderError(String),
RasterizeError(String),
}
fn handle_rasterize(data: String, extension: &str) -> Result<(&str, Bytes), TimeBannerError> {
match extension {
"svg" => Ok(("image/svg+xml", Bytes::from(data))),
"png" => {
let renderer = Rasterizer::new();
let raw_image = renderer.render(data.into_bytes());
if raw_image.is_err() {
return Err(TimeBannerError::RasterizeError(raw_image.unwrap_err().message.unwrap_or("Unknown error".to_string())));
} }
};
_tera
};
}
fn convert_epoch(epoch: u64) -> SystemTime { Ok(("image/x-png", Bytes::from(raw_image.unwrap())))
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(epoch)
}
fn parse_path(path: String) -> Result<(SystemTime, String), String> {
if path.contains(".") {
let split_path = path.split_once(".").unwrap();
let epoch_int = split_path.0.parse::<u64>();
if epoch_int.is_err() {
return Err("Epoch is not a valid integer.".to_string());
} }
_ => Err(TimeBannerError::ParseError(format!("Unsupported extension: {}", extension)))
return Ok((convert_epoch(epoch_int.unwrap()), split_path.1.parse().unwrap()));
} }
let epoch_int = path.parse::<u64>();
if epoch_int.is_err() {
return Err("Epoch is not a valid integer.".to_string());
}
Ok(
(convert_epoch(epoch_int.unwrap()), String::from("svg"))
)
} }
pub async fn relative_handler(Path(path): Path<String>) -> impl IntoResponse {
let (raw_time, extension) = parse_path(path.as_str());
}
pub async fn absolute_handler(Path(path): Path<String>) -> impl IntoResponse {
let (raw_time, extension) = parse_path(path.as_str());
}
// basic handler that responds with a static string // basic handler that responds with a static string
pub async fn root_handler(Path(path): Path<String>) -> impl IntoResponse { pub async fn implicit_handler(Path(path): Path<String>) -> impl IntoResponse {
let renderer = Renderer::new(); // Get extension if available
let mut context = Context::new(); let (raw_time, extension) = parse_path(path.as_str());
let f = Formatter::new();
let parse_result = parse_path(path); // Parse epoch
if parse_result.is_err() { let parsed_epoch = raw_time.parse::<i64>();
if parsed_epoch.is_err() {
return Response::builder() return Response::builder()
.status(StatusCode::BAD_REQUEST) .status(StatusCode::BAD_REQUEST)
.body(Full::from(parse_result.err().unwrap())) .body(Full::from(format!("Failed to parse epoch :: {}", parsed_epoch.unwrap_err())))
.unwrap(); .unwrap();
} }
let (epoch, extension) = parse_result.unwrap();
context.insert("text", &f.convert(epoch.elapsed().ok().unwrap())); // Convert epoch to DateTime
context.insert("width", "512"); let naive_time = NaiveDateTime::from_timestamp_opt(parsed_epoch.unwrap(), 0);
context.insert("height", "34"); let utc_time = DateTime::<Utc>::from_utc(naive_time.unwrap(), Utc);
let data = TEMPLATES.render("basic.svg", &context); // Build context for rendering
if data.is_err() { let context = RenderContext {
output_form: OutputForm::Relative,
value: utc_time,
tz_offset: utc_time.offset().fix(),
tz_name: "UTC",
view: "basic",
};
let rendered_template = render_template(context);
if rendered_template.is_err() {
return Response::builder() return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::from( .body(Full::from(
format!("Template Could Not Be Rendered :: {}", data.err().unwrap()) format!("Template Could Not Be Rendered :: {}", rendered_template.err().unwrap())
)) ))
.unwrap(); .unwrap();
} }
match extension.as_str() { let rasterize_result = handle_rasterize(rendered_template.unwrap(), extension);
"svg" => { match rasterize_result {
Response::builder() Ok((mime_type, bytes)) => {
.header(header::CONTENT_TYPE, "image/svg+xml")
.body(Full::from(data.unwrap()))
.unwrap()
}
"png" => {
let raw_image = renderer.render(data.unwrap().into_bytes());
if raw_image.is_err() {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::from(
format!("Internal Server Error :: {}", raw_image.err().unwrap())
))
.unwrap();
}
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header(header::CONTENT_TYPE, "image/x-png") .header(header::CONTENT_TYPE, mime_type)
.body(Full::from(raw_image.unwrap())) .body(Full::from(bytes))
.unwrap() .unwrap()
} }
_ => { Err(e) => {
Response::builder() match e {
.status(StatusCode::BAD_REQUEST) TimeBannerError::RenderError(msg) => Response::builder()
.body(Full::from("Unsupported extension.")) .status(StatusCode::INTERNAL_SERVER_ERROR)
.unwrap() .body(Full::from(format!("Template Could Not Be Rendered :: {}", msg)))
.unwrap(),
TimeBannerError::ParseError(msg) => Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::from(format!("Failed to parse epoch :: {}", msg)))
.unwrap(),
TimeBannerError::RasterizeError(msg) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::from(format!("Failed to rasterize :: {}", msg)))
.unwrap(),
}
} }
} }
} }

47
src/template.rs Normal file
View File

@@ -0,0 +1,47 @@
use chrono::{DateTime, FixedOffset, Utc};
use timeago::Formatter;
use tera::{Context, Tera};
use lazy_static::lazy_static;
lazy_static! {
static ref TEMPLATES: Tera = {
let mut _tera = match Tera::new("templates/**/*.svg") {
Ok(t) => {
let names: Vec<&str> = t.get_template_names().collect();
println!("{} templates found ([{}]).", names.len(), names.join(", "));
t
},
Err(e) => {
println!("Parsing error(s): {}", e);
::std::process::exit(1);
}
};
_tera
};
}
pub enum OutputForm {
Relative,
Absolute,
}
pub struct RenderContext<'a> {
pub output_form: OutputForm,
pub value: DateTime<Utc>,
pub tz_offset: FixedOffset,
pub tz_name: &'a str,
pub view: &'a str,
}
pub fn render_template(context: RenderContext) -> Result<String, tera::Error> {
let mut template_context = Context::new();
let formatter = Formatter::new();
template_context.insert("text", match context.output_form {
OutputForm::Relative => formatter.convert_chrono(context.value, Utc::now()),
OutputForm::Absolute => context.value.to_rfc3339()
}.as_str());
TEMPLATES.render("basic.svg", &template_context)
}

View File

@@ -1,4 +1,4 @@
<svg width="{{ width }}" height="{{ height }}" xmlns="http://www.w3.org/2000/svg" font-family="Roboto Mono" font-size="27"> <svg width="512" height="34" xmlns="http://www.w3.org/2000/svg" font-family="Roboto Mono" font-size="27">
<text x="8" y="27">{{ text }}</text> <text x="8" y="27">{{ text }}</text>
<style> <style>
text text

Before

Width:  |  Height:  |  Size: 209 B

After

Width:  |  Height:  |  Size: 191 B