visppy 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- visppy-0.0.1/PKG-INFO +11 -0
- visppy-0.0.1/README.md +0 -0
- visppy-0.0.1/pyproject.toml +19 -0
- visppy-0.0.1/pyproject.toml.orig +19 -0
- visppy-0.0.1/src/visppy/__init__.py +138 -0
- visppy-0.0.1/src/visppy/embed.py +0 -0
- visppy-0.0.1/src/visppy/windows.py +177 -0
visppy-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: visppy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: TODO
|
|
5
|
+
Requires-Dist: coredeco>=1.1.0
|
|
6
|
+
Requires-Dist: mss>=10.2.0
|
|
7
|
+
Requires-Dist: ntmemoryapi>=2.6.0
|
|
8
|
+
Requires-Dist: pillow>=12.3.0
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
visppy-0.0.1/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "visppy"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "TODO"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"coredeco>=1.1.0",
|
|
9
|
+
"mss>=10.2.0",
|
|
10
|
+
"ntmemoryapi>=2.6.0",
|
|
11
|
+
"pillow>=12.3.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
visppy = "visppy:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "visppy"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "TODO"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"coredeco>=1.1.0",
|
|
9
|
+
"mss>=10.2.0",
|
|
10
|
+
"ntmemoryapi>=2.6.0",
|
|
11
|
+
"pillow>=12.3.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
visppy = "visppy:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import mss
|
|
4
|
+
import coredeco
|
|
5
|
+
import PIL.Image
|
|
6
|
+
import dataclasses
|
|
7
|
+
import ntmemoryapi
|
|
8
|
+
|
|
9
|
+
# Local imports
|
|
10
|
+
from . import embed
|
|
11
|
+
from . import windows
|
|
12
|
+
|
|
13
|
+
# Custom types
|
|
14
|
+
type Rect[T] = tuple[T, T, T, T]
|
|
15
|
+
type RGBColor[T] = tuple[T, T, T]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ==----------------------------------------------------------------== #
|
|
19
|
+
# Dataclasses #
|
|
20
|
+
# ==----------------------------------------------------------------== #
|
|
21
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
22
|
+
class BoudingBox:
|
|
23
|
+
"""Dataclass that contains information about found image bouding box and confidence."""
|
|
24
|
+
|
|
25
|
+
# Initial confidence score with which image was found
|
|
26
|
+
confidence: float
|
|
27
|
+
|
|
28
|
+
# Position information
|
|
29
|
+
x: int
|
|
30
|
+
y: int
|
|
31
|
+
w: int
|
|
32
|
+
h: int
|
|
33
|
+
|
|
34
|
+
# ==--------------------------------== #
|
|
35
|
+
# Properties #
|
|
36
|
+
# ==--------------------------------== #
|
|
37
|
+
@property
|
|
38
|
+
def center(self) -> tuple[int, int]:
|
|
39
|
+
"""Locate position center."""
|
|
40
|
+
|
|
41
|
+
# Return position
|
|
42
|
+
return self.x + self.w // 2, self.y + self.h // 2
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ==----------------------------------------------------------------== #
|
|
46
|
+
# Classes #
|
|
47
|
+
# ==----------------------------------------------------------------== #
|
|
48
|
+
class VisionEngine:
|
|
49
|
+
"""Capture dispay screen, search needle images on haystack image or display screen capture."""
|
|
50
|
+
|
|
51
|
+
# ==--------------------------------== #
|
|
52
|
+
# Public methods #
|
|
53
|
+
# ==--------------------------------== #
|
|
54
|
+
def __init__(self, tracking_window: str | None = None) -> None:
|
|
55
|
+
"""Create instance of vision engine, capture `tracking_window` region if defined."""
|
|
56
|
+
|
|
57
|
+
# Save initializer arguments as instance attributes
|
|
58
|
+
self.tracking_window = tracking_window
|
|
59
|
+
|
|
60
|
+
# Instance inner attributes
|
|
61
|
+
self.image_cache = {}
|
|
62
|
+
self.screenshoter = mss.MSS()
|
|
63
|
+
|
|
64
|
+
def screenshot(self, region: Rect[int] | None = None) -> PIL.Image.Image:
|
|
65
|
+
"""Get display screenshot."""
|
|
66
|
+
|
|
67
|
+
# Grab display screenshot
|
|
68
|
+
screengrab = self.screenshoter.grab(self.screenshoter.monitors[1] if (region := self.__get_region(region)) is None else {
|
|
69
|
+
key: value for key, value in zip(["left", "top", "width", "height"], region)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
# Convert screenshot to RGB format image and return
|
|
73
|
+
return PIL.Image.frombuffer(
|
|
74
|
+
"RGB", screengrab.size, screengrab.bgra, "raw", "BGRX"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def pixel(self, x: int, y: int, *, cache_haystack: bool = False, haystack: io.BytesIO | PIL.Image.Image | str | None = None) -> RGBColor[int]:
|
|
78
|
+
"""Get pixel color on an image, make display screenshot and use it as haystack if `haystack` is `None`."""
|
|
79
|
+
|
|
80
|
+
# Get image to get pixel information
|
|
81
|
+
image = self.screenshot() if haystack is None else self.__load_image(haystack, cache_haystack)
|
|
82
|
+
|
|
83
|
+
# Get pixel information
|
|
84
|
+
return image.getpixel((x, y))
|
|
85
|
+
|
|
86
|
+
# ==--------------------------------== #
|
|
87
|
+
# Private methods #
|
|
88
|
+
# ==--------------------------------== #
|
|
89
|
+
def __get_region(self, region: Rect[int] | None = None) -> Rect[int] | None:
|
|
90
|
+
"""Get region for capture."""
|
|
91
|
+
|
|
92
|
+
# If tracking window is not defined
|
|
93
|
+
if self.tracking_window is None or region is not None:
|
|
94
|
+
return region
|
|
95
|
+
|
|
96
|
+
# Retrieve window information
|
|
97
|
+
window_information = windows.get_window_position(self.tracking_window)["visible_area"]
|
|
98
|
+
|
|
99
|
+
# If window if out of the screen
|
|
100
|
+
if window_information["w"] <= 0 or window_information["h"] <= 0:
|
|
101
|
+
raise RuntimeError(f"Window {self.tracking_window} is out of the screen")
|
|
102
|
+
|
|
103
|
+
return tuple(window_information.values())
|
|
104
|
+
|
|
105
|
+
def __load_image(self, data: io.BytesIO | PIL.Image.Image | str, cache_image: bool = False) -> PIL.Image.Image:
|
|
106
|
+
"""Load `PIL.Image.Image` instance from given data."""
|
|
107
|
+
|
|
108
|
+
# If data is already an image
|
|
109
|
+
if isinstance(data, PIL.Image.Image):
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
# If data is not an image
|
|
113
|
+
if isinstance(data, (io.BytesIO, str)):
|
|
114
|
+
|
|
115
|
+
# If image caching required
|
|
116
|
+
if cache_image:
|
|
117
|
+
|
|
118
|
+
# If image is not cached
|
|
119
|
+
if data not in self.image_cache:
|
|
120
|
+
|
|
121
|
+
# Open image and load it
|
|
122
|
+
image = PIL.Image.open(data)
|
|
123
|
+
image.load()
|
|
124
|
+
|
|
125
|
+
# Save image to cache
|
|
126
|
+
self.image_cache[data] = image
|
|
127
|
+
|
|
128
|
+
# Return image from cache
|
|
129
|
+
return self.image_cache[data]
|
|
130
|
+
|
|
131
|
+
# Open image and load it
|
|
132
|
+
image = PIL.Image.open(data)
|
|
133
|
+
image.load()
|
|
134
|
+
|
|
135
|
+
return image
|
|
136
|
+
|
|
137
|
+
# Data is not an image
|
|
138
|
+
raise ValueError("Data can't be converted into pillow image")
|
|
File without changes
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# ==----------------------------------------------------------------== #
|
|
5
|
+
# C-structures #
|
|
6
|
+
# ==----------------------------------------------------------------== #
|
|
7
|
+
class Rect(ctypes.Structure):
|
|
8
|
+
"""Structure that contains window rect position."""
|
|
9
|
+
|
|
10
|
+
_fields_ = [
|
|
11
|
+
("left", ctypes.c_long),
|
|
12
|
+
("top", ctypes.c_long),
|
|
13
|
+
("right", ctypes.c_long),
|
|
14
|
+
("bottom", ctypes.c_long),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DevMode(ctypes.Structure):
|
|
19
|
+
"""Structure that contains display information."""
|
|
20
|
+
|
|
21
|
+
_fields_ = [
|
|
22
|
+
("dm_device_name", ctypes.c_wchar * 32),
|
|
23
|
+
("dm_spec_version", ctypes.c_ushort),
|
|
24
|
+
("dm_driver_version", ctypes.c_ushort),
|
|
25
|
+
("dm_size", ctypes.c_ushort),
|
|
26
|
+
("dm_driver_extra", ctypes.c_ushort),
|
|
27
|
+
("dm_fields", ctypes.c_ulong),
|
|
28
|
+
("dm_position_x", ctypes.c_long),
|
|
29
|
+
("dm_Position_y", ctypes.c_long),
|
|
30
|
+
("dm_display_orientation", ctypes.c_ulong),
|
|
31
|
+
("dm_display_fixed_output", ctypes.c_ulong),
|
|
32
|
+
("dm_color", ctypes.c_short),
|
|
33
|
+
("dm_duplex", ctypes.c_short),
|
|
34
|
+
("dm_y_resolution", ctypes.c_short),
|
|
35
|
+
("dm_tt_option", ctypes.c_short),
|
|
36
|
+
("dm_collate", ctypes.c_short),
|
|
37
|
+
("dm_form_name", ctypes.c_wchar * 32),
|
|
38
|
+
("dm_log_pixels", ctypes.c_ushort),
|
|
39
|
+
("dm_bits_per_pel", ctypes.c_ulong),
|
|
40
|
+
("dm_pels_width", ctypes.c_ulong),
|
|
41
|
+
("dm_pels_height", ctypes.c_ulong),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ==----------------------------------------------------------------== #
|
|
46
|
+
# Functions #
|
|
47
|
+
# ==----------------------------------------------------------------== #
|
|
48
|
+
def window_exists(window_title: str) -> bool:
|
|
49
|
+
"""Check if window with given title exist."""
|
|
50
|
+
|
|
51
|
+
return bool(ctypes.windll.user32.FindWindowW(None, window_title))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def set_window_title(window_title: str, new_window_title: str) -> None:
|
|
55
|
+
"""Set new title for window with given title."""
|
|
56
|
+
|
|
57
|
+
# If window not found
|
|
58
|
+
if not (window_descriptor := ctypes.windll.user32.FindWindowW(None, window_title)):
|
|
59
|
+
raise ctypes.WinError(descr="Unable to find window with `%s` title" % window_title)
|
|
60
|
+
|
|
61
|
+
# Set new window title
|
|
62
|
+
if not ctypes.windll.user32.SetWindowTextW(window_descriptor, new_window_title):
|
|
63
|
+
raise ctypes.WinError(descr="Unable to set `%s` title for window with `%s` title" % (new_window_title, window_title))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def set_window_position(window_title: str, position: tuple[int, int]) -> None:
|
|
67
|
+
"""Set new position for window with given title."""
|
|
68
|
+
|
|
69
|
+
# If window not found
|
|
70
|
+
if not (window_descriptor := ctypes.windll.user32.FindWindowW(None, window_title)):
|
|
71
|
+
raise ctypes.WinError(descr="Unable to find window with `%s` title" % window_title)
|
|
72
|
+
|
|
73
|
+
# Set new window position
|
|
74
|
+
if not ctypes.windll.user32.SetWindowPos(window_descriptor, None, *position, 0, 0, 0x1):
|
|
75
|
+
raise ctypes.WinError(descr="Unable to set position for window with `%s` title" % window_title)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def set_window_size(window_title: str, size: tuple[int, int]) -> None:
|
|
79
|
+
"""Set new size for window with given title."""
|
|
80
|
+
|
|
81
|
+
# If window not found
|
|
82
|
+
if not (window_descriptor := ctypes.windll.user32.FindWindowW(None, window_title)):
|
|
83
|
+
raise ctypes.WinError(descr="Unable to find window with `%s` title" % window_title)
|
|
84
|
+
|
|
85
|
+
# Set new window size
|
|
86
|
+
if not ctypes.windll.user32.SetWindowPos(window_descriptor, None, 0, 0, *size, 0x2):
|
|
87
|
+
raise ctypes.WinError(descr="Unable to set size for window with `%s` title" % window_title)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def set_window_foreground(window_title: str) -> None:
|
|
91
|
+
"""Set window with given title foreground."""
|
|
92
|
+
|
|
93
|
+
# If window not found
|
|
94
|
+
if not (window_descriptor := ctypes.windll.user32.FindWindowW(None, window_title)):
|
|
95
|
+
raise ctypes.WinError(descr="Unable to find window with `%s` title" % window_title)
|
|
96
|
+
|
|
97
|
+
# Set window foreground
|
|
98
|
+
if not ctypes.windll.user32.SetForegroundWindow(window_descriptor):
|
|
99
|
+
raise ctypes.WinError(descr="Unable o set window with `%s` title foreground" % window_title)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def set_window_frameless(window_title: str) -> None:
|
|
103
|
+
"""Set window with given title frameless."""
|
|
104
|
+
|
|
105
|
+
# If window not found
|
|
106
|
+
if not (window_descriptor := ctypes.windll.user32.FindWindowW(None, window_title)):
|
|
107
|
+
raise ctypes.WinError(descr="Unable to find window with `%s` title" % window_title)
|
|
108
|
+
|
|
109
|
+
# If unable to retrieve windows styles
|
|
110
|
+
if not (window_style := ctypes.windll.user32.GetWindowLongW(window_descriptor, -16)):
|
|
111
|
+
raise ctypes.WinError(descr="Unable to retrieve styles for window with `%s` title" % window_title)
|
|
112
|
+
|
|
113
|
+
# If unable to update windows styles
|
|
114
|
+
if not ctypes.windll.user32.SetWindowLongW(window_descriptor, -16, window_style & ~(0x10000 | 0x20000 | 0x40000 | 0x80000 | 0xC00000)):
|
|
115
|
+
raise ctypes.WinError(descr="Unable to update styles for window with `%s` tltle" % window_title)
|
|
116
|
+
|
|
117
|
+
# If unable to update window to apply styles
|
|
118
|
+
if not ctypes.windll.user32.SetWindowPos(window_descriptor, None, 0, 0, 0, 0, 0x27):
|
|
119
|
+
raise ctypes.WinError(descr="Unable to update window with `%s` title to apply styles" % window_title)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_window_position(window_title: str) -> dict[str, dict | int]:
|
|
123
|
+
"""Get window position information that contains its absolute screen coordinates, size and visible area."""
|
|
124
|
+
|
|
125
|
+
# If window not found
|
|
126
|
+
if not (window_descriptor := ctypes.windll.user32.FindWindowW(None, window_title)):
|
|
127
|
+
raise ctypes.WinError(descr="Unable to find window with `%s` title" % window_title)
|
|
128
|
+
|
|
129
|
+
# If unable to retrieve physical dispaly resolutions
|
|
130
|
+
if not ctypes.windll.user32.EnumDisplaySettingsW(None, 0xFFFFFFFF, ctypes.byref(devmode := DevMode(dmSize=ctypes.sizeof(DevMode), dmFields=0x00040000 | 0x00080000))):
|
|
131
|
+
raise ctypes.WinError(descr="Unable to retrieve physical display information")
|
|
132
|
+
|
|
133
|
+
# If unable to retrieve window information
|
|
134
|
+
if ctypes.windll.dwmapi.DwmGetWindowAttribute(window_descriptor, 9, ctypes.byref(window_rect := Rect()), ctypes.sizeof(window_rect)):
|
|
135
|
+
raise ctypes.WinError(descr="Unable to retrieve information for window with `%s` title" % window_title)
|
|
136
|
+
|
|
137
|
+
# Result dict
|
|
138
|
+
result = {"visible_area": {"x": 0, "y": 0, "w": 0, "h": 0}}
|
|
139
|
+
|
|
140
|
+
# Visible area of window section
|
|
141
|
+
visible_area = result["visible_area"]
|
|
142
|
+
|
|
143
|
+
# Save window position
|
|
144
|
+
result["x"] = window_rect.left
|
|
145
|
+
result["y"] = window_rect.top
|
|
146
|
+
|
|
147
|
+
# Save window size
|
|
148
|
+
result["w"] = window_rect.right - window_rect.left
|
|
149
|
+
result["h"] = window_rect.bottom - window_rect.top
|
|
150
|
+
|
|
151
|
+
# Get window visible area horizontaly
|
|
152
|
+
if result["x"] < 0:
|
|
153
|
+
visible_area["x"] = 0
|
|
154
|
+
visible_area["w"] = result["w"] + result["x"]
|
|
155
|
+
|
|
156
|
+
elif result["x"] + result["w"] > devmode.dm_pels_width:
|
|
157
|
+
visible_area["x"] = result["x"]
|
|
158
|
+
visible_area["w"] = result["w"] - (result["w"] + result["x"] - devmode.dm_pels_width)
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
visible_area["x"] = result["x"]
|
|
162
|
+
visible_area["w"] = result["w"]
|
|
163
|
+
|
|
164
|
+
# Get window visible area verticaly
|
|
165
|
+
if result["y"] < 0:
|
|
166
|
+
visible_area["y"] = 0
|
|
167
|
+
visible_area["h"] = result["h"] + result["y"]
|
|
168
|
+
|
|
169
|
+
elif result["y"] + result["h"] > devmode.dm_pels_height:
|
|
170
|
+
visible_area["y"] = result["y"]
|
|
171
|
+
visible_area["h"] = result["h"] - (result["h"] + result["y"] - devmode.dm_pels_height)
|
|
172
|
+
|
|
173
|
+
else:
|
|
174
|
+
visible_area["y"] = result["y"]
|
|
175
|
+
visible_area["h"] = result["h"]
|
|
176
|
+
|
|
177
|
+
return result
|