kotonebot 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

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.
Files changed (64) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -312
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/task_action.py +183 -183
  6. kotonebot/backend/core.py +129 -129
  7. kotonebot/backend/debug/entry.py +89 -89
  8. kotonebot/backend/debug/mock.py +78 -78
  9. kotonebot/backend/debug/server.py +222 -222
  10. kotonebot/backend/debug/vars.py +351 -351
  11. kotonebot/backend/dispatch.py +227 -227
  12. kotonebot/backend/flow_controller.py +196 -196
  13. kotonebot/backend/ocr.py +535 -529
  14. kotonebot/backend/preprocessor.py +103 -103
  15. kotonebot/client/__init__.py +9 -9
  16. kotonebot/client/device.py +528 -503
  17. kotonebot/client/fast_screenshot.py +377 -377
  18. kotonebot/client/host/__init__.py +43 -12
  19. kotonebot/client/host/adb_common.py +107 -103
  20. kotonebot/client/host/custom.py +118 -114
  21. kotonebot/client/host/leidian_host.py +196 -201
  22. kotonebot/client/host/mumu12_host.py +353 -358
  23. kotonebot/client/host/protocol.py +214 -213
  24. kotonebot/client/host/windows_common.py +58 -58
  25. kotonebot/client/implements/__init__.py +71 -15
  26. kotonebot/client/implements/adb.py +89 -85
  27. kotonebot/client/implements/adb_raw.py +162 -158
  28. kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
  29. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  30. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  31. kotonebot/client/implements/remote_windows.py +188 -188
  32. kotonebot/client/implements/uiautomator2.py +85 -81
  33. kotonebot/client/implements/windows.py +176 -172
  34. kotonebot/client/protocol.py +69 -69
  35. kotonebot/client/registration.py +24 -24
  36. kotonebot/config/base_config.py +96 -96
  37. kotonebot/config/manager.py +36 -36
  38. kotonebot/errors.py +76 -71
  39. kotonebot/interop/win/__init__.py +10 -3
  40. kotonebot/interop/win/_mouse.py +311 -0
  41. kotonebot/interop/win/message_box.py +313 -313
  42. kotonebot/interop/win/reg.py +37 -37
  43. kotonebot/interop/win/shortcut.py +43 -43
  44. kotonebot/interop/win/task_dialog.py +513 -513
  45. kotonebot/logging/__init__.py +2 -2
  46. kotonebot/logging/log.py +17 -17
  47. kotonebot/primitives/__init__.py +17 -17
  48. kotonebot/primitives/geometry.py +862 -290
  49. kotonebot/primitives/visual.py +63 -63
  50. kotonebot/tools/mirror.py +354 -354
  51. kotonebot/ui/file_host/sensio.py +36 -36
  52. kotonebot/ui/file_host/tmp_send.py +54 -54
  53. kotonebot/ui/pushkit/__init__.py +3 -3
  54. kotonebot/ui/pushkit/image_host.py +88 -87
  55. kotonebot/ui/pushkit/protocol.py +13 -13
  56. kotonebot/ui/pushkit/wxpusher.py +54 -53
  57. kotonebot/ui/user.py +148 -148
  58. kotonebot/util.py +436 -436
  59. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -81
  60. kotonebot-0.5.0.dist-info/RECORD +71 -0
  61. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
  62. kotonebot-0.4.0.dist-info/RECORD +0 -70
  63. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
  64. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,3 @@
