Skip to content

Commit 69c67ed

Browse files
committed
Merge pull request godotengine#88950 from bruvzg/excap
[macOS, Windows] Add support for excluding windows from a screenshot.
2 parents 15aa18b + 9fece03 commit 69c67ed

File tree

12 files changed

+355
-75
lines changed

12 files changed

+355
-75
lines changed

doc/classes/DisplayServer.xml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,15 @@
10791079
[b]Note:[/b] On macOS, this method requires "Screen Recording" permission, if permission is not granted it will return desktop wallpaper color.
10801080
</description>
10811081
</method>
1082+
<method name="screen_get_image_rect" qualifiers="const">
1083+
<return type="Image" />
1084+
<param index="0" name="rect" type="Rect2i" />
1085+
<description>
1086+
Returns screenshot of the screen [param rect].
1087+
[b]Note:[/b] This method is implemented on macOS and Windows.
1088+
[b]Note:[/b] On macOS, this method requires "Screen Recording" permission, if permission is not granted it will return desktop wallpaper color.
1089+
</description>
1090+
</method>
10821091
<method name="screen_get_max_scale" qualifiers="const">
10831092
<return type="float" />
10841093
<description>
@@ -1913,6 +1922,9 @@
19131922
<constant name="FEATURE_WINDOW_DRAG" value="27" enum="Feature">
19141923
The display server supports initiating window drag operation on demand. See [method window_start_drag].
19151924
</constant>
1925+
<constant name="FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE" value="28" enum="Feature">
1926+
Display server supports [constant WINDOW_FLAG_EXCLUDE_FROM_CAPTURE] window flag.
1927+
</constant>
19161928
<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
19171929
Makes the mouse cursor visible if it is hidden.
19181930
</constant>
@@ -2128,7 +2140,12 @@
21282140
Window style is overridden, forcing sharp corners.
21292141
[b]Note:[/b] This flag is implemented only on Windows (11).
21302142
</constant>
2131-
<constant name="WINDOW_FLAG_MAX" value="9" enum="WindowFlags">
2143+
<constant name="WINDOW_FLAG_EXCLUDE_FROM_CAPTURE" value="9" enum="WindowFlags">
2144+
Windows is excluded from screenshots taken by [method screen_get_image], [method screen_get_image_rect], and [method screen_get_pixel].
2145+
[b]Note:[/b] This flag is implemented on macOS and Windows.
2146+
[b]Note:[/b] Setting this flag will [b]NOT[/b] prevent other apps from capturing an image, it should not be used as a security measure.
2147+
</constant>
2148+
<constant name="WINDOW_FLAG_MAX" value="10" enum="WindowFlags">
21322149
Max value of the [enum WindowFlags].
21332150
</constant>
21342151
<constant name="WINDOW_EVENT_MOUSE_ENTER" value="0" enum="WindowEvent">

doc/classes/Window.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,9 @@
586586
<member name="current_screen" type="int" setter="set_current_screen" getter="get_current_screen">
587587
The screen the window is currently on.
588588
</member>
589+
<member name="exclude_from_capture" type="bool" setter="set_flag" getter="get_flag" default="false">
590+
Windows is excluded from screenshots taken by [method DisplayServer.screen_get_image], [method DisplayServer.screen_get_image_rect], and [method DisplayServer.screen_get_pixel].
591+
</member>
589592
<member name="exclusive" type="bool" setter="set_exclusive" getter="is_exclusive" default="false">
590593
If [code]true[/code], the [Window] will be in exclusive mode. Exclusive windows are always on top of their parent and will block all input going to the parent [Window].
591594
Needs [member transient] enabled to work.
@@ -853,7 +856,12 @@
853856
[b]Note:[/b] This flag has no effect in embedded windows.
854857
[b]Note:[/b] This flag is implemented only on Windows (11).
855858
</constant>
856-
<constant name="FLAG_MAX" value="9" enum="Flags">
859+
<constant name="FLAG_EXCLUDE_FROM_CAPTURE" value="9" enum="Flags">
860+
Windows is excluded from screenshots taken by [method DisplayServer.screen_get_image], [method DisplayServer.screen_get_image_rect], and [method DisplayServer.screen_get_pixel].
861+
[b]Note:[/b] This flag is implemented on macOS and Windows.
862+
[b]Note:[/b] Setting this flag will [b]NOT[/b] prevent other apps from capturing an image, it should not be used as a security measure.
863+
</constant>
864+
<constant name="FLAG_MAX" value="10" enum="Flags">
857865
Max value of the [enum Flags].
858866
</constant>
859867
<constant name="CONTENT_SCALE_MODE_DISABLED" value="0" enum="ContentScaleMode">

