Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Magnifier python sample #17416

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
122 changes: 122 additions & 0 deletions source/visionEnhancementProviders/magnifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from ctypes import WinError
from ctypes.wintypes import RECT

from locationHelper import RectLTRB, RectLTWH
from logHandler import log
from vision import (
_isDebug,
)
from .screenCurtain import MAGTRANSFORM, Magnification
from winAPI import _displayTracking
from windowUtils import CustomWindow
import winUser

WindowClassName = "MagnifierWindow"
WindowTitle = "Screen Magnifier Sample"
WC_MAGNIFIER = "Magnifier"
RESTOREDWINDOWSTYLES = (
winUser.WS_SIZEBOX
| winUser.WS_SYSMENU
| winUser.WS_CLIPCHILDREN
| winUser.WS_CAPTION
| winUser.WS_MAXIMIZEBOX
)


class HostWindow(CustomWindow):
className = WindowClassName
windowName = WindowTitle
windowsStyle = RESTOREDWINDOWSTYLES
extendedWindowStyle = (
# Ensure that the window is on top of all other windows
winUser.WS_EX_TOPMOST
# A layered window ensures that L{transparentColor} will be considered transparent, when painted
| winUser.WS_EX_LAYERED
)

def __init__(self, magnificationFactor: int = 2):
super().__init__(
windowName=self.windowName,
windowStyle=self.windowsStyle,
extendedWindowStyle=self.extendedWindowStyle,
parent=None,
)
winUser.SetLayeredWindowAttributes(
self.handle,
0x00,
0xFF,
winUser.LWA_ALPHA,
)
if not winUser.user32.SetWindowPos(
self.handle,
winUser.HWND_TOPMOST,
self.targetRect.left,
self.targetRect.top,
self.targetRect.width,
int(self.targetRect.height),
winUser.SWP_NOACTIVATE | winUser.SWP_NOMOVE | winUser.SWP_NOSIZE,
):
raise WinError()
if not winUser.user32.UpdateWindow(self.handle):
raise WinError()
self.magnifierWindow = MagnifierWindow(self, magnificationFactor)

@property
def targetRect(self) -> RectLTRB:
# Top quarter of screen
return RectLTRB(
0,
0,
_displayTracking._orientationState.width,
_displayTracking._orientationState.height / 4,
)

def windowProc(self, hwnd: int, msg: int, wParam: int, lParam: int):
log.debug(f"received window proc message: {msg}")


class MagnifierWindow(CustomWindow):
className = WC_MAGNIFIER
windowName = "MagnifierWindow"
windowStyle = winUser.WS_CHILD | winUser.MS_SHOWMAGNIFIEDCURSOR | winUser.WS_VISIBLE

def __init__(self, hostWindow: HostWindow, magnificationFactor: int = 2):
self.hostWindow = hostWindow
self.magnificationFactor = magnificationFactor
if _isDebug():
log.debug("initializing NVDA Magnifier window")
super().__init__(
windowName=self.windowName,
windowStyle=self.windowStyle,
parent=hostWindow.handle,
)

magWindowRect = self.magWindowRect
if not winUser.user32.SetWindowPos(
self.handle,
winUser.HWND_TOPMOST,
magWindowRect.left,
magWindowRect.top,
magWindowRect.width,
magWindowRect.height,
winUser.SWP_NOACTIVATE | winUser.SWP_NOMOVE | winUser.SWP_NOSIZE,
):
raise WinError()
if not winUser.user32.UpdateWindow(self.handle):
raise WinError()

Magnification.MagSetWindowSource(self.handle, RECT(200, 200, 700, 700))
Magnification.MagSetWindowTransform(self.handle, MAGTRANSFORM(self.magnificationFactor))

@property
def magWindowRect(self) -> RectLTWH:
r = winUser.getClientRect(self.hostWindow.handle)
return RectLTRB(
r.left,
r.top,
r.right,
r.bottom,
).toLTWH()

