diff --git a/etc/NEWS b/etc/NEWS index ef36df52ec1..4b242370713 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -4439,6 +4439,11 @@ other free systems. Transformed images are smoothed using the bilinear interpolation by means of the GDI+ library. +--- +** Emacs on MS-Windows is now capable of exporting frame screenshots to files. +The new primitive 'w32-export-frame' can be used to export a screenshot +of a specified frame to an image file in one of the supported image +formats, such as JPEG or PNG. --- ** Emacs on MS-Windows now supports the ':data' keyword for 'play-sound'. In addition to ':file FILE' for playing a sound from a file, ':data diff --git a/src/w32fns.c b/src/w32fns.c index d4ade9bf283..33f1006d398 100644 --- a/src/w32fns.c +++ b/src/w32fns.c @@ -91,6 +91,9 @@ typedef enum _WTS_VIRTUAL_CLASS { #include #include #include +#include + +#include "w32gdiplus.h" /* Internal/undocumented constants for Windows Dark mode. @@ -11453,6 +11456,46 @@ w32_register_for_sleep_notifications (void) RegisterSuspendResumeNotification_fn (¶ms, 2); } } +/*********************************************************************** + Exporting frames + ***********************************************************************/ +DEFUN ("w32-export-frame", Fw32_export_frame, Sw32_export_frame, 2, 3, 0, + doc: /* Export screenshot of FRAME to FILE as image of TYPE format. +If FRAME is nil, it defaults to the selected frame. +FRAME must be a visible GUI frame; if not, this function signals an error. +Optional arg TYPE should be either `jpeg' (default), `bmp', `png', +`gif', or `tiff'. + +Value is non-nil if FRAME was successfully exported, nil otherwise. */) + (Lisp_Object frame, Lisp_Object file, Lisp_Object type) +{ + struct frame *f = decode_live_frame (frame); + + if (NILP (type)) + type = Qjpeg; + + if (!FRAME_VISIBLE_P (f)) + error ("Frame to be exported must be visible"); + else if (!FRAME_WINDOW_P (f)) + error ("Frame to be exported must be a GUI frame"); + + /* Make sure the current matrices are up-to-date. */ + redisplay_preserve_echo_area (32); + + HWND frame_hwnd = FRAME_W32_WINDOW (f); + int result = -1; + CLSID image_type_clsid; + block_input (); + if (w32_gdiplus_startup () + && frame_hwnd != NULL + && w32_gdip_get_encoder_clsid (SSDATA (SYMBOL_NAME (type)), + &image_type_clsid) >= 0) + result = w32_gdip_export_frame (frame_hwnd, file, &image_type_clsid); + unblock_input (); + + return result >= 0 ? Qt : Qnil; +} + /*********************************************************************** Initialization @@ -11938,6 +11981,7 @@ keys when IME input is received. */); defsubr (&Sw32_set_wallpaper); defsubr (&Sw32_system_idle_time); #endif + defsubr (&Sw32_export_frame); DEFSYM (Qnot_useful, "not-useful"); DEFSYM (Qpseudo_color, "pseudo-color"); diff --git a/src/w32gdiplus.h b/src/w32gdiplus.h index b438b1a64f8..36eb7071f63 100644 --- a/src/w32gdiplus.h +++ b/src/w32gdiplus.h @@ -50,6 +50,8 @@ typedef GpStatus (WINGDIPAPI *GdipSaveImageToFile_Proc) GDIPCONST EncoderParameters *); typedef GpStatus (WINGDIPAPI *GdipImageRotateFlip_Proc) (GpImage *image, RotateFlipType rfType); +typedef GpStatus (WINGDIPAPI *GdipCreateBitmapFromHBITMAP_Proc) + (HBITMAP, HPALETTE, GpBitmap **); extern GdiplusStartup_Proc fn_GdiplusStartup; extern GdiplusShutdown_Proc fn_GdiplusShutdown; @@ -78,6 +80,7 @@ extern GdipLoadImageFromFile_Proc fn_GdipLoadImageFromFile; extern GdipGetImageThumbnail_Proc fn_GdipGetImageThumbnail; extern GdipSaveImageToFile_Proc fn_GdipSaveImageToFile; extern GdipImageRotateFlip_Proc fn_GdipImageRotateFlip; +extern GdipCreateBitmapFromHBITMAP_Proc fn_GdipCreateBitmapFromHBITMAP; # undef GdiplusStartup # undef GdiplusShutdown @@ -106,6 +109,7 @@ extern GdipImageRotateFlip_Proc fn_GdipImageRotateFlip; # undef GdipGetImageThumbnail # undef GdipSaveImageToFile # undef GdipSaveImageRotateFlip +# undef GdipCreateBitmapFromHBITMAP # define GdiplusStartup fn_GdiplusStartup # define GdiplusShutdown fn_GdiplusShutdown @@ -134,6 +138,8 @@ extern GdipImageRotateFlip_Proc fn_GdipImageRotateFlip; # define GdipGetImageThumbnail fn_GdipGetImageThumbnail # define GdipSaveImageToFile fn_GdipSaveImageToFile # define GdipImageRotateFlip fn_GdipImageRotateFlip +# define GdipCreateBitmapFromHBITMAP fn_GdipCreateBitmapFromHBITMAP #endif -int w32_gdip_get_encoder_clsid (const char *type, CLSID *clsid); +extern int w32_gdip_get_encoder_clsid (const char *, CLSID *); +extern int w32_gdip_export_frame (HWND, Lisp_Object, CLSID *); diff --git a/src/w32image.c b/src/w32image.c index 55da6909c82..36d259a07c2 100644 --- a/src/w32image.c +++ b/src/w32image.c @@ -67,6 +67,7 @@ GdipLoadImageFromFile_Proc fn_GdipLoadImageFromFile; GdipGetImageThumbnail_Proc fn_GdipGetImageThumbnail; GdipSaveImageToFile_Proc fn_GdipSaveImageToFile; GdipImageRotateFlip_Proc fn_GdipImageRotateFlip; +GdipCreateBitmapFromHBITMAP_Proc fn_GdipCreateBitmapFromHBITMAP; static bool gdiplus_init (void) @@ -195,6 +196,10 @@ gdiplus_init (void) get_proc_addr (gdiplus_lib, "GdipImageRotateFlip"); if (!fn_GdipImageRotateFlip) return false; + fn_GdipCreateBitmapFromHBITMAP = (GdipCreateBitmapFromHBITMAP_Proc) + get_proc_addr (gdiplus_lib, "GdipCreateBitmapFromHBITMAP"); + if (!fn_GdipCreateBitmapFromHBITMAP) + return false; return true; } @@ -560,6 +565,77 @@ w32_gdip_get_encoder_clsid (const char *type, CLSID *clsid) return -1; } +int +w32_gdip_export_frame (HWND hwnd, Lisp_Object file, CLSID *clsid) +{ + RECT frame_rect; + int frame_width, frame_height; + HDC hdc, capture_hdc; + int bits_per_pixel; + + /* FIXME (maybe): GetWindowRect returns the rectangle that includes + the "resize borders", which are by default invisible on Windows 10 + and later. It is unclear whether we should strive to exclude those + resize borders: it is not trivial, and OTOH these borders can be + made visible, in which case users might want them in the image. + The downside is that the image has some of the desktop around the + frame included in it. So what? */ + GetWindowRect (hwnd, &frame_rect); + frame_width = frame_rect.right - frame_rect.left; + frame_height = frame_rect.bottom - frame_rect.top; + hdc = GetWindowDC (hwnd); + bits_per_pixel = GetDeviceCaps (hdc, BITSPIXEL); + capture_hdc = CreateCompatibleDC (hdc); + + /* Capture the image. */ + BITMAPINFO capture_bmi; + memset (&capture_bmi, 0, sizeof (capture_bmi)); + BITMAPINFOHEADER *header = &capture_bmi.bmiHeader; + header->biSize = sizeof (BITMAPINFOHEADER); + header->biWidth = frame_width; + header->biHeight = -frame_height; + header->biPlanes = 1; + header->biBitCount = bits_per_pixel; + header->biCompression = BI_RGB; + DWORD *dib_bits; + HBITMAP capture_bitmap = CreateDIBSection (hdc, &capture_bmi, + DIB_PAL_COLORS, + (void **)&dib_bits, NULL, 0); + if (capture_bitmap) + { + SaveDC (capture_hdc); + SelectObject (capture_hdc, capture_bitmap); + BitBlt (capture_hdc, 0, 0, frame_width, frame_height, hdc, 0, 0, SRCCOPY); + RestoreDC (capture_hdc, -1); + } + DeleteDC (capture_hdc); + DeleteDC (hdc); + + /* Save the captured image to file in requested format. */ + GpBitmap *bitmap; + if (capture_bitmap) + { + GpStatus status = GdipCreateBitmapFromHBITMAP (capture_bitmap, NULL, + &bitmap); + if (status == Ok) + { + wchar_t file_w[MAX_PATH]; + file = ENCODE_FILE (Fexpand_file_name (Fcopy_sequence (file), + Qnil)); + unixtodos_filename (SSDATA (file)); + filename_to_utf16 (SSDATA (file), file_w); + status = GdipSaveImageToFile (bitmap, file_w, clsid, NULL); + } + GdipDisposeImage (bitmap); + DeleteObject (capture_bitmap); + + if (status == Ok) + return 0; + } + + return -1; +} + DEFUN ("w32image-create-thumbnail", Fw32image_create_thumbnail, Sw32image_create_thumbnail, 5, 5, 0, doc: /* Create a HEIGHT by WIDTH thumbnail file THUMB-FILE for image INPUT-FILE.