@@ -25,6 +25,9 @@ extern crate webkit2gtk;
2525#[ macro_use]
2626extern crate objc;
2727
28+ #[ cfg( target_os = "macos" ) ]
29+ extern crate cocoa;
30+
2831use clipboard_files;
2932
3033#[ cfg( target_os = "linux" ) ]
@@ -387,6 +390,209 @@ fn get_mac_deep_link_requests() -> Vec<String> {
387390 }
388391}
389392
393+ // Screenshot capture types
394+ #[ derive( serde:: Deserialize ) ]
395+ struct CaptureRect {
396+ x : f64 ,
397+ y : f64 ,
398+ width : f64 ,
399+ height : f64 ,
400+ }
401+
402+ #[ tauri:: command]
403+ async fn capture_page ( window : tauri:: Window , rect : Option < CaptureRect > ) -> Result < Vec < u8 > , String > {
404+ #[ cfg( target_os = "linux" ) ]
405+ {
406+ let _ = ( & window, & rect) ;
407+ return Err ( "capture_page is not implemented on Linux" . to_string ( ) ) ;
408+ }
409+
410+ #[ cfg( target_os = "macos" ) ]
411+ {
412+ let ( tx, rx) = tokio:: sync:: oneshot:: channel :: < Result < Vec < u8 > , String > > ( ) ;
413+
414+ let _ = window. with_webview ( move |webview| {
415+ unsafe {
416+ let wk_webview = webview. inner ( ) ;
417+
418+ // Create WKSnapshotConfiguration
419+ let config: * mut objc:: runtime:: Object = msg_send ! [ class!( WKSnapshotConfiguration ) , new] ;
420+
421+ // Set capture rect if provided
422+ if let Some ( r) = & rect {
423+ // CGRect layout: {origin.x, origin.y, size.width, size.height}
424+ #[ repr( C ) ]
425+ struct CGRect { x : f64 , y : f64 , w : f64 , h : f64 }
426+ unsafe impl objc:: Encode for CGRect {
427+ fn encode ( ) -> objc:: Encoding {
428+ unsafe { objc:: Encoding :: from_str ( "{CGRect={CGPoint=dd}{CGSize=dd}}" ) }
429+ }
430+ }
431+ let cg_rect = CGRect { x : r. x , y : r. y , w : r. width , h : r. height } ;
432+ let ( ) = msg_send ! [ config, setRect: cg_rect] ;
433+ }
434+
435+ // Wrap sender in Arc<Mutex<Option>> so the closure is Fn (not FnOnce)
436+ let tx = std:: sync:: Arc :: new ( std:: sync:: Mutex :: new ( Some ( tx) ) ) ;
437+
438+ let handler = block:: ConcreteBlock :: new ( move |image : cocoa:: base:: id , error : cocoa:: base:: id | {
439+ let mut guard = tx. lock ( ) . unwrap ( ) ;
440+ let tx = match guard. take ( ) {
441+ Some ( tx) => tx,
442+ None => return ,
443+ } ;
444+ if !image. is_null ( ) {
445+ // NSImage -> TIFF -> NSBitmapImageRep -> PNG
446+ let tiff_data: cocoa:: base:: id = msg_send ! [ image, TIFFRepresentation ] ;
447+ if tiff_data. is_null ( ) {
448+ let _ = tx. send ( Err ( "Failed to get TIFF representation" . to_string ( ) ) ) ;
449+ return ;
450+ }
451+ let bitmap_rep: cocoa:: base:: id = msg_send ! [
452+ class!( NSBitmapImageRep ) , imageRepWithData: tiff_data
453+ ] ;
454+ if bitmap_rep. is_null ( ) {
455+ let _ = tx. send ( Err ( "Failed to create bitmap representation" . to_string ( ) ) ) ;
456+ return ;
457+ }
458+ let empty_dict: cocoa:: base:: id = msg_send ! [ class!( NSDictionary ) , dictionary] ;
459+ // NSBitmapImageFileTypePNG = 4
460+ let png_data: cocoa:: base:: id = msg_send ! [
461+ bitmap_rep, representationUsingType: 4usize properties: empty_dict
462+ ] ;
463+ if png_data. is_null ( ) {
464+ let _ = tx. send ( Err ( "Failed to create PNG data" . to_string ( ) ) ) ;
465+ return ;
466+ }
467+ let length: usize = msg_send ! [ png_data, length] ;
468+ let bytes: * const u8 = msg_send ! [ png_data, bytes] ;
469+ let vec = std:: slice:: from_raw_parts ( bytes, length) . to_vec ( ) ;
470+ let _ = tx. send ( Ok ( vec) ) ;
471+ } else {
472+ let err_msg = if !error. is_null ( ) {
473+ let desc: cocoa:: base:: id = msg_send ! [ error, localizedDescription] ;
474+ let utf8: * const std:: os:: raw:: c_char = msg_send ! [ desc, UTF8String ] ;
475+ if !utf8. is_null ( ) {
476+ std:: ffi:: CStr :: from_ptr ( utf8) . to_string_lossy ( ) . to_string ( )
477+ } else {
478+ "Screenshot failed" . to_string ( )
479+ }
480+ } else {
481+ "Screenshot failed with unknown error" . to_string ( )
482+ } ;
483+ let _ = tx. send ( Err ( err_msg) ) ;
484+ }
485+ } ) ;
486+ let handler = handler. copy ( ) ;
487+ let completion_handler: & block:: Block < ( cocoa:: base:: id , cocoa:: base:: id ) , ( ) > = & handler;
488+
489+ let ( ) = msg_send ! [
490+ wk_webview,
491+ takeSnapshotWithConfiguration: config
492+ completionHandler: completion_handler
493+ ] ;
494+ }
495+ } ) . map_err ( |e| e. to_string ( ) ) ?;
496+
497+ return rx. await . map_err ( |_| "Capture channel closed" . to_string ( ) ) ?;
498+ }
499+
500+ #[ cfg( windows) ]
501+ {
502+ return capture_page_windows ( window, rect) ;
503+ }
504+ }
505+
506+ #[ cfg( windows) ]
507+ fn capture_page_windows ( window : tauri:: Window , rect : Option < CaptureRect > ) -> Result < Vec < u8 > , String > {
508+ use winapi:: um:: winuser:: { GetDC , ReleaseDC , GetClientRect , PrintWindow } ;
509+ use winapi:: um:: wingdi:: {
510+ CreateCompatibleDC , CreateCompatibleBitmap , SelectObject , GetDIBits ,
511+ DeleteDC , DeleteObject , BITMAPINFOHEADER , BITMAPINFO , BI_RGB , DIB_RGB_COLORS ,
512+ } ;
513+ use winapi:: shared:: windef:: { RECT , HGDIOBJ } ;
514+
515+ unsafe {
516+ let hwnd = window. hwnd ( ) . map_err ( |e| e. to_string ( ) ) ?;
517+ let hwnd = hwnd. 0 as winapi:: shared:: windef:: HWND ;
518+
519+ let mut client_rect: RECT = std:: mem:: zeroed ( ) ;
520+ GetClientRect ( hwnd, & mut client_rect) ;
521+ let full_width = client_rect. right - client_rect. left ;
522+ let full_height = client_rect. bottom - client_rect. top ;
523+
524+ if full_width <= 0 || full_height <= 0 {
525+ return Err ( "Window has zero client area" . to_string ( ) ) ;
526+ }
527+
528+ let hdc_screen = GetDC ( hwnd) ;
529+ let hdc_mem = CreateCompatibleDC ( hdc_screen) ;
530+ let hbitmap = CreateCompatibleBitmap ( hdc_screen, full_width, full_height) ;
531+ let old_bitmap = SelectObject ( hdc_mem, hbitmap as HGDIOBJ ) ;
532+
533+ // PW_CLIENTONLY=1 | PW_RENDERFULLCONTENT=2 (captures HW-accelerated content)
534+ PrintWindow ( hwnd, hdc_mem, 1 | 2 ) ;
535+
536+ let mut bmi: BITMAPINFO = std:: mem:: zeroed ( ) ;
537+ bmi. bmiHeader . biSize = std:: mem:: size_of :: < BITMAPINFOHEADER > ( ) as u32 ;
538+ bmi. bmiHeader . biWidth = full_width;
539+ bmi. bmiHeader . biHeight = -full_height; // negative = top-down
540+ bmi. bmiHeader . biPlanes = 1 ;
541+ bmi. bmiHeader . biBitCount = 32 ;
542+ bmi. bmiHeader . biCompression = BI_RGB ;
543+
544+ let mut pixels = vec ! [ 0u8 ; ( full_width * full_height * 4 ) as usize ] ;
545+ GetDIBits (
546+ hdc_mem, hbitmap, 0 , full_height as u32 ,
547+ pixels. as_mut_ptr ( ) as * mut _ ,
548+ & mut bmi, DIB_RGB_COLORS ,
549+ ) ;
550+
551+ SelectObject ( hdc_mem, old_bitmap) ;
552+ DeleteObject ( hbitmap as HGDIOBJ ) ;
553+ DeleteDC ( hdc_mem) ;
554+ ReleaseDC ( hwnd, hdc_screen) ;
555+
556+ // BGRA -> RGBA
557+ for chunk in pixels. chunks_exact_mut ( 4 ) {
558+ chunk. swap ( 0 , 2 ) ;
559+ }
560+
561+ // Extract the requested region (or full image)
562+ let ( cap_x, cap_y, cap_w, cap_h) = if let Some ( r) = & rect {
563+ let x = ( r. x as i32 ) . max ( 0 ) . min ( full_width) ;
564+ let y = ( r. y as i32 ) . max ( 0 ) . min ( full_height) ;
565+ let w = ( r. width as i32 ) . min ( full_width - x) . max ( 0 ) ;
566+ let h = ( r. height as i32 ) . min ( full_height - y) . max ( 0 ) ;
567+ ( x, y, w, h)
568+ } else {
569+ ( 0 , 0 , full_width, full_height)
570+ } ;
571+
572+ if cap_w <= 0 || cap_h <= 0 {
573+ return Err ( "Capture region is empty" . to_string ( ) ) ;
574+ }
575+
576+ let mut region_pixels = Vec :: with_capacity ( ( cap_w * cap_h * 4 ) as usize ) ;
577+ for y in cap_y..( cap_y + cap_h) {
578+ let start = ( ( y * full_width + cap_x) * 4 ) as usize ;
579+ let end = start + ( cap_w * 4 ) as usize ;
580+ region_pixels. extend_from_slice ( & pixels[ start..end] ) ;
581+ }
582+
583+ // Encode as PNG
584+ let mut png_bytes: Vec < u8 > = Vec :: new ( ) ;
585+ {
586+ let mut encoder = png:: Encoder :: new ( & mut png_bytes, cap_w as u32 , cap_h as u32 ) ;
587+ encoder. set_color ( png:: ColorType :: Rgba ) ;
588+ encoder. set_depth ( png:: BitDepth :: Eight ) ;
589+ let mut writer = encoder. write_header ( ) . map_err ( |e| e. to_string ( ) ) ?;
590+ writer. write_image_data ( & region_pixels) . map_err ( |e| e. to_string ( ) ) ?;
591+ }
592+ Ok ( png_bytes)
593+ }
594+ }
595+
390596const PHOENIX_CRED_PREFIX : & str = "phcode_" ;
391597
392598fn get_username ( ) -> String {
@@ -627,7 +833,7 @@ fn main() {
627833 put_item, get_item, get_all_items, delete_item,
628834 trust_window_aes_key, remove_trust_window_aes_key,
629835 _get_windows_drives, _rename_path, show_in_folder, move_to_trash, zoom_window,
630- _get_clipboard_files, _open_url_in_browser_win] )
836+ _get_clipboard_files, _open_url_in_browser_win, capture_page ] )
631837 . setup ( |app| {
632838 init:: init_app ( app) ;
633839 #[ cfg( target_os = "linux" ) ]
0 commit comments