Teach Emacs on MS-Windows how to export frame screenshots

* src/w32image.c (w32_gdip_export_frame): New function.
(gdiplus_init): Load 'GdipCreateBitmapFromHBITMAP' from GDI+ DLL.
* src/w32fns.c (Fw32_export_frame): New primitive.

* etc/NEWS: Announce the new primitive.
This commit is contained in:
Eli Zaretskii
2026-03-01 15:08:51 +02:00
parent 1976223c56
commit a755d7fcf0
4 changed files with 132 additions and 1 deletions

View File

@@ -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

View File

@@ -91,6 +91,9 @@ typedef enum _WTS_VIRTUAL_CLASS {
#include <dlgs.h>
#include <imm.h>
#include <windowsx.h>
#include <gdiplus.h>
#include "w32gdiplus.h"
/*
Internal/undocumented constants for Windows Dark mode.
@@ -11453,6 +11456,46 @@ w32_register_for_sleep_notifications (void)
RegisterSuspendResumeNotification_fn (&params, 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");

View File

@@ -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 *);

View File

@@ -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.