@@ -10,7 +10,7 @@ use bytes::Bytes;
10
10
use crates_io_env_vars:: var;
11
11
use serde:: Serialize ;
12
12
use std:: collections:: HashMap ;
13
- use std:: path:: PathBuf ;
13
+ use std:: path:: { Path , PathBuf } ;
14
14
use tempfile:: NamedTempFile ;
15
15
use tokio:: fs;
16
16
use tokio:: process:: Command ;
@@ -76,6 +76,7 @@ impl<'a> OgImageAuthorData<'a> {
76
76
pub struct OgImageGenerator {
77
77
typst_binary_path : PathBuf ,
78
78
typst_font_path : Option < PathBuf > ,
79
+ oxipng_binary_path : PathBuf ,
79
80
}
80
81
81
82
impl OgImageGenerator {
@@ -93,6 +94,7 @@ impl OgImageGenerator {
93
94
Self {
94
95
typst_binary_path,
95
96
typst_font_path : None ,
97
+ oxipng_binary_path : PathBuf :: from ( "oxipng" ) ,
96
98
}
97
99
}
98
100
@@ -113,6 +115,7 @@ impl OgImageGenerator {
113
115
pub fn from_environment ( ) -> Result < Self , OgImageError > {
114
116
let typst_path = var ( "TYPST_PATH" ) . map_err ( OgImageError :: EnvVarError ) ?;
115
117
let font_path = var ( "TYPST_FONT_PATH" ) . map_err ( OgImageError :: EnvVarError ) ?;
118
+ let oxipng_path = var ( "OXIPNG_PATH" ) . map_err ( OgImageError :: EnvVarError ) ?;
116
119
117
120
let mut generator = if let Some ( ref path) = typst_path {
118
121
debug ! ( typst_path = %path, "Using custom Typst binary path from environment" ) ;
@@ -132,6 +135,15 @@ impl OgImageGenerator {
132
135
debug ! ( "No custom font path specified, using Typst default font discovery" ) ;
133
136
}
134
137
138
+ let oxipng_binary_path = if let Some ( ref path) = oxipng_path {
139
+ debug ! ( oxipng_path = %path, "Using custom oxipng binary path from environment" ) ;
140
+ PathBuf :: from ( path)
141
+ } else {
142
+ debug ! ( "OXIPNG_PATH not set, defaulting to 'oxipng' in PATH" ) ;
143
+ PathBuf :: from ( "oxipng" )
144
+ } ;
145
+ generator. oxipng_binary_path = oxipng_binary_path;
146
+
135
147
Ok ( generator)
136
148
}
137
149
@@ -156,6 +168,25 @@ impl OgImageGenerator {
156
168
self
157
169
}
158
170
171
+ /// Sets the oxipng binary path for PNG optimization.
172
+ ///
173
+ /// This allows specifying a custom path to the oxipng binary for PNG optimization.
174
+ /// If not set, defaults to "oxipng" which assumes the binary is available in PATH.
175
+ ///
176
+ /// # Examples
177
+ ///
178
+ /// ```
179
+ /// use std::path::PathBuf;
180
+ /// use crates_io_og_image::OgImageGenerator;
181
+ ///
182
+ /// let generator = OgImageGenerator::default()
183
+ /// .with_oxipng_path(PathBuf::from("/usr/local/bin/oxipng"));
184
+ /// ```
185
+ pub fn with_oxipng_path ( mut self , oxipng_path : PathBuf ) -> Self {
186
+ self . oxipng_binary_path = oxipng_path;
187
+ self
188
+ }
189
+
159
190
/// Processes avatars by downloading URLs and copying assets to the assets directory.
160
191
///
161
192
/// This method handles both asset-based avatars (which are copied from the bundled assets)
@@ -165,7 +196,7 @@ impl OgImageGenerator {
165
196
async fn process_avatars < ' a > (
166
197
& self ,
167
198
data : & ' a OgImageData < ' _ > ,
168
- assets_dir : & std :: path :: Path ,
199
+ assets_dir : & Path ,
169
200
) -> Result < HashMap < & ' a str , String > , OgImageError > {
170
201
let mut avatar_map = HashMap :: new ( ) ;
171
202
@@ -408,20 +439,93 @@ impl OgImageGenerator {
408
439
output_size_bytes, "Typst compilation completed successfully"
409
440
) ;
410
441
442
+ // After successful Typst compilation, optimize the PNG
443
+ self . optimize_png ( output_file. path ( ) ) . await ;
444
+
411
445
let duration = start_time. elapsed ( ) ;
412
446
info ! (
413
447
duration_ms = duration. as_millis( ) ,
414
448
output_size_bytes, "OpenGraph image generation completed successfully"
415
449
) ;
416
450
Ok ( output_file)
417
451
}
452
+
453
+ /// Optimizes a PNG file using oxipng.
454
+ ///
455
+ /// This method attempts to reduce the file size of a PNG using lossless compression.
456
+ /// All errors are handled internally and logged as warnings. The method never fails
457
+ /// to ensure PNG optimization is truly optional.
458
+ async fn optimize_png ( & self , png_file : & Path ) {
459
+ debug ! (
460
+ input_file = %png_file. display( ) ,
461
+ oxipng_path = %self . oxipng_binary_path. display( ) ,
462
+ "Starting PNG optimization"
463
+ ) ;
464
+
465
+ let start_time = std:: time:: Instant :: now ( ) ;
466
+
467
+ let mut command = Command :: new ( & self . oxipng_binary_path ) ;
468
+
469
+ // Default optimization level for speed/compression balance
470
+ command. arg ( "--opt" ) . arg ( "2" ) ;
471
+
472
+ // Remove safe-to-remove metadata
473
+ command. arg ( "--strip" ) . arg ( "safe" ) ;
474
+
475
+ // Overwrite the input PNG file
476
+ command. arg ( png_file) ;
477
+
478
+ // Clear environment variables to avoid leaking sensitive data
479
+ command. env_clear ( ) ;
480
+
481
+ // Preserve environment variables needed for running oxipng
482
+ if let Ok ( path) = std:: env:: var ( "PATH" ) {
483
+ command. env ( "PATH" , path) ;
484
+ }
485
+
486
+ let output = command. output ( ) . await ;
487
+ let duration = start_time. elapsed ( ) ;
488
+
489
+ match output {
490
+ Ok ( output) if output. status . success ( ) => {
491
+ debug ! (
492
+ duration_ms = duration. as_millis( ) ,
493
+ "PNG optimization completed successfully"
494
+ ) ;
495
+ }
496
+ Ok ( output) => {
497
+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
498
+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
499
+ warn ! (
500
+ exit_code = ?output. status. code( ) ,
501
+ stderr = %stderr,
502
+ stdout = %stdout,
503
+ duration_ms = duration. as_millis( ) ,
504
+ input_file = %png_file. display( ) ,
505
+ "PNG optimization failed, continuing with unoptimized image"
506
+ ) ;
507
+ }
508
+ Err ( err) => {
509
+ warn ! (
510
+ error = %err,
511
+ input_file = %png_file. display( ) ,
512
+ oxipng_path = %self . oxipng_binary_path. display( ) ,
513
+ "Failed to execute oxipng, continuing with unoptimized image"
514
+ ) ;
515
+ }
516
+ }
517
+ }
418
518
}
419
519
420
520
impl Default for OgImageGenerator {
421
521
/// Creates a default `OgImageGenerator` that assumes the Typst binary is available
422
522
/// as "typst" in the system PATH.
423
523
fn default ( ) -> Self {
424
- Self :: new ( PathBuf :: from ( "typst" ) )
524
+ Self {
525
+ typst_binary_path : PathBuf :: from ( "typst" ) ,
526
+ typst_font_path : None ,
527
+ oxipng_binary_path : PathBuf :: from ( "oxipng" ) ,
528
+ }
425
529
}
426
530
}
427
531
0 commit comments