mirror of
https://github.com/Xevion/time-banner.git
synced 2025-12-05 23:16:35 -06:00
feat: favicon ico conversion
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -177,6 +177,12 @@ version = "1.23.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder-lite"
|
name = "byteorder-lite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -704,6 +710,16 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ico"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"png",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ignore"
|
name = "ignore"
|
||||||
version = "0.4.23"
|
version = "0.4.23"
|
||||||
@@ -1563,6 +1579,7 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"envy",
|
"envy",
|
||||||
"futures",
|
"futures",
|
||||||
|
"ico",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"phf 0.12.1",
|
"phf 0.12.1",
|
||||||
"phf_codegen 0.12.1",
|
"phf_codegen 0.12.1",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ phf = { version = "0.12.1", features = ["macros"] }
|
|||||||
phf_codegen = "0.12.1"
|
phf_codegen = "0.12.1"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
ico = "0.4.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use axum::body::Bytes;
|
|||||||
use axum::http::{header, StatusCode};
|
use axum::http::{header, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
/// Output format for rendered time banners.
|
/// Output format for rendered time banners.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -41,7 +42,7 @@ impl OutputFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Converts SVG to the requested format. PNG requires rasterization.
|
/// Converts SVG to the requested format. PNG requires rasterization.
|
||||||
fn handle_rasterize(data: String, format: &OutputFormat) -> Result<Bytes, TimeBannerError> {
|
pub fn handle_rasterize(data: String, format: &OutputFormat) -> Result<Bytes, TimeBannerError> {
|
||||||
match format {
|
match format {
|
||||||
OutputFormat::Svg => Ok(Bytes::from(data)),
|
OutputFormat::Svg => Ok(Bytes::from(data)),
|
||||||
OutputFormat::Png => {
|
OutputFormat::Png => {
|
||||||
@@ -103,3 +104,50 @@ pub fn render_time_response(
|
|||||||
Err(e) => get_error_response(e).into_response(),
|
Err(e) => get_error_response(e).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates PNG bytes for the favicon clock.
|
||||||
|
pub fn generate_favicon_png_bytes(time: DateTime<Utc>) -> Result<Vec<u8>, TimeBannerError> {
|
||||||
|
// Build context for rendering
|
||||||
|
let context = RenderContext {
|
||||||
|
value: time,
|
||||||
|
output_form: OutputForm::Clock,
|
||||||
|
output_format: OutputFormat::Png,
|
||||||
|
timezone: None,
|
||||||
|
format: None,
|
||||||
|
now: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render template to SVG
|
||||||
|
let rendered_template = render_template(context)
|
||||||
|
.map_err(|e| TimeBannerError::RenderError(format!("Template rendering failed: {}", e)))?;
|
||||||
|
|
||||||
|
// Convert SVG to PNG
|
||||||
|
let png_bytes = handle_rasterize(rendered_template, &OutputFormat::Png)?;
|
||||||
|
|
||||||
|
Ok(png_bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts PNG bytes to ICO format using the ico crate.
|
||||||
|
pub fn convert_png_to_ico(png_bytes: &[u8]) -> Result<Bytes, String> {
|
||||||
|
// Create a new, empty icon collection
|
||||||
|
let mut icon_dir = ico::IconDir::new(ico::ResourceType::Icon);
|
||||||
|
|
||||||
|
// Read PNG data from bytes
|
||||||
|
let cursor = Cursor::new(png_bytes);
|
||||||
|
let image =
|
||||||
|
ico::IconImage::read_png(cursor).map_err(|e| format!("Failed to read PNG data: {}", e))?;
|
||||||
|
|
||||||
|
// Add the image to the icon collection
|
||||||
|
icon_dir.add_entry(
|
||||||
|
ico::IconDirEntry::encode(&image)
|
||||||
|
.map_err(|e| format!("Failed to encode icon entry: {}", e))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write ICO data to a buffer
|
||||||
|
let mut ico_buffer = Vec::new();
|
||||||
|
icon_dir
|
||||||
|
.write(&mut ico_buffer)
|
||||||
|
.map_err(|e| format!("Failed to write ICO data: {}", e))?;
|
||||||
|
|
||||||
|
Ok(Bytes::from(ico_buffer))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::duration::parse_time_value;
|
use crate::duration::parse_time_value;
|
||||||
use crate::error::{get_error_response, TimeBannerError};
|
use crate::error::{get_error_response, TimeBannerError};
|
||||||
use crate::render::render_time_response;
|
use crate::render::{convert_png_to_ico, generate_favicon_png_bytes, render_time_response};
|
||||||
use crate::template::OutputForm;
|
use crate::template::OutputForm;
|
||||||
use crate::utils::parse_path;
|
use crate::utils::parse_path;
|
||||||
use axum::extract::ConnectInfo;
|
use axum::extract::ConnectInfo;
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
|
use axum::http::{header, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
@@ -53,15 +54,33 @@ pub async fn implicit_handler(Path(path): Path<String>) -> impl IntoResponse {
|
|||||||
|
|
||||||
/// Handles `/favicon.ico` - generates a dynamic clock favicon showing the current time.
|
/// Handles `/favicon.ico` - generates a dynamic clock favicon showing the current time.
|
||||||
///
|
///
|
||||||
/// Logs the client IP address and returns a PNG image of an analog clock.
|
/// Logs the client IP address and returns an ICO image of an analog clock.
|
||||||
pub async fn favicon_handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> impl IntoResponse {
|
pub async fn favicon_handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> impl IntoResponse {
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
|
|
||||||
// Log the IP address for the favicon request
|
// Log the IP address for the favicon request
|
||||||
tracing::info!("Favicon request from IP: {}", addr.ip());
|
tracing::info!("Favicon request from IP: {}", addr.ip());
|
||||||
|
|
||||||
// Generate clock favicon showing current time
|
// Generate PNG bytes directly for conversion
|
||||||
render_time_response(now, OutputForm::Clock, "png").into_response()
|
let png_bytes = match generate_favicon_png_bytes(now) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(e) => return get_error_response(e).into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert PNG to ICO using the ico crate
|
||||||
|
match convert_png_to_ico(&png_bytes) {
|
||||||
|
Ok(ico_bytes) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
[(header::CONTENT_TYPE, "image/x-icon")],
|
||||||
|
ico_bytes,
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(e) => get_error_response(TimeBannerError::RenderError(format!(
|
||||||
|
"Failed to convert PNG to ICO: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fallback handler for unmatched routes.
|
/// Fallback handler for unmatched routes.
|
||||||
|
|||||||
Reference in New Issue
Block a user