@@ -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
}
@@ -768,13 +814,14 @@ mod tests {
768
814
. is_err ( )
769
815
}
770
816
771
- async fn generate_image ( data : OgImageData < ' _ > ) -> Option < Vec < u8 > > {
817
+ async fn generate_image ( data : OgImageData < ' _ > , theme : Theme ) -> Option < Vec < u8 > > {
772
818
if skip_if_typst_unavailable ( ) {
773
819
return None ;
774
820
}
775
821
776
- let generator =
777
- OgImageGenerator :: from_environment ( ) . expect ( "Failed to create OgImageGenerator" ) ;
822
+ let generator = OgImageGenerator :: from_environment ( )
823
+ . expect ( "Failed to create OgImageGenerator" )
824
+ . with_theme ( theme) ;
778
825
779
826
let temp_file = generator
780
827
. generate ( data)
@@ -789,7 +836,7 @@ mod tests {
789
836
let _guard = init_tracing ( ) ;
790
837
let data = create_simple_test_data ( ) ;
791
838
792
- if let Some ( image_data) = generate_image ( data) . await {
839
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
793
840
insta:: assert_binary_snapshot!( "generated_og_image.png" , image_data) ;
794
841
}
795
842
}
@@ -804,7 +851,7 @@ mod tests {
804
851
let authors = create_overflow_authors ( & server_url) ;
805
852
let data = create_overflow_test_data ( & authors) ;
806
853
807
- if let Some ( image_data) = generate_image ( data) . await {
854
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
808
855
insta:: assert_binary_snapshot!( "generated_og_image_overflow.png" , image_data) ;
809
856
}
810
857
}
@@ -814,7 +861,7 @@ mod tests {
814
861
let _guard = init_tracing ( ) ;
815
862
let data = create_minimal_test_data ( ) ;
816
863
817
- if let Some ( image_data) = generate_image ( data) . await {
864
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
818
865
insta:: assert_binary_snapshot!( "generated_og_image_minimal.png" , image_data) ;
819
866
}
820
867
}
@@ -829,7 +876,7 @@ mod tests {
829
876
let authors = create_escaping_authors ( & server_url) ;
830
877
let data = create_escaping_test_data ( & authors) ;
831
878
832
- if let Some ( image_data) = generate_image ( data) . await {
879
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
833
880
insta:: assert_binary_snapshot!( "generated_og_image_escaping.png" , image_data) ;
834
881
}
835
882
}
@@ -858,7 +905,7 @@ mod tests {
858
905
releases : 1 ,
859
906
} ;
860
907
861
- if let Some ( image_data) = generate_image ( data) . await {
908
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
862
909
insta:: assert_binary_snapshot!( "404-avatar.png" , image_data) ;
863
910
}
864
911
}
@@ -886,8 +933,18 @@ mod tests {
886
933
releases : 3 ,
887
934
} ;
888
935
889
- if let Some ( image_data) = generate_image ( data) . await {
936
+ if let Some ( image_data) = generate_image ( data, Theme :: CratesIo ) . await {
890
937
insta:: assert_binary_snapshot!( "unicode-truncation.png" , image_data) ;
891
938
}
892
939
}
940
+
941
+ #[ tokio:: test]
942
+ async fn test_generate_og_image_docs_rs_theme ( ) {
943
+ let _guard = init_tracing ( ) ;
944
+ let data = create_simple_test_data ( ) ;
945
+
946
+ if let Some ( image_data) = generate_image ( data, Theme :: DocsRs ) . await {
947
+ insta:: assert_binary_snapshot!( "docs_rs_theme.png" , image_data) ;
948
+ }
949
+ }
893
950
}
0 commit comments