From 50d2501f27f88db602327f16941a5a53aded9ea6 Mon Sep 17 00:00:00 2001 From: buddsean Date: Fri, 30 Jul 2021 15:48:07 +1000 Subject: [PATCH 1/5] add test to confirm screen is black --- .../screenCurtain.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index c917ea8fc83..b0cde5f2581 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -300,6 +300,43 @@ def confirmInitWithUser(self) -> bool: return res == wx.YES +def isScreenFullyBlack() -> bool: + """ + Uses wx to check that the screen is currently fully black by taking a screen capture and checking: + - there is only one colour used in the image + - the first pixel of the screen capture is black + """ + screen = wx.ScreenDC() + screenSize = screen.GetSize() + bmp: wx.Bitmap = wx.Bitmap(screenSize[0], screenSize[1]) + mem = wx.MemoryDC(bmp) + mem.Blit(0, 0, screenSize[0], screenSize[1], screen, 0, 0) # copy screen over to bmp + del mem # Release bitmap + img: wx.Image = bmp.ConvertToImage() + # https://docs.wxwidgets.org/3.0/classwx_image.html#a7c9d557cd7ad577ed76e4337b1fd843a + hist = wx.ImageHistogram() + numberOfColours = img.ComputeHistogram(hist) + # Due to wxImageHistogram not fully supported in wxPython, the histogram is not subscriptable, + # and thus the key value cannot be accessed for colours. + # https://github.com/wxWidgets/Phoenix/issues/1991 + # This function could be improved in the future using the following logic: + # numberOfBlackPixelsKey = hist.MakeKey(255, 255, 255) + # numberOfBlackPixels = hist[numberOfBlackPixelsKey] + # numberOfDisplayPixels = screenSize[0] * screenSize[1] + # log.debug(f"""Screen Capture: + # - number of colours: {numberOfColours} + # - number of black pixels: {numberOfBlackPixels} + # - number of total pixels: {numberOfDisplayPixels} + # """) + # return numberOfColours == 1 and numberOfBlackPixels == numberOfDisplayPixels + firstPixelIsBlack = img.GetRed(0, 0) == 0 and img.GetBlue(0, 0) == 0 and img.GetGreen(0, 0) == 0 + return numberOfColours == 1 and firstPixelIsBlack + + +class ScreenCurtainFail(RuntimeError): + pass + + class ScreenCurtainProvider(providerBase.VisionEnhancementProvider): _settings = ScreenCurtainSettings() @@ -332,7 +369,13 @@ def __init__(self): try: Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) Magnification.MagShowSystemCursor(False) + if not isScreenFullyBlack(): + raise ScreenCurtainFail("Screen curtain not activated properly") except Exception as e: + try: + Magnification.MagShowSystemCursor(True) + except Exception: + log.error(f"Could not restore cursor after screen curtain failure.", exc_info=True) Magnification.MagUninitialize() raise e if self.getSettings().playToggleSounds: From 5f756a4b3fc4ae2d42bd24551563f14cd0a9d073 Mon Sep 17 00:00:00 2001 From: buddsean Date: Fri, 30 Jul 2021 15:57:57 +1000 Subject: [PATCH 2/5] rename error --- source/visionEnhancementProviders/screenCurtain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index b0cde5f2581..6942f26f3b3 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -333,7 +333,7 @@ def isScreenFullyBlack() -> bool: return numberOfColours == 1 and firstPixelIsBlack -class ScreenCurtainFail(RuntimeError): +class ScreenCurtainInitializationError(RuntimeError): pass @@ -370,7 +370,7 @@ def __init__(self): Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) Magnification.MagShowSystemCursor(False) if not isScreenFullyBlack(): - raise ScreenCurtainFail("Screen curtain not activated properly") + raise ScreenCurtainInitializationError("Screen curtain not activated properly") except Exception as e: try: Magnification.MagShowSystemCursor(True) From f5e5c05be533d9b1a635ec44e1031a3967cd4f79 Mon Sep 17 00:00:00 2001 From: buddsean Date: Mon, 2 Aug 2021 13:32:05 +1000 Subject: [PATCH 3/5] add notes on timing --- source/visionEnhancementProviders/screenCurtain.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 6942f26f3b3..7e3e5de7491 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -305,6 +305,9 @@ def isScreenFullyBlack() -> bool: Uses wx to check that the screen is currently fully black by taking a screen capture and checking: - there is only one colour used in the image - the first pixel of the screen capture is black + + Having to iterate over a large bitmap image in python is slow (~4s depending on machine, number of pixels). + Using this method to calculate a histogram in native code takes ~0.4s comparably. """ screen = wx.ScreenDC() screenSize = screen.GetSize() From 04319c2c66745e59fbfd9478570c7658f0d40fad Mon Sep 17 00:00:00 2001 From: buddsean Date: Mon, 2 Aug 2021 14:13:19 +1000 Subject: [PATCH 4/5] improve documentation --- source/visionEnhancementProviders/screenCurtain.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 7e3e5de7491..159acfcd56d 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -306,8 +306,10 @@ def isScreenFullyBlack() -> bool: - there is only one colour used in the image - the first pixel of the screen capture is black - Having to iterate over a large bitmap image in python is slow (~4s depending on machine, number of pixels). - Using this method to calculate a histogram in native code takes ~0.4s comparably. + Iterating over a large bitmap image in python is slow (~4s depending on machine, number of pixels). + Due to no native support for calculating the number of black pixels, the screenBitmap module is avoided. + wxWidgets has native support for calculating the usage of colours within an image. + Using this method to calculate a colour usage histogram in native code takes ~0.4s comparably. """ screen = wx.ScreenDC() screenSize = screen.GetSize() @@ -319,7 +321,7 @@ def isScreenFullyBlack() -> bool: # https://docs.wxwidgets.org/3.0/classwx_image.html#a7c9d557cd7ad577ed76e4337b1fd843a hist = wx.ImageHistogram() numberOfColours = img.ComputeHistogram(hist) - # Due to wxImageHistogram not fully supported in wxPython, the histogram is not subscriptable, + # Due to wxImageHistogram not being fully supported in wxPython, the histogram is not subscriptable, # and thus the key value cannot be accessed for colours. # https://github.com/wxWidgets/Phoenix/issues/1991 # This function could be improved in the future using the following logic: From 8b2f4169be7ebd5b7303299833e3b4732d5d0216 Mon Sep 17 00:00:00 2001 From: buddsean Date: Tue, 3 Aug 2021 13:12:43 +1000 Subject: [PATCH 5/5] further improve documentation --- .../screenCurtain.py | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 159acfcd56d..93defaae7d7 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-2019 NV Access Limited, Babbage B.V., Leonard de Ruijter +# Copyright (C) 2018-2021 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) @@ -302,14 +302,25 @@ def confirmInitWithUser(self) -> bool: def isScreenFullyBlack() -> bool: """ - Uses wx to check that the screen is currently fully black by taking a screen capture and checking: + Takes a screen capture of all displays, through wxPython, which uses the winGDI API and checks: - there is only one colour used in the image - the first pixel of the screen capture is black - + """ + """ Iterating over a large bitmap image in python is slow (~4s depending on machine, number of pixels). - Due to no native support for calculating the number of black pixels, the screenBitmap module is avoided. + Due to no native support for calculating the number of black pixels, NVDA's screenBitmap module is avoided. wxWidgets has native support for calculating the usage of colours within an image. Using this method to calculate a colour usage histogram in native code takes ~0.4s comparably. + + An example of iterating over a screen bitmap, using NVDA's screenBitmap module. + from screenBitmap import ScreenBitmap + from itertools import chain + from winGDI import RGBQUAD + screenCapture = ScreenBitmap(screenSize[0], screenSize[1]).captureImage(0, 0, screenSize[0], screenSize[1]) + def _isPixelBlack(pixel: RGBQUAD) -> bool: + return pixel.rgbRed == 0 and pixel.rgbGreen == 0 and pixel.rgbBlue == 0 + numNonBlackPixels = len(list(filter(lambda p: not _isPixelBlack(p), chain.from_iterable(screenCapture)))) + return numNonBlackPixels == 0 """ screen = wx.ScreenDC() screenSize = screen.GetSize() @@ -321,19 +332,21 @@ def isScreenFullyBlack() -> bool: # https://docs.wxwidgets.org/3.0/classwx_image.html#a7c9d557cd7ad577ed76e4337b1fd843a hist = wx.ImageHistogram() numberOfColours = img.ComputeHistogram(hist) - # Due to wxImageHistogram not being fully supported in wxPython, the histogram is not subscriptable, - # and thus the key value cannot be accessed for colours. - # https://github.com/wxWidgets/Phoenix/issues/1991 - # This function could be improved in the future using the following logic: - # numberOfBlackPixelsKey = hist.MakeKey(255, 255, 255) - # numberOfBlackPixels = hist[numberOfBlackPixelsKey] - # numberOfDisplayPixels = screenSize[0] * screenSize[1] - # log.debug(f"""Screen Capture: - # - number of colours: {numberOfColours} - # - number of black pixels: {numberOfBlackPixels} - # - number of total pixels: {numberOfDisplayPixels} - # """) - # return numberOfColours == 1 and numberOfBlackPixels == numberOfDisplayPixels + """ + Due to wxImageHistogram not being fully supported in wxPython, the histogram is not subscriptable, + and thus the key value cannot be accessed for colours. + https://github.com/wxWidgets/Phoenix/issues/1991 + This function could be improved in the future using the following logic: + numberOfBlackPixelsKey = hist.MakeKey(255, 255, 255) + numberOfBlackPixels = hist[numberOfBlackPixelsKey] + numberOfDisplayPixels = screenSize[0] * screenSize[1] + log.debug(f'''Screen Capture: + - number of colours: {numberOfColours} + - number of black pixels: {numberOfBlackPixels} + - number of total pixels: {numberOfDisplayPixels} + ''') + return numberOfColours == 1 and numberOfBlackPixels == numberOfDisplayPixels + """ firstPixelIsBlack = img.GetRed(0, 0) == 0 and img.GetBlue(0, 0) == 0 and img.GetGreen(0, 0) == 0 return numberOfColours == 1 and firstPixelIsBlack