Repository: nvaccess/nvda
Pull request #17894: Verify screen curtain at runtime
Author: login: SaschaCowley
Created: 2025-04-04T00:28:57Z | Merged: 2025-04-06T23:54:46Z

### Link to issue number:

Fixes #12708
Supercedes  #12701

### Summary of the issue:

There is a security risk if the screen curtain fails silently. The Magnification API used by Screen Curtain does not officially support WOW64 applications like NVDA. As this is a risk with untested Windows versions, an external check would be helpful to minimise silent failures of the screen curtain.

Notwithstanding this, as this is a security feature, it is better to check that it is working as expected, and inform the user if it is not.

### Description of user facing changes

None.

### Description of development approach

Implemented a new function , `isScreenFullyBlack`, in `NVDAHelper/local/screenCurtain.cpp`. This method:

1. Obtains a reference to the desktop window, which covers the entire virtual screen, and thus all monitors connected to the computer.
2. USES GDI to capture the screen.
3. USES GDIPlus to calculate a histogram of this screen capture.
   * While this seems like overkill, it performs significantly better on my machine than individually checking that each pixel is black, likely due to optimisations and hardware accelleration in GDIPlus.
4. Checks that, for each channel of the histogram, the value at 0 is the area of the screen.

Call this function after applying the fullscreen colour effect and hiding the cursor in `visionEnhancementProviders.screenCurtain.ScreenCurtainProvider.__init__`. If the return is `False`, raise `RuntimeError`, which disables the screen curtain and informs the user that screen curtain failed to activate.

### Testing strategy:

