Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/crates_io_og_image/examples/test_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"Turbo87",
"https://avatars.githubusercontent.com/u/141300",
),
OgImageAuthorData::with_url(
"carols10cents",
"https://avatars.githubusercontent.com/u/193874",
),
],
lines_of_code: Some(2000),
crate_size: 75,
Expand Down
50 changes: 46 additions & 4 deletions crates/crates_io_og_image/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ impl OgImageGenerator {
Self::default()
}

/// Detects the image format from the first few bytes using magic numbers.
///
/// Returns the appropriate file extension for supported formats:
/// - PNG: returns "png"
/// - JPEG: returns "jpg"
/// - Unsupported formats: returns None
fn detect_image_format(bytes: &[u8]) -> Option<&'static str> {
// PNG magic number: 89 50 4E 47 0D 0A 1A 0A
if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
return Some("png");
}

// JPEG magic number: FF D8 FF
if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
return Some("jpg");
}

None
}

/// Creates a new `OgImageGenerator` using the `TYPST_PATH` environment variable.
///
/// If the `TYPST_PATH` environment variable is set, uses that path.
Expand Down Expand Up @@ -218,13 +238,9 @@ impl OgImageGenerator {
let client = reqwest::Client::new();
for (index, author) in data.authors.iter().enumerate() {
if let Some(avatar) = &author.avatar {
let filename = format!("avatar_{index}.png");
let avatar_path = assets_dir.join(&filename);

debug!(
author_name = %author.name,
avatar_url = %avatar,
avatar_path = %avatar_path.display(),
"Processing avatar for author {}", author.name
);

Expand Down Expand Up @@ -271,6 +287,32 @@ impl OgImageGenerator {
bytes
};

// Detect the image format and determine the appropriate file extension
let Some(extension) = Self::detect_image_format(&bytes) else {
// Format not supported, log warning with first 20 bytes for debugging
let debug_bytes = &bytes[..bytes.len().min(20)];
let hex_bytes = debug_bytes
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ");

warn!("Unsupported avatar format at {avatar}, first 20 bytes: {hex_bytes}");

// Skip this avatar and continue with the next one
continue;
};

let filename = format!("avatar_{index}.{extension}");
let avatar_path = assets_dir.join(&filename);

debug!(
author_name = %author.name,
avatar_url = %avatar,
avatar_path = %avatar_path.display(),
"Writing avatar file with detected format"
);

// Write the bytes to the avatar file
fs::write(&avatar_path, &bytes).await.map_err(|err| {
OgImageError::AvatarWriteError {
Expand Down