1
- from .context import *
2
- from .context import _c
3
- from .task_action import task, action, task_registry, action_registry, current_callstack, Task, Action, tasks_from_id
1
+ from .context import *
2
+ from .context import _c
3
+ from .task_action import task, action, task_registry, action_registry, current_callstack, Task, Action, tasks_from_id
@@ -1,184 +1,184 @@
1
- import logging
2
- import warnings
3
- from dataclasses import dataclass
4
- from typing_extensions import deprecated
5
- from typing import Callable, ParamSpec, TypeVar, overload, Literal
6
-
7
-
8
- from .context import ContextStackVars, ScreenshotMode
9
- from ...errors import TaskNotFoundError
10
-
11
- P = ParamSpec('P')
12
- R = TypeVar('R')
13
- logger = logging.getLogger(__name__)
14
-
15
- TaskRunAtType = Literal['pre', 'post', 'manual', 'regular'] | str
16
-
17
-
18
- @dataclass
19
- class Task:
20
- name: str
21
- id: str
22
- description: str
23
- func: Callable
24
- priority: int
25
- """
26
- 任务优先级,数字越大优先级越高。
27
- """
28
- run_at: TaskRunAtType = 'regular'
29
-
30
-
31
- @dataclass
32
- class Action:
33
- name: str
34
- description: str
35
- func: Callable
36
- priority: int
37
- """
38
- 动作优先级,数字越大优先级越高。
39
- """
40
-
41
-
42
- task_registry: dict[str, Task] = {}
43
- action_registry: dict[str, Action] = {}
44
- current_callstack: list[Task|Action] = []
45
-
46
- def _placeholder():
47
- raise NotImplementedError('Placeholder function')
48
-
49
- def task(
50
- name: str,
51
- task_id: str|None = None,
52
- description: str|None = None,
53
- *,
54
- pass_through: bool = False,
55
- priority: int = 0,
56
- screenshot_mode: ScreenshotMode = 'auto',
57
- run_at: TaskRunAtType = 'regular'
58
- ):
59
- """
60
- `task` 装饰器,用于标记一个函数为任务函数。
61
-
62
- :param name: 任务名称
63
- :param task_id: 任务 ID。如果为 None,则使用函数名称作为 ID。
64
- :param description: 任务描述。如果为 None,则使用函数的 docstring 作为描述。
65
- :param pass_through:
66
- 默认情况下, @task 装饰器会包裹任务函数,跟踪其执行情况。
67
- 如果不想跟踪,则设置此参数为 False。
68
- :param priority: 任务优先级,数字越大优先级越高。
69
- :param run_at: 任务运行时间。
70
- """
71
- # 设置 ID
72
- # 获取 caller 信息
73
- def _task_decorator(func: Callable[P, R]) -> Callable[P, R]:
74
- nonlocal description, task_id
75
- description = description or func.__doc__ or ''
76
- # TODO: task_id 冲突检测
77
- task_id = task_id or func.__name__
78
- task = Task(name, task_id, description, _placeholder, priority, run_at)
79
- task_registry[name] = task
80
- logger.debug(f'Task "{name}" registered.')
81
- if pass_through:
82
- return func
83
- else:
84
- def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
85
- current_callstack.append(task)
86
- vars = ContextStackVars.push(screenshot_mode=screenshot_mode)
87
- ret = func(*args, **kwargs)
88
- ContextStackVars.pop()
89
- current_callstack.pop()
90
- return ret
91
- task.func = _wrapper
92
- return _wrapper
93
- return _task_decorator
94
-
95
- @overload
96
- def action(func: Callable[P, R]) -> Callable[P, R]: ...
97
-
98
- @deprecated('Use `action` with screenshot_mode=`manual` instead.')
99
- @overload
100
- def action(
101
- name: str,
102
- *,
103
- description: str|None = None,
104
- pass_through: bool = False,
105
- priority: int = 0,
106
- screenshot_mode: Literal['manual-inherit'],
107
- ) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
108
-
109
- @overload
110
- def action(
111
- name: str,
112
- *,
113
- description: str|None = None,
114
- pass_through: bool = False,
115
- priority: int = 0,
116
- screenshot_mode: ScreenshotMode | None = None,
117
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
118
- """
119
- `action` 装饰器,用于标记一个函数为动作函数。
120
-
121
- :param name: 动作名称。如果为 None,则使用函数的名称作为名称。
122
- :param description: 动作描述。如果为 None,则使用函数的 docstring 作为描述。
123
- :param pass_through:
124
- 默认情况下, @action 装饰器会包裹动作函数,跟踪其执行情况。
125
- 如果不想跟踪,则设置此参数为 False。
126
- :param priority: 动作优先级,数字越大优先级越高。
127
- :param screenshot_mode: 截图模式。
128
- """
129
- ...
130
-
131
- def action(*args, **kwargs):
132
- def _register(func: Callable, name: str, description: str|None = None, priority: int = 0) -> Action:
133
- description = description or func.__doc__ or ''
134
- action = Action(name, description, func, priority)
135
- action_registry[name] = action
136
- logger.debug(f'Action "{name}" registered.')
137
- return action
138
-
139
- if len(args) == 1 and isinstance(args[0], Callable):
140
- func = args[0]
141
- action = _register(_placeholder, func.__name__, func.__doc__)
142
- def _wrapper(*args: P.args, **kwargs: P.kwargs):
143
- current_callstack.append(action)
144
- vars = ContextStackVars.push()
145
- ret = func(*args, **kwargs)
146
- ContextStackVars.pop()
147
- current_callstack.pop()
148
- return ret
149
- action.func = _wrapper
150
- return _wrapper
151
- else:
152
- name = args[0]
153
- description = kwargs.get('description', None)
154
- pass_through = kwargs.get('pass_through', False)
155
- priority = kwargs.get('priority', 0)
156
- screenshot_mode = kwargs.get('screenshot_mode', None)
157
- if screenshot_mode == 'manual-inherit':
158
- warnings.warn('`screenshot_mode=manual-inherit` is deprecated. Use `screenshot_mode=manual` instead.')
159
- def _action_decorator(func: Callable):
160
- nonlocal pass_through
161
- action = _register(_placeholder, name, description)
162
- pass_through = kwargs.get('pass_through', False)
163
- if pass_through:
164
- return func
165
- else:
166
- def _wrapper(*args: P.args, **kwargs: P.kwargs):
167
- current_callstack.append(action)
168
- vars = ContextStackVars.push(screenshot_mode=screenshot_mode)
169
- ret = func(*args, **kwargs)
170
- ContextStackVars.pop()
171
- current_callstack.pop()
172
- return ret
173
- action.func = _wrapper
174
- return _wrapper
175
- return _action_decorator
176
-
177
- def tasks_from_id(task_ids: list[str]) -> list[Task]:
178
- result = []
179
- for tid in task_ids:
180
- target = next(task for task in task_registry.values() if task.id == tid)
181
- if target is None:
182
- raise TaskNotFoundError(f'Task "{tid}" not found.')
183
- result.append(target)
1
+ import logging
2
+ import warnings
3
+ from dataclasses import dataclass
4
+ from typing_extensions import deprecated
5
+ from typing import Callable, ParamSpec, TypeVar, overload, Literal
6
+
7
+
8
+ from .context import ContextStackVars, ScreenshotMode
9
+ from ...errors import TaskNotFoundError
10
+
11
+ P = ParamSpec('P')
12
+ R = TypeVar('R')
13
+ logger = logging.getLogger(__name__)
14
+
15
+ TaskRunAtType = Literal['pre', 'post', 'manual', 'regular'] | str
16
+
17
+
18
+ @dataclass
19
+ class Task:
20
+ name: str
21
+ id: str
22
+ description: str
23
+ func: Callable
24
+ priority: int
25
+ """
26
+ 任务优先级,数字越大优先级越高。
27
+ """
28
+ run_at: TaskRunAtType = 'regular'
29
+
30
+
31
+ @dataclass
32
+ class Action:
33
+ name: str
34
+ description: str
35
+ func: Callable
36
+ priority: int
37
+ """
38
+ 动作优先级,数字越大优先级越高。
39
+ """
40
+
41
+
42
+ task_registry: dict[str, Task] = {}
43
+ action_registry: dict[str, Action] = {}
44
+ current_callstack: list[Task|Action] = []
45
+
46
+ def _placeholder():
47
+ raise NotImplementedError('Placeholder function')
48
+
49
+ def task(
50
+ name: str,
51
+ task_id: str|None = None,
52
+ description: str|None = None,
53
+ *,
54
+ pass_through: bool = False,
55
+ priority: int = 0,
56
+ screenshot_mode: ScreenshotMode = 'auto',
57
+ run_at: TaskRunAtType = 'regular'
58
+ ):
59
+ """
60
+ `task` 装饰器,用于标记一个函数为任务函数。
61
+
62
+ :param name: 任务名称
63
+ :param task_id: 任务 ID。如果为 None,则使用函数名称作为 ID。
64
+ :param description: 任务描述。如果为 None,则使用函数的 docstring 作为描述。
65
+ :param pass_through:
66
+ 默认情况下, @task 装饰器会包裹任务函数,跟踪其执行情况。
67
+ 如果不想跟踪,则设置此参数为 False。
68
+ :param priority: 任务优先级,数字越大优先级越高。
69
+ :param run_at: 任务运行时间。
70
+ """
71
+ # 设置 ID
72
+ # 获取 caller 信息
73
+ def _task_decorator(func: Callable[P, R]) -> Callable[P, R]:
74
+ nonlocal description, task_id
75
+ description = description or func.__doc__ or ''
76
+ # TODO: task_id 冲突检测
77
+ task_id = task_id or func.__name__
78
+ task = Task(name, task_id, description, _placeholder, priority, run_at)
79
+ task_registry[name] = task
80
+ logger.debug(f'Task "{name}" registered.')
81
+ if pass_through:
82
+ return func
83
+ else:
84
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
85
+ current_callstack.append(task)
86
+ vars = ContextStackVars.push(screenshot_mode=screenshot_mode)
87
+ ret = func(*args, **kwargs)
88
+ ContextStackVars.pop()
89
+ current_callstack.pop()
90
+ return ret
91
+ task.func = _wrapper
92
+ return _wrapper
93
+ return _task_decorator
94
+
95
+ @overload
96
+ def action(func: Callable[P, R]) -> Callable[P, R]: ...
97
+
98
+ @deprecated('Use `action` with screenshot_mode=`manual` instead.')
99
+ @overload
100
+ def action(
101
+ name: str,
102
+ *,
103
+ description: str|None = None,
104
+ pass_through: bool = False,
105
+ priority: int = 0,
106
+ screenshot_mode: Literal['manual-inherit'],
107
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
108
+
109
+ @overload
110
+ def action(
111
+ name: str,
112
+ *,
113
+ description: str|None = None,
114
+ pass_through: bool = False,
115
+ priority: int = 0,
116
+ screenshot_mode: ScreenshotMode | None = None,
117
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
118
+ """
119
+ `action` 装饰器,用于标记一个函数为动作函数。
120
+
121
+ :param name: 动作名称。如果为 None,则使用函数的名称作为名称。
122
+ :param description: 动作描述。如果为 None,则使用函数的 docstring 作为描述。
123
+ :param pass_through:
124
+ 默认情况下, @action 装饰器会包裹动作函数,跟踪其执行情况。
125
+ 如果不想跟踪,则设置此参数为 False。
126
+ :param priority: 动作优先级,数字越大优先级越高。
127
+ :param screenshot_mode: 截图模式。
128
+ """
129
+ ...
130
+
131
+ def action(*args, **kwargs):
132
+ def _register(func: Callable, name: str, description: str|None = None, priority: int = 0) -> Action:
133
+ description = description or func.__doc__ or ''
134
+ action = Action(name, description, func, priority)
135
+ action_registry[name] = action
136
+ logger.debug(f'Action "{name}" registered.')
137
+ return action
138
+
139
+ if len(args) == 1 and isinstance(args[0], Callable):
140
+ func = args[0]
141
+ action = _register(_placeholder, func.__name__, func.__doc__)
142
+ def _wrapper(*args: P.args, **kwargs: P.kwargs):
143
+ current_callstack.append(action)
144
+ vars = ContextStackVars.push()
145
+ ret = func(*args, **kwargs)
146
+ ContextStackVars.pop()
147
+ current_callstack.pop()
148
+ return ret
149
+ action.func = _wrapper
150
+ return _wrapper
151
+ else:
152
+ name = args[0]
153
+ description = kwargs.get('description', None)
154
+ pass_through = kwargs.get('pass_through', False)
155
+ priority = kwargs.get('priority', 0)
156
+ screenshot_mode = kwargs.get('screenshot_mode', None)
157
+ if screenshot_mode == 'manual-inherit':
158
+ warnings.warn('`screenshot_mode=manual-inherit` is deprecated. Use `screenshot_mode=manual` instead.')
159
+ def _action_decorator(func: Callable):
160
+ nonlocal pass_through
161
+ action = _register(_placeholder, name, description)
162
+ pass_through = kwargs.get('pass_through', False)
163
+ if pass_through:
164
+ return func
165
+ else:
166
+ def _wrapper(*args: P.args, **kwargs: P.kwargs):
167
+ current_callstack.append(action)
168
+ vars = ContextStackVars.push(screenshot_mode=screenshot_mode)
169
+ ret = func(*args, **kwargs)
170
+ ContextStackVars.pop()
171
+ current_callstack.pop()
172
+ return ret
173
+ action.func = _wrapper
174
+ return _wrapper
175
+ return _action_decorator
176
+
177
+ def tasks_from_id(task_ids: list[str]) -> list[Task]:
178
+ result = []
179
+ for tid in task_ids:
180
+ target = next(task for task in task_registry.values() if task.id == tid)
181
+ if target is None:
182
+ raise TaskNotFoundError(f'Task "{tid}" not found.')
183
+ result.append(target)
184
184
  return result
kotonebot/backend/core.py CHANGED
@@ -1,129 +1,129 @@
1
- import logging
2
- from functools import cache
3
- from typing import Callable
4
- from typing_extensions import deprecated
5
-
6
- import cv2
7
- from cv2.typing import MatLike
8
-
9
- from kotonebot.util import cv2_imread
10
- from kotonebot.primitives import RectTuple, Rect, Point
11
- from kotonebot.errors import ResourceFileMissingError
12
-
13
- @deprecated('unused')
14
- class Ocr:
15
- def __init__(
16
- self,
17
- text: str | Callable[[str], bool],
18
- *,
19
- language: str = 'jp',
20
- ):
21
- self.text = text
22
- self.language = language
23
-
24
- # TODO: 这个类和 kotonebot.primitives.Image 重复了
25
- @deprecated('Use kotonebot.primitives.Image instead.')
26
- class Image:
27
- def __init__(
28
- self,
29
- *,
30
- path: str | None = None,
31
- name: str | None = 'untitled',
32
- data: MatLike | None = None,
33
- ):
34
- self.path = path
35
- self.name = name
36
- self.__data: MatLike | None = data
37
- self.__data_with_alpha: MatLike | None = None
38
-
39
- @cache
40
- def binary(self) -> 'Image':
41
- return Image(data=cv2.cvtColor(self.data, cv2.COLOR_BGR2GRAY))
42
-
43
- @property
44
- def data(self) -> MatLike:
45
- if self.__data is None:
46
- if self.path is None:
47
- raise ValueError('Either path or data must be provided.')
48
- self.__data = cv2_imread(self.path)
49
- if self.__data is None:
50
- raise ResourceFileMissingError(self.path, 'sprite')
51
- logger.debug(f'Read image "{self.name}" from {self.path}')
52
- return self.__data
53
-
54
- @property
55
- def data_with_alpha(self) -> MatLike:
56
- if self.__data_with_alpha is None:
57
- if self.path is None:
58
- raise ValueError('Either path or data must be provided.')
59
- self.__data_with_alpha = cv2_imread(self.path, cv2.IMREAD_UNCHANGED)
60
- if self.__data_with_alpha is None:
61
- raise ResourceFileMissingError(self.path, 'sprite with alpha')
62
- logger.debug(f'Read image "{self.name}" from {self.path}')
63
- return self.__data_with_alpha
64
-
65
- def __repr__(self) -> str:
66
- if self.path is None:
67
- return f'<Image: memory>'
68
- else:
69
- return f'<Image: "{self.name}" at {self.path}>'
70
-
71
- # TODO: 这里的其他类应该移动到 primitives 模块下面
72
- class HintBox(Rect):
73
- def __init__(
74
- self,
75
- x1: int,
76
- y1: int,
77
- x2: int,
78
- y2: int,
79
- *,
80
- name: str | None = None,
81
- description: str | None = None,
82
- source_resolution: tuple[int, int],
83
- ):
84
- super().__init__(x1, y1, x2 - x1, y2 - y1, name=name)
85
- self.description = description
86
- self.source_resolution = source_resolution
87
-
88
- @property
89
- def width(self) -> int:
90
- return self.x2 - self.x1
91
-
92
- @property
93
- def height(self) -> int:
94
- return self.y2 - self.y1
95
-
96
- @property
97
- def rect(self) -> RectTuple:
98
- return self.x1, self.y1, self.width, self.height
99
-
100
- class HintPoint(Point):
101
- def __init__(self, x: int, y: int, *, name: str | None = None, description: str | None = None):
102
- super().__init__(x, y, name=name)
103
- self.description = description
104
-
105
- def __repr__(self) -> str:
106
- return f'HintPoint<"{self.name}" at ({self.x}, {self.y})>'
107
-
108
- def unify_image(image: MatLike | str | Image, transparent: bool = False) -> MatLike:
109
- if isinstance(image, str):
110
- if not transparent:
111
- image = cv2_imread(image)
112
- else:
113
- image = cv2_imread(image, cv2.IMREAD_UNCHANGED)
114
- elif isinstance(image, Image):
115
- if transparent:
116
- image = image.data_with_alpha
117
- else:
118
- image = image.data
119
- return image
120
-
121
- logger = logging.getLogger(__name__)
122
-
123
-
124
- if __name__ == '__main__':
125
- hint_box = HintBox(100, 100, 200, 200, source_resolution=(1920, 1080))
126
- print(hint_box.rect)
127
- print(hint_box.width)
128
- print(hint_box.height)
129
-
1
+ import logging
2
+ from functools import cache
3
+ from typing import Callable
4
+ from typing_extensions import deprecated
5
+
6
+ import cv2
7
+ from cv2.typing import MatLike
8
+
9
+ from kotonebot.util import cv2_imread
10
+ from kotonebot.primitives import RectTuple, Rect, Point
11
+ from kotonebot.errors import ResourceFileMissingError
12
+
13
+ @deprecated('unused')
14
+ class Ocr:
15
+ def __init__(
16
+ self,
17
+ text: str | Callable[[str], bool],
18
+ *,
19
+ language: str = 'jp',
20
+ ):
21
+ self.text = text
22
+ self.language = language
23
+
24
+ # TODO: 这个类和 kotonebot.primitives.Image 重复了
25
+ @deprecated('Use kotonebot.primitives.Image instead.')
26
+ class Image:
27
+ def __init__(
28
+ self,
29
+ *,
30
+ path: str | None = None,
31
+ name: str | None = 'untitled',
32
+ data: MatLike | None = None,
33
+ ):
34
+ self.path = path
35
+ self.name = name
36
+ self.__data: MatLike | None = data
37
+ self.__data_with_alpha: MatLike | None = None
38
+
39
+ @cache
40
+ def binary(self) -> 'Image':
41
+ return Image(data=cv2.cvtColor(self.data, cv2.COLOR_BGR2GRAY))
42
+
43
+ @property
44
+ def data(self) -> MatLike:
45
+ if self.__data is None:
46
+ if self.path is None:
47
+ raise ValueError('Either path or data must be provided.')
48
+ self.__data = cv2_imread(self.path)
49
+ if self.__data is None:
50
+ raise ResourceFileMissingError(self.path, 'sprite')
51
+ logger.debug(f'Read image "{self.name}" from {self.path}')
52
+ return self.__data
53
+
54
+ @property
55
+ def data_with_alpha(self) -> MatLike:
56
+ if self.__data_with_alpha is None:
57
+ if self.path is None:
58
+ raise ValueError('Either path or data must be provided.')
59
+ self.__data_with_alpha = cv2_imread(self.path, cv2.IMREAD_UNCHANGED)
60
+ if self.__data_with_alpha is None:
61
+ raise ResourceFileMissingError(self.path, 'sprite with alpha')
62
+ logger.debug(f'Read image "{self.name}" from {self.path}')
63
+ return self.__data_with_alpha
64
+
65
+ def __repr__(self) -> str:
66
+ if self.path is None:
67
+ return f'<Image: memory>'
68
+ else:
69
+ return f'<Image: "{self.name}" at {self.path}>'
70
+
71
+ # TODO: 这里的其他类应该移动到 primitives 模块下面
72
+ class HintBox(Rect):
73
+ def __init__(
74
+ self,
75
+ x1: int,
76
+ y1: int,
77
+ x2: int,
78
+ y2: int,
79
+ *,
80
+ name: str | None = None,
81
+ description: str | None = None,
82
+ source_resolution: tuple[int, int],
83
+ ):
84
+ super().__init__(x1, y1, x2 - x1, y2 - y1, name=name)
85
+ self.description = description
86
+ self.source_resolution = source_resolution
87
+
88
+ @property
89
+ def width(self) -> int:
90
+ return self.x2 - self.x1
91
+
92
+ @property
93
+ def height(self) -> int:
94
+ return self.y2 - self.y1
95
+
96
+ @property
97
+ def rect(self) -> RectTuple:
98
+ return self.x1, self.y1, self.width, self.height
99
+
100
+ class HintPoint(Point):
101
+ def __init__(self, x: int, y: int, *, name: str | None = None, description: str | None = None):
102
+ super().__init__(x, y, name=name)
103
+ self.description = description
104
+
105
+ def __repr__(self) -> str:
106
+ return f'HintPoint<"{self.name}" at ({self.x}, {self.y})>'
107
+
108
+ def unify_image(image: MatLike | str | Image, transparent: bool = False) -> MatLike:
109
+ if isinstance(image, str):
110
+ if not transparent:
111
+ image = cv2_imread(image)
112
+ else:
113
+ image = cv2_imread(image, cv2.IMREAD_UNCHANGED)
114
+ elif isinstance(image, Image):
115
+ if transparent:
116
+ image = image.data_with_alpha
117
+ else:
118
+ image = image.data
119
+ return image
120
+
121
+ logger = logging.getLogger(__name__)
122
+
123
+
124
+ if __name__ == '__main__':
125
+ hint_box = HintBox(100, 100, 200, 200, source_resolution=(1920, 1080))
126
+ print(hint_box.rect)
127
+ print(hint_box.width)
128
+ print(hint_box.height)
129
+