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"
|
||||
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
@@ -704,6 +710,16 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ico"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.23"
|
||||
@@ -1563,6 +1579,7 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"envy",
|
||||
"futures",
|
||||
"ico",
|
||||
"lazy_static",
|
||||
"phf 0.12.1",
|
||||
"phf_codegen 0.12.1",
|
||||
|
||||
@@ -25,6 +25,7 @@ phf = { version = "0.12.1", features = ["macros"] }
|
||||
phf_codegen = "0.12.1"
|
||||
chrono = "0.4.41"
|
||||
regex = "1.11.1"
|
||||
ico = "0.4.0"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = "0.4.41"
|
||||
|
||||
@@ -5,6 +5,7 @@ use axum::body::Bytes;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Output format for rendered time banners.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -41,7 +42,7 @@ impl OutputFormat {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
OutputFormat::Svg => Ok(Bytes::from(data)),
|
||||
OutputFormat::Png => {
|
||||
@@ -103,3 +104,50 @@ pub fn render_time_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::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::utils::parse_path;
|
||||
use axum::extract::ConnectInfo;
|
||||
use axum::extract::Path;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
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.
|
||||
///
|
||||
/// 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 {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// Log the IP address for the favicon request
|
||||
tracing::info!("Favicon request from IP: {}", addr.ip());
|
||||
|
||||
// Generate clock favicon showing current time
|
||||
render_time_response(now, OutputForm::Clock, "png").into_response()
|
||||
// Generate PNG bytes directly for conversion
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user