From e36a015a7c57a44c757e33f5eef4c8cbd60c91ad Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sun, 6 Jul 2025 12:57:36 +0200 Subject: [PATCH] Add theming support and basic docs.rs theme --- src/lib.rs | 85 +++++++++++++++--- ...tes_io_og_image__tests__docs_rs_theme.snap | 6 ++ ...io_og_image__tests__docs_rs_theme.snap.png | Bin 0 -> 21811 bytes template/assets/docs-rs-logo.svg | 1 + template/og-image.typ | 61 +++++++++---- 5 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 src/snapshots/crates_io_og_image__tests__docs_rs_theme.snap create mode 100644 src/snapshots/crates_io_og_image__tests__docs_rs_theme.snap.png create mode 100644 template/assets/docs-rs-logo.svg diff --git a/src/lib.rs b/src/lib.rs index 6c6ad57..48b406b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,21 @@ use tokio::fs; use tokio::process::Command; use tracing::{debug, error, info, instrument, warn}; +/// Theme options for OpenGraph image generation +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum Theme { + /// Default crates.io theme + CratesIo, + /// docs.rs theme with black and white colors and docs.rs logo + DocsRs, +} + +impl Default for Theme { + fn default() -> Self { + Self::CratesIo + } +} + /// Data structure containing information needed to generate an OpenGraph image /// for a crates.io crate. #[derive(Debug, Clone, Serialize)] @@ -74,6 +89,7 @@ pub struct OgImageGenerator { typst_binary_path: PathBuf, typst_font_path: Option, oxipng_binary_path: PathBuf, + theme: Theme, } impl OgImageGenerator { @@ -219,6 +235,24 @@ impl OgImageGenerator { self } + /// Sets the theme for OpenGraph image generation. + /// + /// This allows specifying which visual theme to use when generating images. + /// Defaults to `Theme::CratesIo` if not explicitly set. + /// + /// # Examples + /// + /// ``` + /// use crates_io_og_image::{OgImageGenerator, Theme}; + /// + /// let generator = OgImageGenerator::default() + /// .with_theme(Theme::DocsRs); + /// ``` + pub fn with_theme(mut self, theme: Theme) -> Self { + self.theme = theme; + self + } + /// Processes avatars by downloading URLs and copying assets to the assets directory. /// /// This method handles both asset-based avatars (which are copied from the bundled assets) @@ -380,11 +414,17 @@ impl OgImageGenerator { fs::create_dir(&assets_dir).await?; debug!("Copying bundled assets to temporary directory"); - let cargo_logo = include_bytes!("../template/assets/cargo.png"); - fs::write(assets_dir.join("cargo.png"), cargo_logo).await?; let rust_logo_svg = include_bytes!("../template/assets/rust-logo.svg"); fs::write(assets_dir.join("rust-logo.svg"), rust_logo_svg).await?; + if self.theme == Theme::DocsRs { + let docs_rs_logo_white_svg = include_bytes!("../template/assets/docs-rs-logo.svg"); + fs::write(assets_dir.join("docs-rs-logo.svg"), docs_rs_logo_white_svg).await?; + } else { + let cargo_logo = include_bytes!("../template/assets/cargo.png"); + fs::write(assets_dir.join("cargo.png"), cargo_logo).await?; + } + // Copy SVG icons debug!("Copying SVG icon assets"); let code_branch_svg = include_bytes!("../template/assets/code-branch.svg"); @@ -419,24 +459,29 @@ impl OgImageGenerator { let output_file = NamedTempFile::new().map_err(OgImageError::TempFileError)?; debug!(output_path = %output_file.path().display(), "Created output file"); - // Serialize data and avatar_map to JSON - debug!("Serializing data and avatar map to JSON"); + // Serialize data, avatar_map, and theme to JSON + debug!("Serializing data, avatar map, and theme to JSON"); let json_data = serde_json::to_string(&data).map_err(OgImageError::JsonSerializationError)?; let json_avatar_map = serde_json::to_string(&avatar_map).map_err(OgImageError::JsonSerializationError)?; + let json_theme = + serde_json::to_string(&self.theme).map_err(OgImageError::JsonSerializationError)?; + // Run typst compile command with input data info!("Running Typst compilation command"); let mut command = Command::new(&self.typst_binary_path); command.arg("compile").arg("--format").arg("png"); - // Pass in the data and avatar map as JSON inputs + // Pass in the data, avatar map, and theme as JSON inputs let input = format!("data={json_data}"); command.arg("--input").arg(input); let input = format!("avatar_map={json_avatar_map}"); command.arg("--input").arg(input); + let input = format!("theme={json_theme}"); + command.arg("--input").arg(input); // Pass in the font path if specified if let Some(font_path) = &self.typst_font_path { @@ -578,6 +623,7 @@ impl Default for OgImageGenerator { typst_binary_path: PathBuf::from("typst"), typst_font_path: None, oxipng_binary_path: PathBuf::from("oxipng"), + theme: Theme::default(), } } } @@ -752,9 +798,10 @@ mod tests { } } - async fn generate_image(data: OgImageData<'_>) -> Option> { - let generator = - OgImageGenerator::from_environment().expect("Failed to create OgImageGenerator"); + async fn generate_image(data: OgImageData<'_>, theme: Theme) -> Option> { + let generator = OgImageGenerator::from_environment() + .expect("Failed to create OgImageGenerator") + .with_theme(theme); let temp_file = generator .generate(data) @@ -769,7 +816,7 @@ mod tests { let _guard = init_tracing(); let data = create_simple_test_data(); - if let Some(image_data) = generate_image(data).await { + if let Some(image_data) = generate_image(data, Theme::CratesIo).await { insta::assert_binary_snapshot!("generated_og_image.png", image_data); } } @@ -784,7 +831,7 @@ mod tests { let authors = create_overflow_authors(&server_url); let data = create_overflow_test_data(&authors); - if let Some(image_data) = generate_image(data).await { + if let Some(image_data) = generate_image(data, Theme::CratesIo).await { insta::assert_binary_snapshot!("generated_og_image_overflow.png", image_data); } } @@ -794,7 +841,7 @@ mod tests { let _guard = init_tracing(); let data = create_minimal_test_data(); - if let Some(image_data) = generate_image(data).await { + if let Some(image_data) = generate_image(data, Theme::CratesIo).await { insta::assert_binary_snapshot!("generated_og_image_minimal.png", image_data); } } @@ -809,7 +856,7 @@ mod tests { let authors = create_escaping_authors(&server_url); let data = create_escaping_test_data(&authors); - if let Some(image_data) = generate_image(data).await { + if let Some(image_data) = generate_image(data, Theme::CratesIo).await { insta::assert_binary_snapshot!("generated_og_image_escaping.png", image_data); } } @@ -838,7 +885,7 @@ mod tests { releases: 1, }; - if let Some(image_data) = generate_image(data).await { + if let Some(image_data) = generate_image(data, Theme::CratesIo).await { insta::assert_binary_snapshot!("404-avatar.png", image_data); } } @@ -866,8 +913,18 @@ mod tests { releases: 3, }; - if let Some(image_data) = generate_image(data).await { + if let Some(image_data) = generate_image(data, Theme::CratesIo).await { insta::assert_binary_snapshot!("unicode-truncation.png", image_data); } } + + #[tokio::test] + async fn test_generate_og_image_docs_rs_theme() { + let _guard = init_tracing(); + let data = create_simple_test_data(); + + if let Some(image_data) = generate_image(data, Theme::DocsRs).await { + insta::assert_binary_snapshot!("docs_rs_theme.png", image_data); + } + } } diff --git a/src/snapshots/crates_io_og_image__tests__docs_rs_theme.snap b/src/snapshots/crates_io_og_image__tests__docs_rs_theme.snap new file mode 100644 index 0000000..60d2fbd --- /dev/null +++ b/src/snapshots/crates_io_og_image__tests__docs_rs_theme.snap @@ -0,0 +1,6 @@ +--- +source: src/lib.rs +expression: image_data +extension: png +snapshot_kind: binary +--- diff --git a/src/snapshots/crates_io_og_image__tests__docs_rs_theme.snap.png b/src/snapshots/crates_io_og_image__tests__docs_rs_theme.snap.png new file mode 100644 index 0000000000000000000000000000000000000000..13c7ab0c93ac3518a582138b815c657716746e33 GIT binary patch literal 21811 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 dzsUD%ffhK6oqU--cu-}$jP%X*8g \ No newline at end of file diff --git a/template/og-image.typ b/template/og-image.typ index 65f15cb..8a31c4f 100644 --- a/template/og-image.typ +++ b/template/og-image.typ @@ -4,10 +4,19 @@ // This template generates Open Graph images for crates.io crate. // ============================================================================= -// COLOR PALETTE +// DATA LOADING // ============================================================================= +// Load data from sys.inputs first so we can determine the theme + +#let data = json(bytes(sys.inputs.data)) +#let avatar_map = json(bytes(sys.inputs.at("avatar_map", default: "{}"))) +#let theme = json(bytes(sys.inputs.at("theme", default: "\"CratesIo\""))) -#let colors = ( +// ============================================================================= +// COLOR PALETTES +// ============================================================================= + +#let colors-crates-io = ( bg: oklch(97%, 0.0147, 98deg), rust-overlay: oklch(36%, 0.07, 144deg, 20%), header-bg: oklch(36%, 0.07, 144deg), @@ -21,6 +30,27 @@ tag-text: oklch(100%, 0, 0deg), ) +#let colors-docs-rs = ( + bg: oklch(100%, 0, 0deg), + rust-overlay: oklch(0%, 0, 0deg, 20%), + header-bg: oklch(0%, 0, 0deg), + header-text: oklch(100%, 0, 0deg), + primary: oklch(0%, 0, 0deg), + text: oklch(20%, 0, 0deg), + text-light: oklch(40%, 0, 0deg), + avatar-bg: oklch(100%, 0, 0deg), + avatar-border: oklch(80%, 0, 0deg), + tag-bg: oklch(0%, 0, 0deg), + tag-text: oklch(100%, 0, 0deg), +) + +// Set colors based on theme +#let colors = if theme == "DocsRs" { + colors-docs-rs +} else { + colors-crates-io +} + // ============================================================================= // LAYOUT CONSTANTS // ============================================================================= @@ -221,12 +251,19 @@ // ============================================================================= // Reusable components for consistent styling -#let render-header = { +#let render-header(theme) = { rect(width: 100%, height: header-height, fill: colors.header-bg, { place(left + horizon, dx: 30pt, { - box(baseline: 30%, image("assets/cargo.png", width: 35pt)) - h(10pt) - text(size: 22pt, fill: colors.header-text, weight: "semibold")[crates.io] + if theme == "DocsRs" { + // Use white version of the docs.rs logo on dark background + box(baseline: 30%, image("assets/docs-rs-logo.svg", width: 35pt)) + h(10pt) + text(size: 22pt, fill: colors.header-text, weight: "semibold")[docs.rs] + } else { + box(baseline: 30%, image("assets/cargo.png", width: 35pt)) + h(10pt) + text(size: 22pt, fill: colors.header-text, weight: "semibold")[crates.io] + } }) }) } @@ -252,14 +289,6 @@ ) } -// ============================================================================= -// DATA LOADING -// ============================================================================= -// Load data from sys.inputs - -#let data = json(bytes(sys.inputs.data)) -#let avatar_map = json(bytes(sys.inputs.at("avatar_map", default: "{}"))) - // ============================================================================= // MAIN DOCUMENT // ============================================================================= @@ -267,8 +296,8 @@ #set page(width: 600pt, height: 315pt, margin: 0pt, fill: colors.bg) #set text(font: "Fira Sans", fill: colors.text) -// Header with crates.io branding -#render-header +// Header with theme-based branding +#render-header(theme) // Bottom border accent #place(bottom,