def windowProc(self, hwnd: int, msg: int, wParam: int, lParam: int):
log.debug(f"received window proc message: {msg}")
89 changes: 87 additions & 2 deletions source/visionEnhancementProviders/screenCurtain.py
Original file line number Diff line number Diff line change
@@ -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-2024 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)
Expand All @@ -10,7 +10,7 @@
import os
from vision import providerBase
from ctypes import Structure, windll, c_float, POINTER, WINFUNCTYPE, WinError
from ctypes.wintypes import BOOL
from ctypes.wintypes import BOOL, FLOAT, HWND, RECT, INT
from autoSettingsUtils.driverSetting import BooleanDriverSetting
from autoSettingsUtils.autoSettings import SupportedSettingType
import wx
Expand All @@ -30,6 +30,24 @@ class MAGCOLOREFFECT(Structure):
_fields_ = (("transform", c_float * 5 * 5),)


class MAGTRANSFORM(Structure):
_fields_ = (("v", c_float * 3 * 3),)

def __init__(self, magnificationFactor: float = 1.0):
"""
https://learn.microsoft.com/en-us/windows/win32/api/magnification/ns-magnification-magtransform

:param magnificationFactor: defaults to 1.0.
The minimum value of this parameter is 1.0, and the maximum value is 4096.0.
If this value is 1.0, the screen content is not magnified and no offsets are applied.
"""
super().__init__()
assert 1.0 <= magnificationFactor <= 4096.0
self.v[0][0] = magnificationFactor
self.v[1][1] = magnificationFactor
self.v[2][2] = 1.0


# homogeneous matrix for a 4-space transformation (red, green, blue, opacity).
# https://docs.microsoft.com/en-gb/windows/win32/gdiplus/-gdiplus-using-a-color-matrix-to-transform-a-single-color-use
TRANSFORM_BLACK = MAGCOLOREFFECT() # empty transformation
Expand Down Expand Up @@ -85,6 +103,73 @@ class Magnification:
MagUninitialize = _MagUninitializeFuncType(("MagUninitialize", _magnification))
MagUninitialize.errcheck = _errCheck

_MagSetWindowSourceFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(RECT))
_MagSetWindowSourceArgTypes = ((1, "hwnd"), (1, "rect"))
MagSetWindowSource = _MagSetWindowSourceFuncType(
("MagSetWindowSource", _magnification),
_MagSetWindowSourceArgTypes,
)
MagSetWindowSource.errcheck = _errCheck

_MagGetWindowSourceFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(RECT))
_MagGetWindowSourceArgTypes = ((1, "hwnd"), (2, "rect"))
MagGetWindowSource = _MagGetWindowSourceFuncType(
("MagGetWindowSource", _magnification),
_MagGetWindowSourceArgTypes,
)
MagGetWindowSource.errcheck = _errCheck

_MagSetWindowTransformFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(MAGTRANSFORM))
_MagSetWindowTransformArgTypes = ((1, "hwnd"), (1, "transform"))
MagSetWindowTransform = _MagSetWindowTransformFuncType(
("MagSetWindowTransform", _magnification),
_MagSetWindowTransformArgTypes,
)
MagSetWindowTransform.errcheck = _errCheck

# Create transformation window
_MagGetWindowTransformFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(MAGTRANSFORM))
_MagGetWindowTransformArgTypes = ((1, "hwnd"), (2, "transform"))
MagGetWindowTransform = _MagGetWindowTransformFuncType(
("MagGetWindowTransform", _magnification),
_MagGetWindowTransformArgTypes,
)
MagGetWindowTransform.errcheck = _errCheck

_MagSetFullscreenTransformFuncType = WINFUNCTYPE(BOOL, POINTER(FLOAT), POINTER(INT), POINTER(INT))
_MagSetFullscreenTransformArgTypes = ((1, "magLevel"), (1, "offsetX"), (1, "offsetY"))
MagSetFullscreenTransform = _MagSetFullscreenTransformFuncType(
("MagSetFullscreenTransform", _magnification),
_MagSetFullscreenTransformArgTypes,
)
MagSetFullscreenTransform.errcheck = _errCheck

_MagGetFullscreenTransformFuncType = WINFUNCTYPE(BOOL, POINTER(FLOAT), POINTER(INT), POINTER(INT))
_MagGetFullscreenTransformArgTypes = ((2, "magLevel"), (2, "offsetX"), (2, "offsetY"))
MagGetFullscreenTransform = _MagGetFullscreenTransformFuncType(
("MagGetFullscreenTransform", _magnification),
_MagGetFullscreenTransformArgTypes,
)
MagGetFullscreenTransform.errcheck = _errCheck

