@@ -18,6 +18,21 @@ use tokio::fs;
18
18
use tokio:: process:: Command ;
19
19
use tracing:: { debug, error, info, instrument, warn} ;
20
20
21
+ /// Theme options for OpenGraph image generation
22
+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize ) ]
23
+ pub enum Theme {
24
+ /// Default crates.io theme
25
+ CratesIo ,
26
+ /// docs.rs theme with black and white colors and docs.rs logo
27
+ DocsRs ,
28
+ }
29
+
30
+ impl Default for Theme {
31
+ fn default ( ) -> Self {
32
+ Self :: CratesIo
33
+ }
34
+ }
35
+
21
36
/// Data structure containing information needed to generate an OpenGraph image
22
37
/// for a crates.io crate.
23
38
#[ derive( Debug , Clone , Serialize ) ]
@@ -74,6 +89,7 @@ pub struct OgImageGenerator {
74
89
typst_binary_path : PathBuf ,
75
90
typst_font_path : Option < PathBuf > ,
76
91
oxipng_binary_path : PathBuf ,
92
+ theme : Theme ,
77
93
}
78
94
79
95
impl OgImageGenerator {
@@ -219,6 +235,24 @@ impl OgImageGenerator {
219
235
self
220
236
}
221
237
238
+ /// Sets the theme for OpenGraph image generation.
239
+ ///
240
+ /// This allows specifying which visual theme to use when generating images.
241
+ /// Defaults to `Theme::CratesIo` if not explicitly set.
242
+ ///
243
+ /// # Examples
244
+ ///
245
+ /// ```
246
+ /// use crates_io_og_image::{OgImageGenerator, Theme};
247
+ ///
248
+ /// let generator = OgImageGenerator::default()
249
+ /// .with_theme(Theme::DocsRs);
250
+ /// ```
251
+ pub fn with_theme ( mut self , theme : Theme ) -> Self {
252
+ self . theme = theme;
253
+ self
254
+ }
255
+
222
256
/// Processes avatars by downloading URLs and copying assets to the assets directory.
223
257
///
224
258
/// This method handles both asset-based avatars (which are copied from the bundled assets)
@@ -380,11 +414,17 @@ impl OgImageGenerator {
380
414
fs:: create_dir ( & assets_dir) . await ?;
381
415
382
416
debug ! ( "Copying bundled assets to temporary directory" ) ;
383
- let cargo_logo = include_bytes ! ( "../template/assets/cargo.png" ) ;
384
- fs:: write ( assets_dir. join ( "cargo.png" ) , cargo_logo) . await ?;
385
417
let rust_logo_svg = include_bytes ! ( "../template/assets/rust-logo.svg" ) ;
386
418
fs:: write ( assets_dir. join ( "rust-logo.svg" ) , rust_logo_svg) . await ?;
387
419
420
+ if self . theme == Theme :: DocsRs {
421
+ let docs_rs_logo_white_svg = include_bytes ! ( "../template/assets/docs-rs-logo.svg" ) ;
422
+ fs:: write ( assets_dir. join ( "docs-rs-logo.svg" ) , docs_rs_logo_white_svg) . await ?;
423
+ } else {
424
+ let cargo_logo = include_bytes ! ( "../template/assets/cargo.png" ) ;
425
+ fs:: write ( assets_dir. join ( "cargo.png" ) , cargo_logo) . await ?;
426
+ }
427
+
388
428
// Copy SVG icons
389
429
debug ! ( "Copying SVG icon assets" ) ;
390
430
let code_branch_svg = include_bytes ! ( "../template/assets/code-branch.svg" ) ;
@@ -419,24 +459,29 @@ impl OgImageGenerator {
419
459
let output_file = NamedTempFile :: new ( ) . map_err ( OgImageError :: TempFileError ) ?;
420
460
debug ! ( output_path = %output_file. path( ) . display( ) , "Created output file" ) ;
421
461
422
- // Serialize data and avatar_map to JSON
423
- debug ! ( "Serializing data and avatar map to JSON" ) ;
462
+ // Serialize data, avatar_map, and theme to JSON
463
+ debug ! ( "Serializing data, avatar map, and theme to JSON" ) ;
424
464
let json_data =
425
465
serde_json:: to_string ( & data) . map_err ( OgImageError :: JsonSerializationError ) ?;
426
466
427
467
let json_avatar_map =
428
468
serde_json:: to_string ( & avatar_map) . map_err ( OgImageError :: JsonSerializationError ) ?;
429
469
470
+ let json_theme =
471
+ serde_json:: to_string ( & self . theme ) . map_err ( OgImageError :: JsonSerializationError ) ?;
472
+
430
473
// Run typst compile command with input data
431
474
info ! ( "Running Typst compilation command" ) ;
432
475
let mut command = Command :: new ( & self . typst_binary_path ) ;
433
476
command. arg ( "compile" ) . arg ( "--format" ) . arg ( "png" ) ;
434
477
435
- // Pass in the data and avatar map as JSON inputs
478
+ // Pass in the data, avatar map, and theme as JSON inputs
436
479
let input = format ! ( "data={json_data}" ) ;
437
480
command. arg ( "--input" ) . arg ( input) ;
438
481
let input = format ! ( "avatar_map={json_avatar_map}" ) ;
439
482
command. arg ( "--input" ) . arg ( input) ;
483
+ let input = format ! ( "theme={json_theme}" ) ;
484
+ command. arg ( "--input" ) . arg ( input) ;
440
485
441
486
// Pass in the font path if specified
442
487
if let Some ( font_path) = & self . typst_font_path {
@@ -578,6 +623,7 @@ impl Default for OgImageGenerator {
578
623
typst_binary_path : PathBuf :: from ( "typst" ) ,
579
624
typst_font_path : None ,
580
625
oxipng_binary_path : PathBuf :: from ( "oxipng" ) ,
626
+ theme : Theme :: default ( ) ,
581
627
}
582
628
}
583
629
}
@@ -752,9 +798,10 @@ mod tests {
752
798
}
753
799
}
754
800
755
- async fn generate_image ( data : OgImageData < ' _ > ) -> Option < Vec < u8 > > {
756
- let generator =
757
- OgImageGenerator :: from_environment ( ) . expect ( "Failed to create OgImageGenerator" ) ;
801
+ async fn generate_image ( data : OgImageData < ' _ > , theme : Theme ) -> Option < Vec < u8 > > {
802
+ let generator = OgImageGenerator :: from_environment ( )
803
+ . expect ( "Failed to create OgImageGenerator" )
804
+ . with_theme ( theme) ;
758
805
759
806
let temp_file = generator
760
807
. generate ( data)
@@ -769,7 +816,7 @@ mod tests {
769
816
let _guard = init_tracing ( ) ;
770
817
let data = create_simple_test_data ( ) ;
771
818
772
- if let Some ( image_data) = generate_image ( data) . await {
819
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
773
820
insta:: assert_binary_snapshot!( "generated_og_image.png" , image_data) ;
774
821
}
775
822
}
@@ -784,7 +831,7 @@ mod tests {
784
831
let authors = create_overflow_authors ( & server_url) ;
785
832
let data = create_overflow_test_data ( & authors) ;
786
833
787
- if let Some ( image_data) = generate_image ( data) . await {
834
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
788
835
insta:: assert_binary_snapshot!( "generated_og_image_overflow.png" , image_data) ;
789
836
}
790
837
}
@@ -794,7 +841,7 @@ mod tests {
794
841
let _guard = init_tracing ( ) ;
795
842
let data = create_minimal_test_data ( ) ;
796
843
797
- if let Some ( image_data) = generate_image ( data) . await {
844
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
798
845
insta:: assert_binary_snapshot!( "generated_og_image_minimal.png" , image_data) ;
799
846
}
800
847
}
@@ -809,7 +856,7 @@ mod tests {
809
856
let authors = create_escaping_authors ( & server_url) ;
810
857
let data = create_escaping_test_data ( & authors) ;
811
858
812
- if let Some ( image_data) = generate_image ( data) . await {
859
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
813
860
insta:: assert_binary_snapshot!( "generated_og_image_escaping.png" , image_data) ;
814
861
}
815
862
}
@@ -838,7 +885,7 @@ mod tests {
838
885
releases : 1 ,
839
886
} ;
840
887
841
- if let Some ( image_data) = generate_image ( data) . await {
888
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
842
889
insta:: assert_binary_snapshot!( "404-avatar.png" , image_data) ;
843
890
}
844
891
}
@@ -866,8 +913,18 @@ mod tests {
866
913
releases : 3 ,
867
914
} ;
868
915
869
- if let Some ( image_data) = generate_image ( data) . await {
916
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
870
917
insta:: assert_binary_snapshot!( "unicode-truncation.png" , image_data) ;
871
918
}
872
919
}
920
+
921
+ #[ tokio:: test]
922
+ async fn test_generate_og_image_docs_rs_theme ( ) {
923
+ let _guard = init_tracing ( ) ;
924
+ let data = create_simple_test_data ( ) ;
925
+
926
+ if let Some ( image_data) = generate_image ( data, Theme :: DocsRs ) . await {
927
+ insta:: assert_binary_snapshot!( "docs_rs_theme.png" , image_data) ;
928
+ }
929
+ }
873
930
}
0 commit comments