From d886c71a7bdb8ffca52078483efdb616d4731015 Mon Sep 17 00:00:00 2001 From: torbjornmolin <37022228+torbjornmolin@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:52:43 +0200 Subject: [PATCH] Added file format picker, changed re-scale interpolation, added deduplication --- assets/index.html | 177 +++------------------------------------------- assets/style.css | 167 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 34 +++++++-- 3 files changed, 205 insertions(+), 173 deletions(-) create mode 100644 assets/style.css diff --git a/assets/index.html b/assets/index.html index 0fa9fcc..3899466 100644 --- a/assets/index.html +++ b/assets/index.html @@ -4,155 +4,7 @@ Image Generator - +
@@ -161,15 +13,11 @@

Image Generator

- - + +
@@ -203,7 +51,7 @@
- +
@@ -219,15 +67,12 @@ statusDiv.classList.add("show"); if (type === "success") { - statusIcon.src = "https://img.icons8.com/color/48/ok--v1.png"; + statusIcon.innerHTML = "✅"; } else if (type === "error") { - statusIcon.src = "https://img.icons8.com/color/48/cancel--v1.png"; + statusIcon.innerHTML = "❌"; } else { - statusIcon.src = - "https://img.icons8.com/color/48/medium-priority.png"; + statusIcon.innerHTML = "⏳"; } - - statusIcon.alt = type; }; form.addEventListener("submit", async (e) => { @@ -235,7 +80,7 @@ showStatus("Generating images... Please wait.", "info"); const formData = { - count: parseInt(form.count.value), + filetype: form.filetype.value, width: Number(form.width.value), height: Number(form.height.value), filenames: form.filenames.value diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..bff8a50 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,167 @@ +* { + box-sizing: border-box; +} +body { + font-family: "Segoe UI", sans-serif; + margin: 0; + padding: 0; + background: linear-gradient(135deg, #667eea, #764ba2); + color: #333; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.container { + width: 100%; + max-width: 600px; + background: white; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + padding: 35px; + margin: 40px 20px; + text-align: center; + position: relative; +} + +.logo { + width: 200px; + margin-bottom: 15px; +} + +h1 { + font-size: 26px; + color: #4c2885; + margin-bottom: 10px; +} + +form { + text-align: left; + margin-top: 20px; +} + +label { + display: block; + margin-top: 20px; + margin-bottom: 6px; + font-weight: 600; + color: #444; +} + +select, +::picker(select) { + appearance: base-select; +} + +select { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 14px; + margin-top: 4px; + transition: border 0.2s; +} + +select:hover, +select:focus { + /* background: #dddddd; */ +} + +input[type="text"], +input[type="number"], +textarea { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 14px; + margin-top: 4px; + transition: border 0.2s; +} + +input:focus, +textarea:focus { + border-color: #764ba2; + outline: none; +} +input:invalid textarea:invalid { + border-color: #760000; + outline: none; +} +textarea { + resize: vertical; + min-height: 100px; +} + +.row { + display: flex; + gap: 10px; +} + +.row input { + flex: 1; +} + +button { + background-color: #ff6b6b; + color: white; + padding: 12px 20px; + border: none; + border-radius: 8px; + font-size: 16px; + margin-top: 30px; + width: 100%; + cursor: pointer; + transition: background 0.3s; +} + +button:hover { + background-color: #fa5252; +} + +.status { + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; + font-size: 14px; + color: #444; + min-height: 24px; +} + +.status-icon { + width: 20px; + height: 20px; + margin-right: 8px; + opacity: 0; + transform: scale(0.8); + transition: opacity 0.3s, transform 0.3s; +} + +.status.show .status-icon { + opacity: 1; + transform: scale(1); +} + +.fade-in { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@media (max-width: 500px) { + .row { + flex-direction: column; + } +} diff --git a/src/main.rs b/src/main.rs index 6acd53d..8aea0a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,11 @@ use axum::{ }; use image::GenericImage; use serde::Deserialize; -use std::io::{Cursor, Write}; +use std::{ + collections::HashSet, + fmt::Display, + io::{Cursor, Write}, +}; use tower_http::services::ServeFile; #[tokio::main] async fn main() { @@ -14,6 +18,7 @@ async fn main() { let app = Router::new() .route_service("/", ServeFile::new("assets/index.html")) .route_service("/logo.svg", ServeFile::new("assets/logo.svg")) + .route_service("/style.css", ServeFile::new("assets/style.css")) .route("/api/generate", post(download_zip)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); @@ -22,16 +27,31 @@ async fn main() { #[derive(Deserialize)] pub struct GenerateOptions { - count: u32, + filetype: ImageFileType, filenames: Vec, width: u32, height: u32, } +#[derive(Deserialize)] +enum ImageFileType { + Png, + Jpg, +} + +impl Display for ImageFileType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ImageFileType::Png => write!(f, "png"), + ImageFileType::Jpg => write!(f, "jpg"), + } + } +} + async fn download_zip(Json(payload): Json) -> Response { println!( - "Count: {}. w: {}, h: {}, \nFiles: ' [ {} ]'", - payload.count, + "Filetype: '{}', w: {}, h: {}, \nFiles: ' [ {} ]'", + payload.filetype, payload.width, payload.height, payload.filenames.join(",") @@ -69,9 +89,9 @@ fn get_files(options: GenerateOptions) -> Vec<(String, Vec)> { let mut files: Vec<(String, Vec)> = Vec::new(); use text_to_png::TextRenderer; let renderer = TextRenderer::default(); - let image_file_ending = "png"; + let image_file_ending = options.filetype; - for f in options.filenames { + for f in options.filenames.iter().collect::>() { let mut imgbuf = image::ImageBuffer::new(options.width, options.height); for (_x, _y, pixel) in imgbuf.enumerate_pixels_mut() { *pixel = image::Rgba([190, 190, 190, 255]); @@ -89,7 +109,7 @@ fn get_files(options: GenerateOptions) -> Vec<(String, Vec)> { let resized_text = pngtextsource.resize( options.width / 2, options.height, - image::imageops::FilterType::Nearest, + image::imageops::FilterType::CatmullRom, ); let xpos = (imgbuf.width() - resized_text.width()) / 2; let ypos = (imgbuf.height() - resized_text.height()) / 2;