# # Create transformation window
# _MagGetInputTransformFuncType = WINFUNCTYPE(BOOL, POINTER(BOOL), POINTER(RECT), POINTER(RECT))
# _MagGetInputTransformArgTypes = ((2, "enabled"), (2, "src"), (2, "dest"))
# MagGetInputTransform = _MagGetInputTransformFuncType(
# ("MagGetInputTransform", _magnification),
# _MagGetInputTransformArgTypes,
# )
# MagGetInputTransform.errcheck = _errCheck

# # Create transformation window
# _MagSetInputTransformFuncType = WINFUNCTYPE(BOOL, POINTER(BOOL), POINTER(RECT), POINTER(RECT))
# _MagSetInputTransformArgTypes = ((1, "enabled"), (1, "src"), (1, "dest"))
# MagSetInputTransform = _MagGetInputTransformFuncType(
# ("MagSetInputTransform", _magnification),
# _MagSetInputTransformArgTypes,
# )
# MagSetInputTransform.errcheck = _errCheck


# Translators: Name for a vision enhancement provider that disables output to the screen,
# making it black.
Expand Down
5 changes: 4 additions & 1 deletion source/winUser.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ class GUITHREADINFO(Structure):
WS_VSCROLL = 0x200000
WS_CAPTION = 0xC00000
WS_CLIPCHILDREN = 0x02000000
WS_MAXIMIZEBOX = 0x00010000
WS_CHILD = 0x40000000
MS_SHOWMAGNIFIEDCURSOR = 0x0001
WS_EX_TOPMOST = 0x00000008
WS_EX_LAYERED = 0x80000
WS_EX_TOOLWINDOW = 0x00000080
Expand Down Expand Up @@ -533,7 +536,7 @@ def getControlID(hwnd):
return user32.GetWindowLongW(hwnd, GWL_ID)


def getClientRect(hwnd):
def getClientRect(hwnd: HWND) -> RECT:
r = RECT()
if not user32.GetClientRect(hwnd, byref(r)):
raise WinError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import unittest

from visionEnhancementProviders.screenCurtain import Magnification, TRANSFORM_BLACK
from winAPI import _displayTracking
from visionEnhancementProviders.magnifier import HostWindow
from visionEnhancementProviders.screenCurtain import MAGTRANSFORM, Magnification, TRANSFORM_BLACK


class _Test_MagnificationAPI(unittest.TestCase):
Expand Down Expand Up @@ -53,3 +55,49 @@ def test_MagShowSystemCursor(self):
def test_MagHideSystemCursor(self):
result = Magnification.MagShowSystemCursor(False)
self.assertTrue(result)


class Test_Magnification(unittest.TestCase):
def setUp(self):
self.hostWindow: HostWindow | None = None
self._prevOrientationState = _displayTracking._orientationState
self.assertIsNone(self._prevOrientationState)
_displayTracking.initialize()
self.assertTrue(Magnification.MagInitialize())

def tearDown(self):
self.assertTrue(Magnification.MagUninitialize())
_displayTracking._orientationState = self._prevOrientationState
if self.hostWindow:
self.hostWindow.destroy()

def _initializeMagWindow(self, magnificationFactor: int = 1):
self.hostWindow = HostWindow(magnificationFactor)
self.assertTrue(self.hostWindow.handle)
self.assertTrue(self.hostWindow.magnifierWindow.handle)

def test_setAndConfirmMagLevel(self):
expectedTransform = MAGTRANSFORM(2)
self._initializeMagWindow(2)
resultTransform = Magnification.MagGetWindowTransform(self.hostWindow.magnifierWindow.handle)
for i in range(3):
for j in range(3):
with self.subTest(i=i, j=j):
self.assertEqual(
expectedTransform.v[i][j],
resultTransform.v[i][j],
msg=f"i={i}, j={j}, resultTransform={resultTransform}",
)

def test_getDefaultIdentityMagLevel(self):
self._initializeMagWindow()
resultTransform = Magnification.MagGetWindowTransform(self.hostWindow.magnifierWindow.handle)
for i in range(3):
for j in range(3):
with self.subTest(i=i, j=j):
self.assertEqual(
# The transform matrix should be the identity matrix
int(i == j),
resultTransform.v[i][j],
msg=f"i={i}, j={j}, resultTransform={resultTransform}",
)