@@ -10,7 +10,7 @@ use bytes::Bytes;
1010use crates_io_env_vars:: var;
1111use serde:: Serialize ;
1212use std:: collections:: HashMap ;
13- use std:: path:: PathBuf ;
13+ use std:: path:: { Path , PathBuf } ;
1414use tempfile:: NamedTempFile ;
1515use tokio:: fs;
1616use tokio:: process:: Command ;
@@ -76,6 +76,7 @@ impl<'a> OgImageAuthorData<'a> {
7676pub struct OgImageGenerator {
7777 typst_binary_path : PathBuf ,
7878 typst_font_path : Option < PathBuf > ,
79+ oxipng_binary_path : PathBuf ,
7980}
8081
8182impl OgImageGenerator {
@@ -93,6 +94,7 @@ impl OgImageGenerator {
9394 Self {
9495 typst_binary_path,
9596 typst_font_path : None ,
97+ oxipng_binary_path : PathBuf :: from ( "oxipng" ) ,
9698 }
9799 }
98100
@@ -113,6 +115,7 @@ impl OgImageGenerator {
113115 pub fn from_environment ( ) -> Result < Self , OgImageError > {
114116 let typst_path = var ( "TYPST_PATH" ) . map_err ( OgImageError :: EnvVarError ) ?;
115117 let font_path = var ( "TYPST_FONT_PATH" ) . map_err ( OgImageError :: EnvVarError ) ?;
118+ let oxipng_path = var ( "OXIPNG_PATH" ) . map_err ( OgImageError :: EnvVarError ) ?;
116119
117120 let mut generator = if let Some ( ref path) = typst_path {
118121 debug ! ( typst_path = %path, "Using custom Typst binary path from environment" ) ;
@@ -132,6 +135,15 @@ impl OgImageGenerator {
132135 debug ! ( "No custom font path specified, using Typst default font discovery" ) ;
133136 }
134137
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+
135147 Ok ( generator)
136148 }
137149
@@ -156,6 +168,25 @@ impl OgImageGenerator {
156168 self
157169 }
158170
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+
159190 /// Processes avatars by downloading URLs and copying assets to the assets directory.
160191 ///
161192 /// This method handles both asset-based avatars (which are copied from the bundled assets)
@@ -165,7 +196,7 @@ impl OgImageGenerator {
165196 async fn process_avatars < ' a > (
166197 & self ,
167198 data : & ' a OgImageData < ' _ > ,
168- assets_dir : & std :: path :: Path ,
199+ assets_dir : & Path ,
169200 ) -> Result < HashMap < & ' a str , String > , OgImageError > {
170201 let mut avatar_map = HashMap :: new ( ) ;
171202
@@ -408,20 +439,93 @@ impl OgImageGenerator {
408439 output_size_bytes, "Typst compilation completed successfully"
409440 ) ;
410441
442+ // After successful Typst compilation, optimize the PNG
443+ self . optimize_png ( output_file. path ( ) ) . await ;
444+
411445 let duration = start_time. elapsed ( ) ;
412446 info ! (
413447 duration_ms = duration. as_millis( ) ,
414448 output_size_bytes, "OpenGraph image generation completed successfully"
415449 ) ;
416450 Ok ( output_file)
417451 }
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+ }
418518}
419519
420520impl Default for OgImageGenerator {
421521 /// Creates a default `OgImageGenerator` that assumes the Typst binary is available
422522 /// as "typst" in the system PATH.
423523 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+ }
425529 }
426530}
427531
0 commit comments