Compare commits
5 commits
85c6d4741f
...
8b139af199
Author | SHA1 | Date | |
---|---|---|---|
8b139af199 | |||
e912b8a4d1 | |||
4bdcacba41 | |||
cec5d50aa6 | |||
9d89154b23 |
12 changed files with 852 additions and 298 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
|||
[submodule "artwork"]
|
||||
path = artwork
|
||||
url = https://git.makerlab-murnau.de/MakerLab/artwork.git
|
874
Cargo.lock
generated
874
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,7 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
png = "0.11.0"
|
||||
cairo-rs = { version = "0.17.0", default-features = false, features = ["png"] }
|
||||
cairo-rs = { version = "0.17.0", default-features = false, features = ["png", "svg", "freetype"] }
|
||||
axum = "0.6.10"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
rand = "0.8.5"
|
||||
|
@ -15,3 +15,6 @@ serde = { version = "1.0.152", features = ["derive"] }
|
|||
rust-embed = { version = "6.6.0", features = ["include-exclude"] }
|
||||
mime_guess = "2.0.4"
|
||||
serde_json = "1.0.94"
|
||||
axum-macros = "0.3.7"
|
||||
tower-http = { version = "0.4.0", features = ["fs", "full"] }
|
||||
tracing = "0.1.37"
|
||||
|
|
1
artwork
1
artwork
|
@ -1 +0,0 @@
|
|||
Subproject commit 13b39b2537350a24fe69a309e7f676a3c25f7bd0
|
BIN
fonts/Nunito-ExtraLight.ttf
Normal file
BIN
fonts/Nunito-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
fonts/Nunito-Light.ttf
Normal file
BIN
fonts/Nunito-Light.ttf
Normal file
Binary file not shown.
BIN
fonts/Nunito-Regular.ttf
Normal file
BIN
fonts/Nunito-Regular.ttf
Normal file
Binary file not shown.
33
src/main.rs
33
src/main.rs
|
@ -1,20 +1,41 @@
|
|||
use axum::Router;
|
||||
use std::net::SocketAddr;
|
||||
use axum::{http::Request, response::Response, routing::get, Router};
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
use text::EmbeddedFonts;
|
||||
use tower_http::{catch_panic::CatchPanicLayer, trace::TraceLayer};
|
||||
use tracing::Span;
|
||||
|
||||
use crate::routes::static_files;
|
||||
|
||||
mod color;
|
||||
mod polygon;
|
||||
mod routes;
|
||||
mod text;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SharedState {}
|
||||
pub struct SharedState {
|
||||
fonts: EmbeddedFonts,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let state = SharedState {};
|
||||
let state = SharedState {
|
||||
fonts: EmbeddedFonts::load(),
|
||||
};
|
||||
let app = Router::new()
|
||||
.nest("/", routes::static_files::routes())
|
||||
.nest("/logo", routes::logo::routes())
|
||||
.fallback(static_files::router)
|
||||
.route("/logo", get(routes::logo::png))
|
||||
.route("/logo.png", get(routes::logo::png))
|
||||
.nest("/favicon.ico", routes::favicon::routes())
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.on_request(|request: &Request<_>, _span: &Span| {
|
||||
println!("Request {} {}", request.method(), request.uri());
|
||||
})
|
||||
.on_response(|response: &Response, latency: Duration, _span: &Span| {
|
||||
println!("Response {}, {}ms", response.status(), latency.as_millis());
|
||||
}),
|
||||
)
|
||||
.layer(CatchPanicLayer::new())
|
||||
.with_state(state);
|
||||
|
||||
// run it
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use axum::{extract::Query, routing::get, Router};
|
||||
use cairo::{Context, Format, ImageSurface};
|
||||
use rust_embed::RustEmbed;
|
||||
use axum::extract::{Query, State};
|
||||
use cairo::{freetype::Face, Context, FontFace, Format, ImageSurface};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{polygon, SharedState};
|
||||
use crate::{color::Color, polygon, text::DrawableText, SharedState};
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[allow(non_camel_case_types)]
|
||||
|
@ -22,60 +21,33 @@ enum LogoOrientation {
|
|||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct ImageProperties {
|
||||
pub struct ImageProperties {
|
||||
#[serde(default)]
|
||||
variant: LogoVariant,
|
||||
#[serde(default)]
|
||||
orientation: LogoOrientation,
|
||||
}
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "artwork/logo"]
|
||||
#[include = "*_T.png"]
|
||||
struct ImageFiles;
|
||||
|
||||
fn get_surface_and_logo_coordiates(
|
||||
properties: &ImageProperties,
|
||||
) -> (ImageSurface, (f64, f64), f64, f64) {
|
||||
if let LogoVariant::NoText = properties.variant {
|
||||
return (
|
||||
ImageSurface::create(Format::ARgb32, 400, 400).unwrap(),
|
||||
(200.0, 200.0),
|
||||
148.0,
|
||||
67.0,
|
||||
);
|
||||
fn get_surface_size(properties: &ImageProperties) -> (i32, i32) {
|
||||
match (&properties.variant, &properties.orientation) {
|
||||
(LogoVariant::NoText, _) => (400, 400),
|
||||
(_, LogoOrientation::Landscape) => (2127, 591),
|
||||
(_, LogoOrientation::Portrait) => (1654, 1654),
|
||||
}
|
||||
}
|
||||
|
||||
let background_image_path = match (&properties.variant, &properties.orientation) {
|
||||
(LogoVariant::DarkText, LogoOrientation::Landscape) => "landscape/4C_T.png",
|
||||
(LogoVariant::LightText, LogoOrientation::Landscape) => "landscape/W_T.png",
|
||||
(LogoVariant::DarkText, LogoOrientation::Portrait) => "portrait/4C_T.png",
|
||||
(LogoVariant::LightText, LogoOrientation::Portrait) => "portrait/W_T.png",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let (coordinates, outer_radius, inner_radius) = match &properties.orientation {
|
||||
LogoOrientation::Landscape => ((412.0, 299.0), 209.0, 99.0),
|
||||
LogoOrientation::Portrait => ((828.0, 563.0), 253.0, 118.0),
|
||||
};
|
||||
|
||||
let image = ImageFiles::get(background_image_path).unwrap();
|
||||
(
|
||||
ImageSurface::create_from_png(&mut image.data.as_ref()).unwrap(),
|
||||
coordinates,
|
||||
outer_radius,
|
||||
inner_radius,
|
||||
)
|
||||
fn create_image_surface(properties: &ImageProperties) -> ImageSurface {
|
||||
let (width, height) = get_surface_size(properties);
|
||||
ImageSurface::create(Format::ARgb32, width, height).unwrap()
|
||||
}
|
||||
|
||||
async fn handler(Query(properties): Query<ImageProperties>) -> impl axum::response::IntoResponse {
|
||||
let (surface, logo_coordinates, logo_outer_radius, logo_inner_radius) =
|
||||
get_surface_and_logo_coordiates(&properties);
|
||||
let context = Context::new(&surface).unwrap();
|
||||
|
||||
context.set_source_rgba(0.0, 0.0, 0.0, 0.0);
|
||||
|
||||
context.paint().unwrap();
|
||||
fn draw_logo(context: &Context, properties: &ImageProperties) {
|
||||
let (logo_coordinates, logo_outer_radius, logo_inner_radius) =
|
||||
match (&properties.variant, &properties.orientation) {
|
||||
(LogoVariant::NoText, _) => ((200.0, 200.0), 148.0, 67.0),
|
||||
(_, LogoOrientation::Landscape) => ((412.0, 299.0), 209.0, 99.0),
|
||||
(_, LogoOrientation::Portrait) => ((828.0, 563.0), 253.0, 118.0),
|
||||
};
|
||||
|
||||
polygon::draw_polygon_of_segmented_polygons(
|
||||
logo_coordinates,
|
||||
|
@ -85,6 +57,81 @@ async fn handler(Query(properties): Query<ImageProperties>) -> impl axum::respon
|
|||
&context,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn draw_text(
|
||||
context: &Context,
|
||||
font_regular: &Face,
|
||||
font_light: &Face,
|
||||
properties: &ImageProperties,
|
||||
) {
|
||||
if let LogoVariant::NoText = properties.variant {
|
||||
return;
|
||||
}
|
||||
|
||||
let color = match &properties.variant {
|
||||
LogoVariant::DarkText => Color::from("#383F50"),
|
||||
LogoVariant::LightText => Color::from("#ffffff"),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
context.set_source_rgb(
|
||||
color.r as f64 / 255.0,
|
||||
color.g as f64 / 255.0,
|
||||
color.b as f64 / 255.0,
|
||||
);
|
||||
|
||||
let texts = match &properties.orientation {
|
||||
LogoOrientation::Landscape => (
|
||||
DrawableText {
|
||||
position: (760.0, 356.0),
|
||||
size: 228.0,
|
||||
spacing: 45.5,
|
||||
text: "makerlab",
|
||||
},
|
||||
DrawableText {
|
||||
position: (1495.0, 504.0),
|
||||
size: 110.0,
|
||||
spacing: 10.5,
|
||||
text: "MURNAU",
|
||||
},
|
||||
),
|
||||
LogoOrientation::Portrait => (
|
||||
DrawableText {
|
||||
position: (48.0, 1242.0),
|
||||
size: 280.0,
|
||||
spacing: 53.5,
|
||||
text: "makerlab",
|
||||
},
|
||||
DrawableText {
|
||||
position: (493.0, 1424.0),
|
||||
size: 135.0,
|
||||
spacing: 13.0,
|
||||
text: "MURNAU",
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
context.set_font_face(&FontFace::create_from_ft(font_regular).unwrap());
|
||||
texts.0.draw(&context);
|
||||
|
||||
context.set_font_face(&FontFace::create_from_ft(font_light).unwrap());
|
||||
texts.1.draw(&context);
|
||||
}
|
||||
|
||||
#[axum_macros::debug_handler]
|
||||
pub async fn png(
|
||||
Query(properties): Query<ImageProperties>,
|
||||
State(state): State<SharedState>,
|
||||
) -> impl axum::response::IntoResponse {
|
||||
let (font_regular, font_light) = state.fonts.get().await;
|
||||
|
||||
// cannot use await after this, because surface does not implement Send
|
||||
let surface = create_image_surface(&properties);
|
||||
let context = Context::new(&surface).unwrap();
|
||||
|
||||
draw_logo(&context, &properties);
|
||||
draw_text(&context, &font_regular, &font_light, &properties);
|
||||
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
surface.write_to_png(&mut data).unwrap();
|
||||
|
@ -94,7 +141,3 @@ async fn handler(Query(properties): Query<ImageProperties>) -> impl axum::respon
|
|||
data,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn routes() -> Router<SharedState> {
|
||||
Router::new().route("/", get(handler))
|
||||
}
|
||||
|
|
|
@ -2,17 +2,14 @@ use axum::{
|
|||
body::{boxed, Full},
|
||||
http::{header, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
Router,
|
||||
};
|
||||
use rust_embed::{EmbeddedFile, RustEmbed};
|
||||
|
||||
use crate::SharedState;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "web"]
|
||||
struct StaticFiles;
|
||||
|
||||
async fn static_files(uri: Uri) -> impl IntoResponse {
|
||||
pub async fn router(uri: Uri) -> impl IntoResponse {
|
||||
let path = uri.path().trim_start_matches('/');
|
||||
|
||||
match StaticFiles::get(path) {
|
||||
|
@ -50,7 +47,3 @@ fn not_found() -> Response {
|
|||
.body(boxed(Full::from("404")))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn routes() -> Router<SharedState> {
|
||||
Router::new().fallback(static_files)
|
||||
}
|
||||
|
|
74
src/text.rs
Normal file
74
src/text.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use cairo::{
|
||||
freetype::{Face, Library},
|
||||
Context,
|
||||
};
|
||||
use rust_embed::RustEmbed;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "fonts"]
|
||||
struct FontFiles;
|
||||
|
||||
struct InternalFonts {
|
||||
light: Face,
|
||||
regular: Face,
|
||||
}
|
||||
unsafe impl Send for InternalFonts {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EmbeddedFonts {
|
||||
fonts: Arc<Mutex<InternalFonts>>,
|
||||
}
|
||||
|
||||
impl EmbeddedFonts {
|
||||
fn load_face(lib: &Library, name: &str) -> Result<Face, cairo::freetype::Error> {
|
||||
let font_file = FontFiles::get(name).unwrap();
|
||||
let font_data = Vec::from(font_file.data);
|
||||
lib.new_memory_face(font_data, 0)
|
||||
}
|
||||
|
||||
pub fn load() -> EmbeddedFonts {
|
||||
let lib = Library::init().unwrap();
|
||||
let font_regular = Self::load_face(&lib, "Nunito-Regular.ttf").unwrap();
|
||||
let font_light = Self::load_face(&lib, "Nunito-Light.ttf").unwrap();
|
||||
|
||||
let fonts = InternalFonts {
|
||||
regular: font_regular,
|
||||
light: font_light,
|
||||
};
|
||||
|
||||
EmbeddedFonts {
|
||||
fonts: Arc::new(Mutex::new(fonts)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> (Face, Face) {
|
||||
let fonts = self.fonts.lock().await;
|
||||
(fonts.regular.clone(), fonts.light.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DrawableText {
|
||||
pub position: (f64, f64),
|
||||
pub size: f64,
|
||||
pub spacing: f64,
|
||||
pub text: &'static str,
|
||||
}
|
||||
|
||||
impl DrawableText {
|
||||
pub fn draw(&self, context: &Context) {
|
||||
context.new_path();
|
||||
context.set_font_size(self.size);
|
||||
context.line_to(self.position.0, self.position.1);
|
||||
|
||||
for char in self.text.chars() {
|
||||
context.text_path(&String::from(char));
|
||||
let (x, y) = context.current_point().unwrap();
|
||||
context.line_to(x + self.spacing, y);
|
||||
}
|
||||
context.close_path();
|
||||
context.fill().unwrap();
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@
|
|||
<label for="logo_variant">
|
||||
Variant
|
||||
</label>
|
||||
<select id="logo_variant" value="DarkText">
|
||||
<select id="logo_variant" name="variant" value="DarkText">
|
||||
<option value="DarkText" selected>dark text</option>
|
||||
<option value="LightText">light text</option>
|
||||
<option value="NoText">no text</option>
|
||||
|
@ -59,7 +59,7 @@
|
|||
Orientation
|
||||
</label>
|
||||
|
||||
<select id="logo_orientation" value="Landscape">
|
||||
<select id="logo_orientation" name="orientation" value="Landscape">
|
||||
<option value="Landscape" selected>landscape</option>
|
||||
<option value="Portrait" selected>portrait</option>
|
||||
</select>
|
||||
|
|
Loading…
Reference in a new issue