feat: favicon ico conversion

This commit is contained in:
2025-07-10 18:48:07 -05:00
parent 696f18af3f
commit b427c9d094
4 changed files with 90 additions and 5 deletions

17
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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))
}

View File

@@ -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.