platform/macos/display_server_macos.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ class DisplayServerMacOS : public DisplayServer {
129129
bool focused = false;
130130
bool is_visible = true;
131131
bool extend_to_title = false;
132+
bool hide_from_capture = false;
132133

133134
Rect2i parent_safe_rect;
134135
};
@@ -326,6 +327,7 @@ class DisplayServerMacOS : public DisplayServer {
326327
virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
327328
virtual Color screen_get_pixel(const Point2i &p_position) const override;
328329
virtual Ref<Image> screen_get_image(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
330+
virtual Ref<Image> screen_get_image_rect(const Rect2i &p_rect) const override;
329331
virtual void screen_set_keep_on(bool p_enable) override;
330332
virtual bool screen_is_kept_on() const override;
331333

platform/macos/display_server_macos.mm

Lines changed: 129 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
#include "main/main.h"
5151
#include "scene/resources/image_texture.h"
5252

53+
#include <AppKit/AppKit.h>
54+
5355
#if defined(GLES3_ENABLED)
5456
#include "drivers/gles3/rasterizer_gles3.h"
5557
#endif
@@ -1609,78 +1611,139 @@
16091611
}
16101612

16111613
Color DisplayServerMacOS::screen_get_pixel(const Point2i &p_position) const {
1612-
Point2i position = p_position;
1613-
// macOS native y-coordinate relative to _get_screens_origin() is negative,
1614-
// Godot passes a positive value.
1615-
position.y *= -1;
1616-
position += _get_screens_origin();
1614+
HashSet<CGWindowID> exclude_windows;
1615+
for (HashMap<WindowID, WindowData>::ConstIterator E = windows.begin(); E; ++E) {
1616+
if (E->value.hide_from_capture) {
1617+
exclude_windows.insert([E->value.window_object windowNumber]);
1618+
}
1619+
}
1620+
1621+
CFArrayRef on_screen_windows = CGWindowListCreate(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
1622+
CFMutableArrayRef capture_windows = CFArrayCreateMutableCopy(nullptr, 0, on_screen_windows);
1623+
for (long i = CFArrayGetCount(on_screen_windows) - 1; i >= 0; i--) {
1624+
CGWindowID window = (CGWindowID)(uintptr_t)CFArrayGetValueAtIndex(capture_windows, i);
1625+
if (exclude_windows.has(window)) {
1626+
CFArrayRemoveValueAtIndex(capture_windows, i);
1627+
}
1628+
}
1629+
1630+
Point2i position = p_position - Vector2i(1, 1);
1631+
position -= screen_get_position(0); // Note: coordinates where the screen origin is in the upper-left corner of the main display and y-axis values increase downward.
16171632
position /= screen_get_max_scale();
16181633

16191634
Color color;
1620-
for (NSScreen *screen in [NSScreen screens]) {
1621-
NSRect frame = [screen frame];
1622-
if (NSMouseInRect(NSMakePoint(position.x, position.y), frame, NO)) {
1623-
NSDictionary *screenDescription = [screen deviceDescription];
1624-
CGDirectDisplayID display_id = [[screenDescription objectForKey:@"NSScreenNumber"] unsignedIntValue];
1625-
CGImageRef image = CGDisplayCreateImageForRect(display_id, CGRectMake(position.x - frame.origin.x, frame.size.height - (position.y - frame.origin.y), 1, 1));
1626-
if (image) {
1627-
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB();
1628-
if (color_space) {
1629-
uint8_t img_data[4];
1630-
CGContextRef context = CGBitmapContextCreate(img_data, 1, 1, 8, 4, color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
1631-
if (context) {
1632-
CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image);
1633-
color = Color(img_data[0] / 255.0f, img_data[1] / 255.0f, img_data[2] / 255.0f, img_data[3] / 255.0f);
1634-
CGContextRelease(context);
1635-
}
1636-
CGColorSpaceRelease(color_space);
1637-
}
1638-
CGImageRelease(image);
1635+
CGImageRef image = CGWindowListCreateImageFromArray(CGRectMake(position.x, position.y, 1, 1), capture_windows, kCGWindowListOptionAll);
1636+
if (image) {
1637+
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB();
1638+
if (color_space) {
1639+
uint8_t img_data[4];
1640+
CGContextRef context = CGBitmapContextCreate(img_data, 1, 1, 8, 4, color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
1641+
if (context) {
1642+
CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image);
1643+
color = Color(img_data[0] / 255.0f, img_data[1] / 255.0f, img_data[2] / 255.0f, img_data[3] / 255.0f);
1644+
CGContextRelease(context);
16391645
}
1646+
CGColorSpaceRelease(color_space);
16401647
}
1648+
CGImageRelease(image);
16411649
}
16421650
return color;
16431651
}
16441652

16451653
Ref<Image> DisplayServerMacOS::screen_get_image(int p_screen) const {
16461654
ERR_FAIL_INDEX_V(p_screen, get_screen_count(), Ref<Image>());
16471655

1648-
switch (p_screen) {
1649-
case SCREEN_PRIMARY: {
1650-
p_screen = get_primary_screen();
1651-
} break;
1652-
case SCREEN_OF_MAIN_WINDOW: {
1653-
p_screen = window_get_current_screen(MAIN_WINDOW_ID);
1654-
} break;
1655-
default:
1656-
break;
1656+
HashSet<CGWindowID> exclude_windows;
1657+
for (HashMap<WindowID, WindowData>::ConstIterator E = windows.begin(); E; ++E) {
1658+
if (E->value.hide_from_capture) {
1659+
exclude_windows.insert([E->value.window_object windowNumber]);
1660+
}
16571661
}
16581662

1663+
CFArrayRef on_screen_windows = CGWindowListCreate(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
1664+
CFMutableArrayRef capture_windows = CFArrayCreateMutableCopy(nullptr, 0, on_screen_windows);
1665+
for (long i = CFArrayGetCount(on_screen_windows) - 1; i >= 0; i--) {
1666+
CGWindowID window = (CGWindowID)(uintptr_t)CFArrayGetValueAtIndex(capture_windows, i);
1667+
if (exclude_windows.has(window)) {
1668+
CFArrayRemoveValueAtIndex(capture_windows, i);
1669+
}
1670+
}
1671+
1672+
Point2i position = screen_get_position(p_screen);
1673+
position -= screen_get_position(0); // Note: coordinates where the screen origin is in the upper-left corner of the main display and y-axis values increase downward.
1674+
position /= screen_get_max_scale();
1675+
1676+
Size2i size = screen_get_size(p_screen);
1677+
size /= screen_get_max_scale();
1678+
16591679
Ref<Image> img;
1660-
NSArray *screenArray = [NSScreen screens];
1661-
if ((NSUInteger)p_screen < [screenArray count]) {
1662-
NSRect nsrect = [[screenArray objectAtIndex:p_screen] frame];
1663-
NSDictionary *screenDescription = [[screenArray objectAtIndex:p_screen] deviceDescription];
1664-
CGDirectDisplayID display_id = [[screenDescription objectForKey:@"NSScreenNumber"] unsignedIntValue];
1665-
CGImageRef image = CGDisplayCreateImageForRect(display_id, CGRectMake(0, 0, nsrect.size.width, nsrect.size.height));
1666-
if (image) {
1667-
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB();
1668-
if (color_space) {
1669-
NSUInteger width = CGImageGetWidth(image);
1670-
NSUInteger height = CGImageGetHeight(image);
1671-
1672-
Vector<uint8_t> img_data;
1673-
img_data.resize(height * width * 4);
1674-
CGContextRef context = CGBitmapContextCreate(img_data.ptrw(), width, height, 8, 4 * width, color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
1675-
if (context) {
1676-
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
1677-
img = Image::create_from_data(width, height, false, Image::FORMAT_RGBA8, img_data);
1678-
CGContextRelease(context);
1679-
}
1680-
CGColorSpaceRelease(color_space);
1680+
CGImageRef image = CGWindowListCreateImageFromArray(CGRectMake(position.x, position.y, size.width, size.height), capture_windows, kCGWindowListOptionAll);
1681+
if (image) {
1682+
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB();
1683+
if (color_space) {
1684+
NSUInteger width = CGImageGetWidth(image);
1685+
NSUInteger height = CGImageGetHeight(image);
1686+
1687+
Vector<uint8_t> img_data;
1688+
img_data.resize(height * width * 4);
1689+
CGContextRef context = CGBitmapContextCreate(img_data.ptrw(), width, height, 8, 4 * width, color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
1690+
if (context) {
1691+
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
1692+
img = Image::create_from_data(width, height, false, Image::FORMAT_RGBA8, img_data);
1693+
CGContextRelease(context);
1694+
img->resize(screen_get_size(p_screen).x, screen_get_size(p_screen).y, Image::INTERPOLATE_NEAREST);
16811695
}
1682-
CGImageRelease(image);
1696+
CGColorSpaceRelease(color_space);
16831697
}
1698+
CGImageRelease(image);
1699+
}
1700+
return img;
1701+
}
1702+
1703+
Ref<Image> DisplayServerMacOS::screen_get_image_rect(const Rect2i &p_rect) const {
1704+
HashSet<CGWindowID> exclude_windows;
1705+
for (HashMap<WindowID, WindowData>::ConstIterator E = windows.begin(); E; ++E) {
1706+
if (E->value.hide_from_capture) {
1707+
exclude_windows.insert([E->value.window_object windowNumber]);
1708+
}
1709+
}
1710+
1711+
CFArrayRef on_screen_windows = CGWindowListCreate(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
1712+
CFMutableArrayRef capture_windows = CFArrayCreateMutableCopy(nullptr, 0, on_screen_windows);
1713+
for (long i = CFArrayGetCount(on_screen_windows) - 1; i >= 0; i--) {
1714+
CGWindowID window = (CGWindowID)(uintptr_t)CFArrayGetValueAtIndex(capture_windows, i);
1715+
if (exclude_windows.has(window)) {
1716+
CFArrayRemoveValueAtIndex(capture_windows, i);
1717+
}
1718+
}
1719+
1720+
Point2i position = p_rect.position;
1721+
position -= screen_get_position(0); // Note: coordinates where the screen origin is in the upper-left corner of the main display and y-axis values increase downward.
1722+
position /= screen_get_max_scale();
1723+
1724+
Size2i size = p_rect.size;
1725+
size /= screen_get_max_scale();
1726+
1727+
Ref<Image> img;
1728+
CGImageRef image = CGWindowListCreateImageFromArray(CGRectMake(position.x, position.y, size.width, size.height), capture_windows, kCGWindowListOptionAll);
1729+
if (image) {
1730+
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB();
1731+
if (color_space) {
1732+
NSUInteger width = CGImageGetWidth(image);
1733+
NSUInteger height = CGImageGetHeight(image);
1734+
1735+
Vector<uint8_t> img_data;
1736+
img_data.resize_zeroed(height * width * 4);
1737+
CGContextRef context = CGBitmapContextCreate(img_data.ptrw(), width, height, 8, 4 * width, color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
1738+
if (context) {
1739+
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
1740+
img = Image::create_from_data(width, height, false, Image::FORMAT_RGBA8, img_data);
1741+
CGContextRelease(context);
1742+
img->resize(p_rect.size.x, p_rect.size.y, Image::INTERPOLATE_NEAREST);
1743+
}
1744+
CGColorSpaceRelease(color_space);
1745+
}
1746+
CGImageRelease(image);
16841747
}
16851748
return img;
16861749
}
@@ -2528,6 +2591,14 @@
25282591
NSWindow *w = wd.window_object;
25292592
w.excludedFromWindowsMenu = wd.is_popup || wd.no_focus;
25302593
} break;
2594+
case WINDOW_FLAG_EXCLUDE_FROM_CAPTURE: {
2595+
if (p_enabled) {
2596+
[wd.window_object setSharingType:NSWindowSharingNone];
2597+
} else {
2598+
[wd.window_object setSharingType:NSWindowSharingReadWrite];
2599+
}
2600+
wd.hide_from_capture = p_enabled;
2601+
} break;
25312602
case WINDOW_FLAG_MOUSE_PASSTHROUGH: {
25322603
wd.mpass = p_enabled;
25332604
} break;
@@ -2573,6 +2644,9 @@
25732644
case WINDOW_FLAG_NO_FOCUS: {
25742645
return wd.no_focus;
25752646
} break;
2647+
case WINDOW_FLAG_EXCLUDE_FROM_CAPTURE: {
2648+
return wd.hide_from_capture;
2649+
} break;
25762650
case WINDOW_FLAG_MOUSE_PASSTHROUGH: {
25772651
return wd.mpass;
25782652
} break;

0 commit comments

Comments
 (0)