Perform each of the following tests, with:
* A single monitor connected (in my case, my laptop's display)
* Multiple monitors of the same resolution connected (in my case, an external HDMI monitor connected, and projection mode set to duplicate)
* Multiple monitors with different resolutions connected (in my case, my laptop's display (1920 × 1200) and an HDMI monitor (1920 × 1080) and projection mode set to extend)
* Multiple monitors connected, but only one enabled (in my case, my laptop's display and an HDMI monitor, with projection modes "PC screen only" and "Second screen only" tested).

1. Test the screen curtain activates properly.
2. In the NVDA python console, change the black transformation matrix so it will fail to initialise the screen curtain properly.

   1. ```py
      from visionEnhancementProviders import screenCurtain
      screenCurtain.TRANSFORM_BLACK.transform[1][1] = 1  # retain the green channel
      ```

   2. ```py
      from visionEnhancementProviders import screenCurtain
      screenCurtain.TRANSFORM_BLACK.transform[3][3] = 0  # change the opacity to 0
      ```

3. After each of the above, test that the screen curtain doesn't start and a warning is provided to the user.

### Known issues with pull request:

None.

### Code Review Checklist:

- [x] Documentation:
  - Change log entry
  - User Documentation
  - Developer / Technical Documentation
  - Context sensitive help for GUI changes
- [x] Testing:
  - Unit tests
  - System (end to end) tests
  - Manual testing
- [x] UX of all users considered:
  - Speech
  - Braille
  - Low Vision
  - Different web browsers
  - Localization in other languages / culture than English
- [x] API is compatible with existing add-ons.
- [x] Security precautions taken.

<!-- Please keep the following -->
@coderabbitai summary


Linked issue #12708: Screen curtain check for success
### Is your feature request related to a problem? Please describe.

The screen curtain relies on an unreliable API for WOW64 applications like NVDA. If it fails silently on an untested version of Windows, personal or confidential information may be exposed on the display without a user knowing. 

### Describe the solution you'd like

Add a check for success when using the screen curtain.

Ensure it:
- captures all displays
- relies on a low level, reliable windows API, eg winGDI
- can be confident in the success of the screen capture (eg an empty buffer after a failed copy could look result in a black, empty bitmap)
- can be confident in the analysis of the screen capture
- is fast, as iterating over many pixels is slow in python.

### Describe alternatives you've considered

There are multiple options for implementing the check, using an API like winGDI

- Using the wxWidgets API
     - a simple and fast, but not complete API. We would need to ensure the API is reliable as well. 
- Using the NVDA screenBitmap module
     - slow as it requires iterating over pixels within Python to check the image
- A custom NVDA C++ implementation
     - requires dev time but can ensure no unintended changes compared to using an intermediate API

### Additional context

See #12491

Linked issue comments: user: login: feerrenrut
created_at: 2021-09-03T04:44:53Z
body: We won't let this block the release, but we do want to keep making progress on this.
author_association: CONTRIBUTOR

Diff:
diff --git a/nvdaHelper/local/nvdaHelperLocal.def b/nvdaHelper/local/nvdaHelperLocal.def
index 5f95d8cea88..d98ff2ac89f 100644
--- a/nvdaHelper/local/nvdaHelperLocal.def
+++ b/nvdaHelper/local/nvdaHelperLocal.def
@@ -87,3 +87,4 @@ EXPORTS
 	wasSilence_init
 	wasSilence_playFor
 	wasSilence_terminate
+	isScreenFullyBlack
diff --git a/nvdaHelper/local/sconscript b/nvdaHelper/local/sconscript
index 43ba2a91520..605722e45a7 100644
--- a/nvdaHelper/local/sconscript
+++ b/nvdaHelper/local/sconscript
@@ -97,6 +97,7 @@ localLib = env.SharedLibrary(
 		"mixer.cpp",
 		"oleUtils.cpp",
 		"wasapi.cpp",
+		"screenCurtain.cpp",
 	],
 	LIBS=[
 		"advapi32.lib",
@@ -107,6 +108,8 @@ localLib = env.SharedLibrary(
 		"rpcrt4",
 		"shlwapi",
 		detoursLib,
+		"Gdi32",
+		"Gdiplus",
 	],
 )
 
diff --git a/nvdaHelper/local/screenCurtain.cpp b/nvdaHelper/local/screenCurtain.cpp
new file mode 100644
index 00000000000..040b8c45f80
--- /dev/null
+++ b/nvdaHelper/local/screenCurtain.cpp
@@ -0,0 +1,199 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+#include <string>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY
+	);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB;  // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	// Each scan line must be a multiple of 32bits long, including padding if necessary.
+	// So add 31 to push us to that boundary if padding is needed.
+	// If the scan line doesn't need any padding, adding 31 will make no difference,
+	// since (32n + 31) / 32 = n R 31.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
+
+	try {
+		// Convert the device-dependent bitmap to a device-independent bitmap.
+		// Initialise each byte to 1 as a canary.
+		auto diScreenshotBits = make_shared<char[]>(diScreenshotSize, 1);
+		bytesWritten = GetDIBits(
+			// Source device context and device-dependent bitmap
+			captureDc.get(), captureBitmap.get(),
+			// Range of scan lines to copy
+			0, (UINT)ddScreenshot.bmHeight,
+			// Destination buffer
+			diScreenshotBits.get(),
+			// Format of DIB
+			(BITMAPINFO*)&diScreenshotHeader, DIB_RGB_COLORS
+		);
+		if (bytesWritten == 0 || bytesWritten == ERROR_INVALID_PARAMETER) {
+			LOG_ERROR(L"Failed to convert device dependent bitmap to device independent bitmap. Got " << bytesWritten << L".");
+			return false;
+		}
+
+		// Create a GDI+ bitmap from the captured virtual screen, and calculate a histogram of colours.
+		auto diScreenshot = make_shared<Bitmap>((BITMAPINFO*)&diScreenshotHeader, diScreenshotBits.get());
+		sStatus = diScreenshot->GetHistogramSize(HistogramFormatARGB, &histogramSize);
+		if (sStatus != Ok) {
+			LOG_ERROR(L"Failed to calculate histogram size. Error #" << sStatus << L".");
+			return false;
+		}
+		// Allocate size for the histogram.
+		auto histR = make_shared<UINT[]>(histogramSize);
+		auto histG = std::make_shared<UINT[]>(histogramSize);
+		auto histB = std::make_shared<UINT[]>(histogramSize);
+
+		diScreenshot->GetHistogram(HistogramFormatRGB, histogramSize, histR.get(), histG.get(), histB.get(), NULL);
+		auto ssHist = make_unique<wostringstream>();
+		*ssHist << L"Histogram of virtual screen:";
+		for (UINT i = 0; i < histogramSize; i++) {
+			*ssHist << L" (" << histR[i] << L", " << histG[i] << L", " << histB[i] << L")";
+		}
+		LOG_DEBUG(ssHist->str());
+
+		// If the entire screen is black, then the only colour in the histogram must be (0, 0, 0).
+		// Since the sum of values in each channel must be the number of pixels in the image,
+		// if the screen is entirely black,
+		// the 0th entry in each of the channels must be the number of pixels in the image.
+		return histR[0] == screenArea && histG[0] == screenArea && histB[0] == screenArea;
+	}
+	catch (bad_alloc)
+	{
+		LOG_ERROR("Failed to allocate space on heap for requisite buffers.");
+		return false;
+	}
+}
diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py
index 769a0e4977d..920cb661188 100644
--- a/source/visionEnhancementProviders/screenCurtain.py
+++ b/source/visionEnhancementProviders/screenCurtain.py
@@ -1,7 +1,7 @@
 # A part of NonVisual Desktop Access (NVDA)
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
-# Copyright (C) 2018-2023 NV Access Limited, Babbage B.V., Leonard de Ruijter
+# Copyright (C) 2018-2025 NV Access Limited, Babbage B.V., Leonard de Ruijter
 
 """Screen curtain implementation based on the windows magnification API.
 The Magnification API has been marked by MS as unsupported for WOW64 applications such as NVDA. (#12491)
@@ -24,6 +24,12 @@
 from typing import Optional, Type
 import nvwave
 import globalVars
+import NVDAHelper
+
+
+isScreenFullyBlack = NVDAHelper.localLib.isScreenFullyBlack
+isScreenFullyBlack.argtypes = ()
+isScreenFullyBlack.restype = BOOL
 
 
 class MAGCOLOREFFECT(Structure):
@@ -339,6 +345,8 @@ def __init__(self):
 		try:
 			Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK)
 			Magnification.MagShowSystemCursor(False)
+			if not isScreenFullyBlack():
+				raise RuntimeError("Screen is not black.")
 		except Exception as e:
 			Magnification.MagUninitialize()
 			raise e


Reviews:
user: login: seanbudd
body: Nice work on this! just minor notes and liniting
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T01:15:06Z
user: login: SaschaCowley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T03:14:55Z
user: login: SaschaCowley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T03:24:42Z
user: login: seanbudd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T03:53:50Z
user: login: SaschaCowley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T07:50:42Z
user: login: seanbudd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T07:55:28Z
user: login: seanbudd
body: Nice work
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T07:56:23Z

Review comments:
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: ```suggestion
	bool bStatus;
```
created_at: 2025-04-04T01:09:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: ```suggestion
		SRCCOPY
	);
```
created_at: 2025-04-04T01:10:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: why 31?
created_at: 2025-04-04T01:10:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: ```suggestion
	diScreenshotHeader.biPlanes = 1;  // Can only ever be 1
	diScreenshotHeader.biBitCount = 32;  // High byte unused
	diScreenshotHeader.biCompression = BI_RGB;  // Uncompressed
	diScreenshotHeader.biSizeImage = 0;  // Unneeded as uncompressed
```
created_at: 2025-04-04T01:11:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
+
+	try {
+		// Convert the device-dependent bitmap to a device-independent bitmap.
+		auto diScreenshotBits = make_shared<char[]>(diScreenshotSize);
+		bytesWritten = GetDIBits(
+			// Source device context and device-dependent bitmap
+			captureDc.get(), captureBitmap.get(),
+			// Range of scan lines to copy
+			0, (UINT)ddScreenshot.bmHeight,
+			// Destination buffer
+			diScreenshotBits.get(),
+			// Format of DIB
+			(BITMAPINFO *)&diScreenshotHeader, DIB_RGB_COLORS);
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: ```suggestion
			(BITMAPINFO*)&diScreenshotHeader, DIB_RGB_COLORS
		);
```
created_at: 2025-04-04T01:11:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
+
+	try {
+		// Convert the device-dependent bitmap to a device-independent bitmap.
+		auto diScreenshotBits = make_shared<char[]>(diScreenshotSize);
+		bytesWritten = GetDIBits(
+			// Source device context and device-dependent bitmap
+			captureDc.get(), captureBitmap.get(),
+			// Range of scan lines to copy
+			0, (UINT)ddScreenshot.bmHeight,
+			// Destination buffer
+			diScreenshotBits.get(),
+			// Format of DIB
+			(BITMAPINFO *)&diScreenshotHeader, DIB_RGB_COLORS);
+		if (bytesWritten == 0 || bytesWritten == ERROR_INVALID_PARAMETER) {
+			LOG_ERROR(L"Failed to convert device dependent bitmap to device independent bitmap. Got " << bytesWritten << L".");
+			return false;
+		}
+
+		// Create a GDI+ bitmap from the captured virtual screen, and calculate a histogram of colours.
+		auto diScreenshot = make_shared<Bitmap>((BITMAPINFO *)&diScreenshotHeader, diScreenshotBits.get());
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: ```suggestion
		auto diScreenshot = make_shared<Bitmap>((BITMAPINFO*)&diScreenshotHeader, diScreenshotBits.get());
```
created_at: 2025-04-04T01:12:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
+
+	try {
+		// Convert the device-dependent bitmap to a device-independent bitmap.
+		auto diScreenshotBits = make_shared<char[]>(diScreenshotSize);
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: when I initially worked on this, reef suggest to set some canary bits, to ensure the screen grab converts them to 0


created_at: 2025-04-04T01:13:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
+
+	try {
+		// Convert the device-dependent bitmap to a device-independent bitmap.
+		auto diScreenshotBits = make_shared<char[]>(diScreenshotSize);
+		bytesWritten = GetDIBits(
+			// Source device context and device-dependent bitmap
+			captureDc.get(), captureBitmap.get(),
+			// Range of scan lines to copy
+			0, (UINT)ddScreenshot.bmHeight,
+			// Destination buffer
+			diScreenshotBits.get(),
+			// Format of DIB
+			(BITMAPINFO *)&diScreenshotHeader, DIB_RGB_COLORS);
+		if (bytesWritten == 0 || bytesWritten == ERROR_INVALID_PARAMETER) {
+			LOG_ERROR(L"Failed to convert device dependent bitmap to device independent bitmap. Got " << bytesWritten << L".");
+			return false;
+		}
+
+		// Create a GDI+ bitmap from the captured virtual screen, and calculate a histogram of colours.
+		auto diScreenshot = make_shared<Bitmap>((BITMAPINFO *)&diScreenshotHeader, diScreenshotBits.get());
+		sStatus = diScreenshot->GetHistogramSize(HistogramFormatARGB, &histogramSize);
+		if (sStatus != Ok) {
+			LOG_ERROR(L"Failed to calculate histogram size. Error #" << sStatus << L".");
+			return false;
+		}
+		// Allocate size for the histogram.
+		auto histR = make_shared<UINT[]>(histogramSize);
+		auto histG = std::make_shared<UINT[]>(histogramSize);
+		auto histB = std::make_shared<UINT[]>(histogramSize);
+
+		diScreenshot->GetHistogram(HistogramFormatRGB, histogramSize, histR.get(), histG.get(), histB.get(), NULL);
+
+		// If the entire screen is black, then the only colour in the histogram must be (0, 0, 0).
+		// Since the sum of values in each channel must be the number of pixels in the image,
+		// if the screen is entirely black,
+		// the 0th entry in each of the channels must be the number of pixels in the image.
+		LOG_INFO("Work out if is black.");
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: I think this log message is unnecessary - would be more useful to log the histogram values
created_at: 2025-04-04T01:14:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
+
+	try {
+		// Convert the device-dependent bitmap to a device-independent bitmap.
+		auto diScreenshotBits = make_shared<char[]>(diScreenshotSize);
+		bytesWritten = GetDIBits(
+			// Source device context and device-dependent bitmap
+			captureDc.get(), captureBitmap.get(),
+			// Range of scan lines to copy
+			0, (UINT)ddScreenshot.bmHeight,
+			// Destination buffer
+			diScreenshotBits.get(),
+			// Format of DIB
+			(BITMAPINFO *)&diScreenshotHeader, DIB_RGB_COLORS);
+		if (bytesWritten == 0 || bytesWritten == ERROR_INVALID_PARAMETER) {
+			LOG_ERROR(L"Failed to convert device dependent bitmap to device independent bitmap. Got " << bytesWritten << L".");
+			return false;
+		}
+
+		// Create a GDI+ bitmap from the captured virtual screen, and calculate a histogram of colours.
+		auto diScreenshot = make_shared<Bitmap>((BITMAPINFO *)&diScreenshotHeader, diScreenshotBits.get());
+		sStatus = diScreenshot->GetHistogramSize(HistogramFormatARGB, &histogramSize);
+		if (sStatus != Ok) {
+			LOG_ERROR(L"Failed to calculate histogram size. Error #" << sStatus << L".");
+			return false;
+		}
+		// Allocate size for the histogram.
+		auto histR = make_shared<UINT[]>(histogramSize);
+		auto histG = std::make_shared<UINT[]>(histogramSize);
+		auto histB = std::make_shared<UINT[]>(histogramSize);
+
+		diScreenshot->GetHistogram(HistogramFormatRGB, histogramSize, histR.get(), histG.get(), histB.get(), NULL);
+
+		// If the entire screen is black, then the only colour in the histogram must be (0, 0, 0).
+		// Since the sum of values in each channel must be the number of pixels in the image,
+		// if the screen is entirely black,
+		// the 0th entry in each of the channels must be the number of pixels in the image.
+		LOG_INFO("Work out if is black.");
path: nvdaHelper/local/screenCurtain.cpp
user: login: SaschaCowley
body: Damn, that is left over from when I was logging every action to work out where I was causing a segfault. I thought I got all of them, but evidently not haha!
```suggestion
```
created_at: 2025-04-04T03:14:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
path: nvdaHelper/local/screenCurtain.cpp
user: login: SaschaCowley
body: Because each scan line has to be a multiple of 32 bits in length. If the scan line is less than 32 bits long, the end bits are padding, and their value does not matter.
Dividing ints in C gives us the quotient, so we need to make sure if we have a scanline with padding bits, we'll capture those bits. If the scan line fits perfectly into 32 bits, however, adding 31 will not tip us over into a larger value.
```suggestion
	// Each scan line must be a multiple of 32bits long, including padding if necessary.
	// So add 31 to push us to that boundary if padding is needed.
	// If the scan line doesn't need any padding, adding 31 will make no difference,
	// since (32n + 31) / 32 = n R 31.
	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
```
created_at: 2025-04-04T03:24:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
+
+	try {
+		// Convert the device-dependent bitmap to a device-independent bitmap.
+		auto diScreenshotBits = make_shared<char[]>(diScreenshotSize);
+		bytesWritten = GetDIBits(
+			// Source device context and device-dependent bitmap
+			captureDc.get(), captureBitmap.get(),
+			// Range of scan lines to copy
+			0, (UINT)ddScreenshot.bmHeight,
+			// Destination buffer
+			diScreenshotBits.get(),
+			// Format of DIB
+			(BITMAPINFO *)&diScreenshotHeader, DIB_RGB_COLORS);
+		if (bytesWritten == 0 || bytesWritten == ERROR_INVALID_PARAMETER) {
+			LOG_ERROR(L"Failed to convert device dependent bitmap to device independent bitmap. Got " << bytesWritten << L".");
+			return false;
+		}
+
+		// Create a GDI+ bitmap from the captured virtual screen, and calculate a histogram of colours.
+		auto diScreenshot = make_shared<Bitmap>((BITMAPINFO *)&diScreenshotHeader, diScreenshotBits.get());
+		sStatus = diScreenshot->GetHistogramSize(HistogramFormatARGB, &histogramSize);
+		if (sStatus != Ok) {
+			LOG_ERROR(L"Failed to calculate histogram size. Error #" << sStatus << L".");
+			return false;
+		}
+		// Allocate size for the histogram.
+		auto histR = make_shared<UINT[]>(histogramSize);
+		auto histG = std::make_shared<UINT[]>(histogramSize);
+		auto histB = std::make_shared<UINT[]>(histogramSize);
+
+		diScreenshot->GetHistogram(HistogramFormatRGB, histogramSize, histR.get(), histG.get(), histB.get(), NULL);
+
+		// If the entire screen is black, then the only colour in the histogram must be (0, 0, 0).
+		// Since the sum of values in each channel must be the number of pixels in the image,
+		// if the screen is entirely black,
+		// the 0th entry in each of the channels must be the number of pixels in the image.
+		LOG_INFO("Work out if is black.");
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: Can you log the histogram values for debug purposes?
created_at: 2025-04-04T03:53:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+/**
+ * A part of NonVisual Desktop Access (NVDA)
+ * This file is covered by the GNU General Public License.
+ * See the file COPYING for more details.
+ * Copyright (C) 2025 NV Access Limited
+ *
+ * Utilities for Screen Curtain.
+ *
+ * Must be linked with Gdi32.lib and Gdiplus.lib.
+*/
+#define GDIPVER 0x110   // GDIPlus 1.1, required for histograms.
+#include <windows.h>
+#include <common/log.h>
+#include <gdiplus.h>
+#include <memory>
+#include <wil/resource.h>
+
+using namespace Gdiplus;
+using namespace std;
+
+/**
+ * @brief Captures the entire virtual screen and determines if the screen is entirely black.
+ *
+ * This function captures the contents of the virtual screen, which is the bounding rectangle
+ * of all monitors on the system. It creates a device-independent bitmap (DIB) from the captured
+ * screen and calculates a histogram of colors to determine if the screen is entirely black.
+ *
+ * @return true if the screen is entirely black, false otherwise or if an error occurs.
+ *
+ * @details
+ * - The function retrieves the dimensions and origin of the virtual screen using `GetSystemMetrics`.
+ * - It creates a compatible device context and bitmap for capturing the screen.
+ * - The screen contents are copied into the compatible device context using `BitBlt`.
+ * - The captured bitmap is converted into a device-independent bitmap (DIB) using `GetDIBits`.
+ * - A GDI+ bitmap is created from the DIB, and a histogram of colors is calculated.
+ * - The function checks if the histogram indicates that the entire screen is black by verifying
+ *   that the only color present is black (0, 0, 0) and that the count of black pixels matches
+ *   the total screen area.
+ *
+ * @note
+ * - The function uses the GDI+ library for bitmap manipulation and histogram calculation.
+ * - Error handling is implemented to log and handle failures at various stages.
+ * - Per <https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen>,
+ *   We assume that the screen coordinates and dimensions fit within 16-bit integers.
+ */
+bool isScreenFullyBlack() {
+	// The virtual screen is the bounding rectangle of all of the monitors on the system.
+	int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN),
+		screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN),
+		// While the primary monitor'sStatus top left corner is at the origin,
+		// it is not necessarily at the top left of the virtual screen.
+		// Thus, the top left of the virtual screen may be negative.
+		screenOriginX = GetSystemMetrics(SM_XVIRTUALSCREEN),
+		screenOriginY = GetSystemMetrics(SM_YVIRTUALSCREEN),
+		// Screen coordinates are 16-bit integers,
+		// and since 2^16 * 2^16 = 2^32,
+		// the area of the screen is guaranteed to fit in an int on all supported platforms.
+		screenArea = screenWidth * screenHeight,
+		bytesWritten;
+	UINT histogramSize;
+	DWORD diScreenshotSize;
+	HWND desktopWnd;
+	BITMAP ddScreenshot;
+	HGDIOBJ oldObj;
+	BITMAPINFOHEADER diScreenshotHeader;
+	bool bStatus ;
+	Status sStatus;
+
+	// The desktop window covers the entire virtual screen.
+	desktopWnd = GetDesktopWindow();
+	if (desktopWnd == NULL) {
+		LOG_ERROR(L"Failed to get handle for desktop window.");
+		return false;
+	}
+	wil::unique_hdc_window desktopDc(GetDC(desktopWnd));
+	if (!desktopDc.is_valid()) {
+		LOG_ERROR(L"Failed to get device context for desktop.");
+		return false;
+	}
+	wil::unique_hdc captureDc(CreateCompatibleDC(desktopDc.get()));
+	if (!captureDc.is_valid()) {
+		LOG_ERROR("Failed to create compatible device context.");
+		return false;
+	}
+	wil::unique_hbitmap captureBitmap(CreateCompatibleBitmap(desktopDc.get(), screenWidth, screenHeight));
+	if (captureBitmap == NULL) {
+		LOG_ERROR(L"Failed to create compatible bitmap.");
+		return false;
+	}
+
+	// Set captureDc to draw to captureBitmap.
+	oldObj = SelectObject(captureDc.get(), captureBitmap.get());
+	if (oldObj == NULL) {
+		LOG_ERROR("Failed to select capture bitmap into capture device context.");
+		return false;
+	}
+
+	// Replace the contents of captureDc with those of desktopDc
+	bStatus = BitBlt(
+		// Destination device context
+		captureDc.get(),
+		// Top left of destination
+		0, 0,
+		// Size of source and destination
+		screenWidth, screenHeight,
+		// Source device context
+		desktopDc.get(),
+		// Top left of source
+		screenOriginX, screenOriginY,
+		// Raster operation to perform.
+		// In this case, replace destination with source.
+		SRCCOPY);
+	// Restore captureDC for safety.
+	SelectObject(captureDc.get(), oldObj);
+	if (!bStatus) {
+		LOG_ERROR("Failed to bit blit desktop device context to capture device context. Error #" << GetLastError());
+		return false;
+	}
+
+	// Get properties of captureBitmap
+	bytesWritten = GetObject(captureBitmap.get(), sizeof(BITMAP), &ddScreenshot);
+	if (bytesWritten == 0) {
+		LOG_ERROR("Failed to get bitmap metadata.");
+		return false;
+	}
+
+	// And use them to set the properties of the device independent bitmap.
+	diScreenshotHeader.biSize = sizeof(BITMAPINFOHEADER);
+	diScreenshotHeader.biWidth = ddScreenshot.bmWidth;
+	diScreenshotHeader.biHeight = ddScreenshot.bmHeight;
+	diScreenshotHeader.biPlanes = 1;			  // Can only ever be 1
+	diScreenshotHeader.biBitCount = 32;		  // High byte unused
+	diScreenshotHeader.biCompression = BI_RGB; // Uncompressed
+	diScreenshotHeader.biSizeImage = 0;		  // Unneeded as uncompressed
+	diScreenshotHeader.biXPelsPerMeter = 0;
+	diScreenshotHeader.biYPelsPerMeter = 0;
+	diScreenshotHeader.biClrUsed = 0;
+	diScreenshotHeader.biClrImportant = 0; // All colours are needed
+
+	// Calculate the size (in bytes) of the DIB.
+	diScreenshotSize = ((ddScreenshot.bmWidth * diScreenshotHeader.biBitCount + 31) / 32) * 4 * ddScreenshot.bmHeight;
+
+	try {
+		// Convert the device-dependent bitmap to a device-independent bitmap.
+		auto diScreenshotBits = make_shared<char[]>(diScreenshotSize);
+		bytesWritten = GetDIBits(
+			// Source device context and device-dependent bitmap
+			captureDc.get(), captureBitmap.get(),
+			// Range of scan lines to copy
+			0, (UINT)ddScreenshot.bmHeight,
+			// Destination buffer
+			diScreenshotBits.get(),
+			// Format of DIB
+			(BITMAPINFO *)&diScreenshotHeader, DIB_RGB_COLORS);
+		if (bytesWritten == 0 || bytesWritten == ERROR_INVALID_PARAMETER) {
+			LOG_ERROR(L"Failed to convert device dependent bitmap to device independent bitmap. Got " << bytesWritten << L".");
+			return false;
+		}
+
+		// Create a GDI+ bitmap from the captured virtual screen, and calculate a histogram of colours.
+		auto diScreenshot = make_shared<Bitmap>((BITMAPINFO *)&diScreenshotHeader, diScreenshotBits.get());
+		sStatus = diScreenshot->GetHistogramSize(HistogramFormatARGB, &histogramSize);
+		if (sStatus != Ok) {
+			LOG_ERROR(L"Failed to calculate histogram size. Error #" << sStatus << L".");
+			return false;
+		}
+		// Allocate size for the histogram.
+		auto histR = make_shared<UINT[]>(histogramSize);
+		auto histG = std::make_shared<UINT[]>(histogramSize);
+		auto histB = std::make_shared<UINT[]>(histogramSize);
+
+		diScreenshot->GetHistogram(HistogramFormatRGB, histogramSize, histR.get(), histG.get(), histB.get(), NULL);
+
+		// If the entire screen is black, then the only colour in the histogram must be (0, 0, 0).
+		// Since the sum of values in each channel must be the number of pixels in the image,
+		// if the screen is entirely black,
+		// the 0th entry in each of the channels must be the number of pixels in the image.
+		LOG_INFO("Work out if is black.");
path: nvdaHelper/local/screenCurtain.cpp
user: login: SaschaCowley
body: Done. I'm not sure how to debug log from c++. I have my log level set to debug, and if I use `LOG_INFO` I see the logged text, but `LOG_DEBUG` I see nothing. Nevertheless, I've committed with `LOG_DEBUG` as I don't think this should be logged at info level.
created_at: 2025-04-04T07:50:42Z
author_association: MEMBER
diff_hunk: @@ -177,6 +178,14 @@ bool isScreenFullyBlack() {
 		auto histB = std::make_shared<UINT[]>(histogramSize);
 
 		diScreenshot->GetHistogram(HistogramFormatRGB, histogramSize, histR.get(), histG.get(), histB.get(), NULL);
+		auto ssHist = make_unique<wostringstream>();
+		*ssHist << L"Histogram of virtual screen:";
+		for (UINT i = 0; i < histogramSize; i++) {
+			*ssHist << L" (" << histR[i] << L", " << histG[i] << L", " << histB[i] << L")";
+		}
+		LOG_DEBUG(ssHist->str());
+
+
path: nvdaHelper/local/screenCurtain.cpp
user: login: seanbudd
body: ```suggestion
```
created_at: 2025-04-04T07:55:28Z
author_association: MEMBER

PR comments:
user: login: hwf1324
created_at: 2025-04-04T04:34:09Z
body: Since this feature uses GDI+, I'm not sure if #17434 appears when using screen curtain?

I'm not sure if there is a strong correlation between the use of GDI+ and the occupancy of dwm.exe, but that's all I can think of.
author_association: CONTRIBUTOR
user: login: LeonarddeR
created_at: 2025-04-05T06:52:31Z
body: @SaschaCowley Have you tested this with multiple screens attached, I wonder?
author_association: COLLABORATOR
user: login: SaschaCowley
created_at: 2025-04-06T22:09:52Z
body: @LeonarddeR wrote:
> @SaschaCowley Have you tested this with multiple screens attached, I wonder?


Good catch. Yes, I did. I will update the testing methodology now.
author_association: MEMBER