@@ -203,15 +203,66 @@ pub struct CaptureWindowWithThumbnail {
203203 pub app_icon : Option < String > ,
204204}
205205
206+ #[ cfg( any( target_os = "macos" , windows) ) ]
207+ const THUMBNAIL_WIDTH : u32 = 320 ;
208+ #[ cfg( any( target_os = "macos" , windows) ) ]
209+ const THUMBNAIL_HEIGHT : u32 = 180 ;
210+
211+ #[ cfg( any( target_os = "macos" , windows) ) ]
212+ fn normalize_thumbnail_dimensions ( image : & image:: RgbaImage ) -> image:: RgbaImage {
213+ let width = image. width ( ) ;
214+ let height = image. height ( ) ;
215+
216+ if width == THUMBNAIL_WIDTH && height == THUMBNAIL_HEIGHT {
217+ return image. clone ( ) ;
218+ }
219+
220+ if width == 0 || height == 0 {
221+ return image:: RgbaImage :: from_pixel (
222+ THUMBNAIL_WIDTH ,
223+ THUMBNAIL_HEIGHT ,
224+ image:: Rgba ( [ 0 , 0 , 0 , 0 ] ) ,
225+ ) ;
226+ }
227+
228+ let scale = ( THUMBNAIL_WIDTH as f32 / width as f32 )
229+ . min ( THUMBNAIL_HEIGHT as f32 / height as f32 )
230+ . max ( f32:: MIN_POSITIVE ) ;
231+
232+ let scaled_width = ( width as f32 * scale)
233+ . round ( )
234+ . clamp ( 1.0 , THUMBNAIL_WIDTH as f32 ) as u32 ;
235+ let scaled_height = ( height as f32 * scale)
236+ . round ( )
237+ . clamp ( 1.0 , THUMBNAIL_HEIGHT as f32 ) as u32 ;
238+
239+ let resized = image:: imageops:: resize (
240+ image,
241+ scaled_width. max ( 1 ) ,
242+ scaled_height. max ( 1 ) ,
243+ image:: imageops:: FilterType :: Lanczos3 ,
244+ ) ;
245+
246+ let mut canvas =
247+ image:: RgbaImage :: from_pixel ( THUMBNAIL_WIDTH , THUMBNAIL_HEIGHT , image:: Rgba ( [ 0 , 0 , 0 , 0 ] ) ) ;
248+
249+ let offset_x = ( THUMBNAIL_WIDTH - scaled_width) / 2 ;
250+ let offset_y = ( THUMBNAIL_HEIGHT - scaled_height) / 2 ;
251+
252+ image:: imageops:: overlay ( & mut canvas, & resized, offset_x as i64 , offset_y as i64 ) ;
253+
254+ canvas
255+ }
256+
206257#[ cfg( target_os = "macos" ) ]
207258async fn capture_thumbnail_from_filter ( filter : & cidre:: sc:: ContentFilter ) -> Option < String > {
208259 use cidre:: { cv, sc} ;
209260 use image:: { ImageEncoder , RgbaImage , codecs:: png:: PngEncoder } ;
210261 use std:: { io:: Cursor , slice} ;
211262
212263 let mut config = sc:: StreamCfg :: new ( ) ;
213- config. set_width ( 200 ) ;
214- config. set_height ( 112 ) ;
264+ config. set_width ( THUMBNAIL_WIDTH as usize ) ;
265+ config. set_height ( THUMBNAIL_HEIGHT as usize ) ;
215266 config. set_shows_cursor ( false ) ;
216267
217268 let sample_buf =
@@ -271,12 +322,13 @@ async fn capture_thumbnail_from_filter(filter: &cidre::sc::ContentFilter) -> Opt
271322 warn ! ( "Failed to construct RGBA image for thumbnail" ) ;
272323 return None ;
273324 } ;
325+ let thumbnail = normalize_thumbnail_dimensions ( & img) ;
274326 let mut png_data = Cursor :: new ( Vec :: new ( ) ) ;
275327 let encoder = PngEncoder :: new ( & mut png_data) ;
276328 if let Err ( err) = encoder. write_image (
277- img . as_raw ( ) ,
278- img . width ( ) ,
279- img . height ( ) ,
329+ thumbnail . as_raw ( ) ,
330+ thumbnail . width ( ) ,
331+ thumbnail . height ( ) ,
280332 image:: ColorType :: Rgba8 . into ( ) ,
281333 ) {
282334 warn ! ( error = ?err, "Failed to encode thumbnail as PNG" ) ;
@@ -583,7 +635,7 @@ async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option<St
583635 use scap_direct3d:: { Capturer , Settings } ;
584636 use std:: io:: Cursor ;
585637
586- let item = display. raw_handle ( ) . get_capture_item ( ) . ok ( ) ?;
638+ let item = display. raw_handle ( ) . try_as_capture_item ( ) . ok ( ) ?;
587639
588640 let ( tx, rx) = std:: sync:: mpsc:: channel ( ) ;
589641
@@ -593,7 +645,7 @@ async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option<St
593645 ..Default :: default ( )
594646 } ;
595647
596- let capturer = Capturer :: new (
648+ let mut capturer = Capturer :: new (
597649 item,
598650 settings. clone ( ) ,
599651 move |frame| {
@@ -617,9 +669,6 @@ async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option<St
617669 return None ;
618670 }
619671
620- let target_width = 320u32 ;
621- let target_height = ( height as f32 * ( target_width as f32 / width as f32 ) ) as u32 ;
622-
623672 let frame_buffer = frame. as_buffer ( ) . ok ( ) ?;
624673 let data = frame_buffer. data ( ) ;
625674 let stride = frame_buffer. stride ( ) as usize ;
@@ -638,21 +687,16 @@ async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option<St
638687 }
639688
640689 let img = image:: RgbaImage :: from_raw ( width, height, rgba_data) ?;
641- let resized = image:: imageops:: resize (
642- & img,
643- target_width,
644- target_height,
645- image:: imageops:: FilterType :: Lanczos3 ,
646- ) ;
690+ let thumbnail = normalize_thumbnail_dimensions ( & img) ;
647691
648692 let mut png_data = Cursor :: new ( Vec :: new ( ) ) ;
649693 let encoder = PngEncoder :: new ( & mut png_data) ;
650694 encoder
651695 . write_image (
652- resized . as_raw ( ) ,
653- target_width ,
654- target_height ,
655- ColorType :: Rgba8 ,
696+ thumbnail . as_raw ( ) ,
697+ thumbnail . width ( ) ,
698+ thumbnail . height ( ) ,
699+ ColorType :: Rgba8 . into ( ) ,
656700 )
657701 . ok ( ) ?;
658702
@@ -675,7 +719,7 @@ async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option<Strin
675719 use scap_direct3d:: { Capturer , Settings } ;
676720 use std:: io:: Cursor ;
677721
678- let item = window. raw_handle ( ) . get_capture_item ( ) . ok ( ) ?;
722+ let item = window. raw_handle ( ) . try_as_capture_item ( ) . ok ( ) ?;
679723
680724 let ( tx, rx) = std:: sync:: mpsc:: channel ( ) ;
681725
@@ -685,7 +729,7 @@ async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option<Strin
685729 ..Default :: default ( )
686730 } ;
687731
688- let capturer = Capturer :: new (
732+ let mut capturer = Capturer :: new (
689733 item,
690734 settings. clone ( ) ,
691735 move |frame| {
@@ -709,9 +753,6 @@ async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option<Strin
709753 return None ;
710754 }
711755
712- let target_width = 200u32 ;
713- let target_height = ( height as f32 * ( target_width as f32 / width as f32 ) ) as u32 ;
714-
715756 let frame_buffer = frame. as_buffer ( ) . ok ( ) ?;
716757 let data = frame_buffer. data ( ) ;
717758 let stride = frame_buffer. stride ( ) as usize ;
@@ -730,21 +771,16 @@ async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option<Strin
730771 }
731772
732773 let img = image:: RgbaImage :: from_raw ( width, height, rgba_data) ?;
733- let resized = image:: imageops:: resize (
734- & img,
735- target_width,
736- target_height,
737- image:: imageops:: FilterType :: Lanczos3 ,
738- ) ;
774+ let thumbnail = normalize_thumbnail_dimensions ( & img) ;
739775
740776 let mut png_data = Cursor :: new ( Vec :: new ( ) ) ;
741777 let encoder = PngEncoder :: new ( & mut png_data) ;
742778 encoder
743779 . write_image (
744- resized . as_raw ( ) ,
745- target_width ,
746- target_height ,
747- ColorType :: Rgba8 ,
780+ thumbnail . as_raw ( ) ,
781+ thumbnail . width ( ) ,
782+ thumbnail . height ( ) ,
783+ ColorType :: Rgba8 . into ( ) ,
748784 )
749785 . ok ( ) ?;
750786
@@ -797,20 +833,16 @@ fn collect_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithThumbnail>,
797833 let mut results = Vec :: new ( ) ;
798834 for ( capture_window, window) in windows {
799835 let thumbnail = capture_window_thumbnail ( & window) . await ;
800- let app_icon = window
801- . app_icon ( )
802- . and_then ( |bytes| {
803- if bytes. is_empty ( ) {
804- None
805- } else {
806- Some (
807- base64:: Engine :: encode (
808- & base64:: engine:: general_purpose:: STANDARD ,
809- bytes,
810- ) ,
811- )
812- }
813- } ) ;
836+ let app_icon = window. app_icon ( ) . and_then ( |bytes| {
837+ if bytes. is_empty ( ) {
838+ None
839+ } else {
840+ Some ( base64:: Engine :: encode (
841+ & base64:: engine:: general_purpose:: STANDARD ,
842+ bytes,
843+ ) )
844+ }
845+ } ) ;
814846
815847 if thumbnail. is_none ( ) {
816848 warn ! (
0 commit comments