Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ apparser.egg-info
dist
__pycache__
_build*/
_tmp*/
.pypirc
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Apparser is a Python library for automating desktop applications and interacting

# Installation
```bash
# Base Apparser package with base ocr model
# Base Apparser package with base OCR model
pip install apparser

# Apparser with text recognition support
Expand Down Expand Up @@ -78,7 +78,7 @@ Full documentation is available <a href="https://apparser-development.github.io/
Package page on <a href="https://pypi.org/project/apparser/">PyPI</a>

# Donation
If you'd like to financially support the developers for their work:
If you'd like to financially support the developers' work:

<a href="https://dalink.to/apparser">Donation link</a>

Expand Down
3 changes: 2 additions & 1 deletion apparser/core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ def start_app(self):
self.__find_window_by_process_id(i.get_process_id())
if self.__ui is not None:
return
self.__find_window_by_title()
if self.__window_title_name is not None:
self.__find_window_by_title()
if self.__ui is None:
raise WindowDoesNotValidException()

Expand Down
4 changes: 2 additions & 2 deletions apparser/core/ui/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ def __init__(
raise TypeError('from_ui must be BaseUi')

if not isinstance(point_one, (Point, RelativelyPoint)):
raise TypeError('point1 must be Point or RelativelyPoint')
raise TypeError('point_one must be Point or RelativelyPoint')

elif not isinstance(point_two, (Point, RelativelyPoint)):
raise TypeError('point2 must be Point, RelativelyPoint')
raise TypeError('point_two must be Point or RelativelyPoint')

self.__from_ui = from_ui
self.__point_one = point_one
Expand Down
2 changes: 1 addition & 1 deletion apparser/cv/events/undetected.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ def __str__(self) -> str:
:return: Undetected event name.
:rtype: str
"""
return "UnDetected"
return "Undetected"
2 changes: 1 addition & 1 deletion apparser/exceptions/instruction_not_found.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class InstructionNotFoundException(Exception):
"""Represent a failure to resolve an instruction."""

def __init__(self, text: str):
def __init__(self, text: str | None):
"""Initialize an instruction lookup exception.

:param text: Error message text.
Expand Down
6 changes: 3 additions & 3 deletions apparser/exceptions/text_not_found.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ def __init__(self, min_similarity: float):
"""Initialize a text lookup exception.

:param min_similarity: Minimum accepted similarity value.
:type min_similarity: float
:type min_similarity: float | int
:raises TypeError: If ``min_similarity`` has an invalid type.
:raises ValueError: If ``min_similarity`` is outside the inclusive range from 0 to 1.
"""
if not isinstance(min_similarity, float):
raise TypeError("min_similarity must be float")
if not isinstance(min_similarity, (float, int)):
raise TypeError("min_similarity must be a number")

if min_similarity < 0 or min_similarity > 1:
raise ValueError("min_similarity must be between 0 and 1")
Expand Down
4 changes: 2 additions & 2 deletions apparser/geometry/relatively_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ def __init__(self, x_percent: float, y_percent: float):
raise TypeError('y_percent must be number')

if x_percent < -1 or x_percent > 1:
raise ValueError('x must be between -1 and 1')
raise ValueError('x_percent must be between -1 and 1')

if y_percent < -1 or y_percent > 1:
raise ValueError('y must be between -1 and 1')
raise ValueError('y_percent must be between -1 and 1')

self.__x_percent = x_percent
self.__y_percent = y_percent
Expand Down
4 changes: 2 additions & 2 deletions apparser/instructions/default/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, click_type: BaseKeyCode = LeftClick()):

:param click_type: Mouse button to press down.
:type click_type: BaseKeyCode
:raises TypeError: If ``click_type`` is neither :class:`BaseKeyCode`.
:raises TypeError: If ``click_type`` is not a :class:`BaseKeyCode`.
"""
if not isinstance(click_type, BaseKeyCode):
raise TypeError('click_type must be BaseKeyCode')
Expand All @@ -58,7 +58,7 @@ def __init__(self, click_type: BaseKeyCode = LeftClick()):

:param click_type: Mouse button to release.
:type click_type: BaseKeyCode
:raises TypeError: If ``click_type`` is neither :class:`BaseKeyCode`.
:raises TypeError: If ``click_type`` is not a :class:`BaseKeyCode`.
"""
if not isinstance(click_type, BaseKeyCode):
raise TypeError('click_type must be BaseKeyCode')
Expand Down
10 changes: 6 additions & 4 deletions apparser/instructions/default/press.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,20 @@ def perform(self, *args, **kwargs):
class PressKeysCombination(BaseInstruction):
"""Send a keyboard shortcut as a pressed combination."""

def __init__(self, keys: list[BaseKeyCode | str] | str):
def __init__(self, keys: list[BaseKeyCode | str]):
"""Initialize a key combination instruction.

:param keys: Keys to press together.
:type keys: list[BaseKeyCode | str] | str
:type keys: list[BaseKeyCode | str]
:raises TypeError: If ``keys`` or any key has an invalid type.
"""
if not isinstance(keys, list):
raise TypeError('keys must be list')

self.__keys = keys
self.__validate()

def __validate(self):

for key in self.__keys:
if not (isinstance(key, BaseKeyCode) or isinstance(key, str)):
raise TypeError('key_code must be BaseKeyCode or str')
Expand All @@ -52,7 +55,6 @@ def id(self) -> int:
def perform(self, *args, **kwargs):
for key in self.__keys:
pyautogui.keyDown(str(key))

for key in self.__keys:
pyautogui.keyUp(str(key))

Expand Down
4 changes: 2 additions & 2 deletions apparser/instructions/default/sleep.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
class Sleep(BaseInstruction):
"""Pause execution for a fixed amount of time."""

def __init__(self, sleep_time: float):
def __init__(self, sleep_time: float | int):
"""Initialize a sleep instruction.

:param sleep_time: Delay duration in seconds.
:type sleep_time: float
:type sleep_time: float | int
:raises ValueError: If ``sleep_time`` is not greater than zero.
:raises TypeError: If ``sleep_time`` is not a number.
"""
Expand Down
2 changes: 1 addition & 1 deletion apparser/instructions/ocr/click_on_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(self, text: str,
:type min_similarity: float
:param offset: Offset relative to the detected text center.
:type offset: Point | RelativelyPoint
:param text_getter: Instruction used to extract text from the screen. If None use GetText()
:param text_getter: Instruction used to extract text from the screen. If None, use GetText().
:type text_getter: GetText | None
:param sleep_time_after_move: Delay before the click is performed.
:type sleep_time_after_move: float
Expand Down
10 changes: 5 additions & 5 deletions apparser/instructions/ocr/move_to_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __init__(self, text: str,
:type min_similarity: float
:param offset: Offset relative to the detected text center.
:type offset: Point | RelativelyPoint
:param text_getter: Instruction used to extract text from the screen. If None use GetText()
:param text_getter: Instruction used to extract text from the screen. If None, use GetText().
:type text_getter: GetText | None
"""
if text_getter is None:
Expand Down Expand Up @@ -58,11 +58,11 @@ def perform(self, ui: BaseUi, text_reader: BaseTextReader, *args, **kwargs):
needed_data, rating = self.find_text(self.__text_getter.local_answer)
if self.__min_similarity > rating:
raise TextNotFoundException(self.__min_similarity)
y_cords = [needed_data.coordinates.right_top.y, needed_data.coordinates.right_bottom.y]
x_cords = [needed_data.coordinates.left_top.x, needed_data.coordinates.right_top.x]
y_coords = [needed_data.coordinates.right_top.y, needed_data.coordinates.right_bottom.y]
x_coords = [needed_data.coordinates.left_top.x, needed_data.coordinates.right_top.x]
offset_point = self.__get_local_offset(ui)
x_center = round((x_cords[0] - x_cords[1]) / 2 + x_cords[1]) + offset_point.x
y_center = round((y_cords[0] - y_cords[1]) / 2 + y_cords[1]) + offset_point.y
x_center = round((x_coords[0] - x_coords[1]) / 2 + x_coords[1]) + offset_point.x
y_center = round((y_coords[0] - y_coords[1]) / 2 + y_coords[1]) + offset_point.y
MouseMove(Point(x_center, y_center)).perform(ui)

@property
Expand Down
8 changes: 4 additions & 4 deletions apparser/instructions/ocr/plot_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ def __init__(self, draw: ImageDraw.Draw, color: Tuple[int, int, int, int],

def draw(self, bboxes: list[TextData]):
for data in bboxes:
self.__paint_cords(data)
self.__paint_coords(data)
self.__paint_lines(data)

def __paint_lines(self, data: TextData):
shape = [(data.coordinates.left_top.x, data.coordinates.left_top.y), (data.coordinates.right_bottom.x, data.coordinates.right_bottom.y)]
self.__draw.rectangle(shape, outline=self.__color, width=1)

def __paint_cords(self, data: TextData):
def __paint_coords(self, data: TextData):
y = data.coordinates.left_top.y + self.__text_move.y
if y < 0:
y = data.coordinates.right_bottom.y - self.__text_move.y
Expand All @@ -54,7 +54,7 @@ def __init__(self, text_getter: GetText | None = None,
text_move: Point = Point(0, 20)):
"""Initialize an OCR plotting instruction.

:param text_getter: Instruction used to extract text from the screen. If None use GetText()
:param text_getter: Instruction used to extract text from the screen. If None, use GetText().
:type text_getter: GetText | None
:param color_rgba: RGBA color used for the rendered overlays.
:type color_rgba: tuple[int, int, int, int]
Expand All @@ -72,7 +72,7 @@ def id(self) -> int:

def perform(self, ui: BaseUi, text_reader: BaseTextReader, *args, **kwargs):
self.__text_getter.perform(ui, text_reader)
texts = self.__text_getter.global_answer
texts = self.__text_getter.local_answer
image = self.__text_getter.screenshot
image = Image.fromarray(image)
draw = ImageDraw.Draw(image)
Expand Down
2 changes: 1 addition & 1 deletion apparser/instructions/ocr/print_all_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class PrintAllText(OCRInstruction):
def __init__(self, text_getter: GetText | None = None):
"""Initialize an OCR text printing instruction.

:param text_getter: Instruction used to extract text from the screen. If None use GetText()
:param text_getter: Instruction used to extract text from the screen. If None, use GetText().
:type text_getter: GetText | None
"""
if text_getter is None:
Expand Down
27 changes: 14 additions & 13 deletions apparser/instructions/ocr/text_getter.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,42 +30,43 @@ def __init__(self,
self.__reload_every_try = reload_every_try
self.__local_answer = []
self.__global_answer = []
self.__left_top_point_global = left_top_point
self.__left_top_point_local = left_top_point
self.__screenshot = None

@property
def id(self) -> int:
return 2000

def __text_coordinates_to_local(self, text: TextData) -> TextData:
@staticmethod
def __shift_text_coordinates(text: TextData, offset: Point) -> TextData:
new_coordinates = QuadPoints(
text.coordinates.left_top + self.__left_top_point_global,
text.coordinates.right_top + self.__left_top_point_global,
text.coordinates.right_bottom + self.__left_top_point_global,
text.coordinates.left_bottom + self.__left_top_point_global,
text.coordinates.left_top + offset,
text.coordinates.right_top + offset,
text.coordinates.right_bottom + offset,
text.coordinates.left_bottom + offset,
)
return TextData(text.text, new_coordinates)

def __texts_coordinates_to_local(self, texts: list[TextData]) -> list[TextData]:
def __shift_texts_coordinates(self, texts: list[TextData], offset: Point) -> list[TextData]:
returned_data = []
for text in texts:
returned_data.append(self.__text_coordinates_to_local(text))
returned_data.append(self.__shift_text_coordinates(text, offset))
return returned_data

def perform(self, ui: BaseUi, text_reader: BaseTextReader, *args, **kwargs):
if len(self.__local_answer) != 0 and not self.__reload_every_try:
return
right_bottom_point = ui.point_to_local(ui.point_to_global(self.__right_bottom_point))
self.__left_top_point_global = ui.point_to_local(ui.point_to_global(self.__left_top_point))
self.__left_top_point_local = ui.point_to_local(ui.point_to_global(self.__left_top_point))
left_top_point_global = ui.point_to_global(self.__left_top_point_local)
screen = Image.fromarray(ui.get_screenshot())
screen = screen.crop((self.__left_top_point_global.x, self.__left_top_point_global.y, right_bottom_point.x,
screen = screen.crop((self.__left_top_point_local.x, self.__left_top_point_local.y, right_bottom_point.x,
right_bottom_point.y))
screen = numpy.array(screen)
self.__screenshot = screen
ai_answer = text_reader.read_image(screen)
self.__global_answer = ai_answer.copy()
ai_answer = self.__texts_coordinates_to_local(ai_answer)
self.__local_answer = ai_answer
self.__global_answer = self.__shift_texts_coordinates(ai_answer, left_top_point_global)
self.__local_answer = self.__shift_texts_coordinates(ai_answer, self.__left_top_point_local)

@property
def local_answer(self) -> list[TextData]:
Expand Down
9 changes: 6 additions & 3 deletions apparser/instructions/ui/algorithms/ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@

def _check_instruction(instruction: tuple[int, list[Any]]) -> tuple[int, list[Any]]:
if not isinstance(instruction, tuple):
raise TypeError(f"{instruction} must be tuple")
raise TypeError(f"instruction must be tuple")

if len(instruction) < 2:
raise TypeError(f"instruction must have at least 2 arguments")

instruction_id, instruction_args = instruction
if not isinstance(instruction_id, int):
raise TypeError(f"{instruction_id} must be int")
raise TypeError(f"instruction_id must be int")

if not isinstance(instruction_args, list):
raise TypeError(f"{instruction_args} must be list")
raise TypeError(f"instruction_args must be list")

return instruction_id, instruction_args

Expand Down
9 changes: 6 additions & 3 deletions apparser/instructions/ui/algorithms/names.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@

def _check_instruction(instruction: tuple[str, list[Any]]) -> tuple[str, list[Any]]:
if not isinstance(instruction, tuple):
raise TypeError(f"{instruction} must be tuple")
raise TypeError(f"instruction must be tuple")

if len(instruction) < 2:
raise TypeError(f"instruction must have at least 2 arguments")

instruction_name, instruction_args = instruction
if not isinstance(instruction_name, str):
raise TypeError(f"{instruction_name} must be str")
raise TypeError(f"instruction_name must be str")

if not isinstance(instruction_args, list):
raise TypeError(f"{instruction_args} must be list")
raise TypeError(f"instruction_args must be list")

return instruction_name, instruction_args

Expand Down
4 changes: 1 addition & 3 deletions apparser/instructions/ui/algorithms/unique.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@ def __init__(self,
elif debugger == False:
debugger = None

attributes.reverse()

self.__instructions = instructions
self.__attributes = attributes
self.__attributes = list(reversed(attributes))
self.__debugger = debugger

def __form_args(self, instruction: BaseInstruction) -> dict[str, Any]:
Expand Down
4 changes: 2 additions & 2 deletions apparser/instructions/ui/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ def __init__(self, coordinates: Point | RelativelyPoint,
:type click_type: RightClick | LeftClick
:param mover: Mouse movement strategy used before the click.
:type mover: BaseMover
:raises ValueError: If ``coordinates`` has an invalid type.
:raises TypeError: If ``coordinates`` has an invalid type.
"""
if (not isinstance(coordinates, Point)
and not isinstance(coordinates, RelativelyPoint)):
raise ValueError('coordinates must be Point or RelativelyPoint')
raise TypeError('coordinates must be Point or RelativelyPoint')

self.__click = MouseClick(click_type)
self.__move = MouseMove(coordinates, mover=mover)
Expand Down
6 changes: 3 additions & 3 deletions apparser/movers/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ def __init__(self,
:raises ValueError: If ``duration`` is negative.
"""
if not (isinstance(duration, float) or isinstance(duration, int)):
raise TypeError("Duration must be a number")
raise TypeError("duration must be a number")

if duration < 0:
raise ValueError("Duration must be >= 0")
raise ValueError("duration must be >= 0")

self.__duration = duration

Expand All @@ -31,4 +31,4 @@ def move(self, position: Point):
:type position: Point
"""
pyautogui.moveTo(position.x, position.y,
duration=self.__duration)
duration=self.__duration)
6 changes: 3 additions & 3 deletions apparser/text_readers/readers/compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def _cut_by_coordinates(image: numpy.ndarray, coordinates: QuadPoints) -> numpy.
class CompoundReader(BaseTextReader):
"""Detect text regions and scan each detected image fragment."""

def __init__(self, detector: BaseTextDetector, scanner: BaseTextScanner ):
def __init__(self, detector: BaseTextDetector, scanner: BaseTextScanner):
"""Initialize a compound text reader.

:param detector: Detector used to find text regions in an image.
Expand Down Expand Up @@ -62,7 +62,7 @@ def read_image(self, image: numpy.ndarray) -> list[TextData]:
"""
result = []
for coordinates in self.__detector.read_image(image):
cuted_image = _cut_by_coordinates(image, coordinates)
text = self.__scanner.read_image(cuted_image)
cropped_image = _cut_by_coordinates(image, coordinates)
text = self.__scanner.read_image(cropped_image)
result.append(TextData(text=text, coordinates=coordinates))
return result
Loading
Loading