Feat: completely generate logo without relying on pngs

This commit is contained in:
Dorian Zedler 2023-04-23 17:16:25 +02:00
parent 85c6d4741f
commit 9d89154b23
Signed by: dozedler
GPG key ID: 989DE36109AFA354
10 changed files with 484 additions and 67 deletions

View file

@ -1,20 +1,38 @@
use axum::Router;
use std::net::SocketAddr;
use axum::{http::Request, response::Response, Router};
use std::{net::SocketAddr, time::Duration};
use text::EmbeddedFonts;
use tower_http::{catch_panic::CatchPanicLayer, trace::TraceLayer};
use tracing::Span;
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())
.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

View file

@ -1,9 +1,14 @@
use axum::{extract::Query, routing::get, Router};
use cairo::{Context, Format, ImageSurface};
use rust_embed::RustEmbed;
use std::time::Instant;
use axum::{
extract::{Query, State},
routing::get,
Router,
};
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)]
@ -29,53 +34,23 @@ struct ImageProperties {
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,
);
}
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!(),
fn create_surface(properties: &ImageProperties) -> ImageSurface {
let (width, height) = match (&properties.variant, &properties.orientation) {
(LogoVariant::NoText, _) => (400, 400),
(_, LogoOrientation::Landscape) => (2127, 591),
(_, LogoOrientation::Portrait) => (1654, 1654),
};
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,
)
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,9 +60,101 @@ 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]
async fn handler(
Query(properties): Query<ImageProperties>,
State(state): State<SharedState>,
) -> impl axum::response::IntoResponse {
let start = Instant::now();
let (font_regular, font_light) = state.fonts.get().await;
println!("{:?}", start.elapsed());
let start = Instant::now();
// cannot use await after this, because surface does not implement Send
let surface = create_surface(&properties);
let context = Context::new(&surface).unwrap();
println!("{:?}", (surface.width(), surface.height()));
println!("{:?}", start.elapsed());
let start = Instant::now();
draw_logo(&context, &properties);
println!("{:?}", start.elapsed());
let start = Instant::now();
draw_text(&context, &font_regular, &font_light, &properties);
println!("{:?}", start.elapsed());
let start = Instant::now();
let mut data: Vec<u8> = Vec::new();
surface.write_to_png(&mut data).unwrap();
println!("{:?}", start.elapsed());
(
axum::response::AppendHeaders([(axum::http::header::CONTENT_TYPE, "image/png")]),
@ -96,5 +163,5 @@ async fn handler(Query(properties): Query<ImageProperties>) -> impl axum::respon
}
pub fn routes() -> Router<SharedState> {
Router::new().route("/", get(handler))
Router::<SharedState>::new().route("/", get(handler))
}

74
src/text.rs Normal file
View 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();
}
}