Skip to content

Commit 555c6cb

Browse files
committed
img - more tests, refactors, fixes. snip DONE
1 parent a780ee7 commit 555c6cb

File tree

7 files changed

+223
-29
lines changed

7 files changed

+223
-29
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pythonpath = [
3838
testpaths = [
3939
"tests",
4040
]
41+
tmp_path_retention_count = 0
4142

4243

4344
[tool.mypy]

src/tapper/helper/_util/image/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ def from_path(pathlike: ImagePathT) -> ImagePixelMatrixT:
4141
return np.asarray(pil_img)
4242

4343

44+
def api_from_path(pathlike: ImagePathT, cache: bool) -> ImagePixelMatrixT:
45+
if not cache:
46+
from_path.cache_clear()
47+
return from_path(pathlike) # type: ignore
48+
49+
4450
def to_pixel_matrix(image: ImageT | None) -> ImagePixelMatrixT | None:
4551
if image is None:
4652
return None
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
from typing import Any
3+
from typing import Callable
4+
5+
import tapper
6+
from tapper.helper._util.image import base
7+
from tapper.helper.model_types import BboxT
8+
from tapper.helper.model_types import ImagePixelMatrixT
9+
from tapper.helper.model_types import XyCoordsT
10+
11+
snip_start_coords: XyCoordsT | None = None
12+
13+
14+
def toggle_snip(
15+
prefix: str | None = None,
16+
bbox_to_name: bool = True,
17+
override_existing: bool = True,
18+
bbox_callback: Callable[[tuple[int, int, int, int]], Any] | None = None,
19+
picture_callback: Callable[[ImagePixelMatrixT], Any] | None = None,
20+
) -> None:
21+
global snip_start_coords
22+
if not snip_start_coords:
23+
start_snip()
24+
else:
25+
stop_coords = tapper.mouse.get_pos()
26+
x1 = min(snip_start_coords[0], stop_coords[0])
27+
x2 = max(snip_start_coords[0], stop_coords[0])
28+
y1 = min(snip_start_coords[1], stop_coords[1])
29+
y2 = max(snip_start_coords[1], stop_coords[1])
30+
snip_start_coords = None
31+
finish_snip_with_callback(
32+
prefix,
33+
bbox_to_name,
34+
(x1, y1, x2, y2),
35+
override_existing,
36+
bbox_callback,
37+
picture_callback,
38+
)
39+
40+
41+
def start_snip() -> None:
42+
global snip_start_coords
43+
snip_start_coords = tapper.mouse.get_pos()
44+
45+
46+
def finish_snip_with_callback(
47+
prefix: str | None = None,
48+
bbox_to_name: bool = True,
49+
bbox: BboxT | None = None,
50+
override_existing: bool = True,
51+
bbox_callback: Callable[[tuple[int, int, int, int]], Any] | None = None,
52+
picture_callback: Callable[[ImagePixelMatrixT], Any] | None = None,
53+
) -> None:
54+
nd_sct, bbox = finish_snip(prefix, bbox, bbox_to_name, override_existing)
55+
if bbox and bbox_callback:
56+
bbox_callback(bbox)
57+
if picture_callback:
58+
picture_callback(nd_sct)
59+
60+
61+
def finish_snip(
62+
prefix: str | None,
63+
bbox: BboxT | None,
64+
bbox_to_name: bool,
65+
override_existing: bool,
66+
) -> tuple[ImagePixelMatrixT, BboxT | None]:
67+
sct = base.get_screenshot_if_none_and_cut(None, bbox)
68+
if prefix is not None:
69+
bbox_str = (
70+
f"-BBOX({bbox[0]},{bbox[1]},{bbox[2]},{bbox[3]})"
71+
if bbox and bbox_to_name
72+
else ""
73+
)
74+
ending = bbox_str + ".png"
75+
full_name = ""
76+
if override_existing or not os.path.exists(prefix + ending):
77+
full_name = prefix + ending
78+
else:
79+
for i in range(1, 100):
80+
potential_name = prefix + f"({i})" + ending
81+
if not os.path.exists(potential_name):
82+
full_name = potential_name
83+
break
84+
base.save_to_disk(sct, full_name)
85+
return sct, bbox

src/tapper/helper/_util/image_util.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,11 @@ def save_to_disk(
253253
sct: ImagePixelMatrixT,
254254
prefix: str,
255255
bbox: BboxT | None,
256-
bbox_in_name: bool,
256+
bbox_to_name: bool,
257257
) -> None:
258258
bbox_str = (
259259
f"-(BBOX_{bbox[0]}_{bbox[1]}_{bbox[2]}_{bbox[3]})"
260-
if bbox and bbox_in_name
260+
if bbox and bbox_to_name
261261
else ""
262262
)
263263
ending = bbox_str + ".png"
@@ -335,5 +335,5 @@ def pixel_find(
335335
first_match = matching_px[0]
336336
x = start_x + first_match[1]
337337
y = start_y + first_match[0]
338-
return x, y
338+
return x, y # noqa - np variables are fine as ints
339339
return None

src/tapper/helper/img.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from tapper.helper._util import image_util as _image_util
99
from tapper.helper._util.image import base as _base_util
1010
from tapper.helper._util.image import find_util as _find_util
11+
from tapper.helper._util.image import snip_util as _snip_util
1112
from tapper.helper.model_types import BboxT
1213
from tapper.helper.model_types import ImagePathT
1314
from tapper.helper.model_types import ImagePixelMatrixT
@@ -32,10 +33,10 @@ def _check_dependencies() -> None:
3233
)
3334

3435

35-
def from_path(pathlike: ImagePathT) -> ImagePixelMatrixT:
36+
def from_path(pathlike: ImagePathT, cache: bool = True) -> ImagePixelMatrixT:
3637
"""Get image from file path."""
3738
_check_dependencies()
38-
return _base_util.from_path(pathlike) # type: ignore
39+
return _base_util.api_from_path(pathlike, cache) # type: ignore
3940

4041

4142
def find(
@@ -62,9 +63,9 @@ def find(
6263

6364

6465
def find_one_of(
65-
targets: list[ImageT]
66-
| tuple[list[ImageT], BboxT]
67-
| list[tuple[ImageT, BboxT | None]],
66+
targets: (
67+
list[ImageT] | tuple[list[ImageT], BboxT] | list[tuple[ImageT, BboxT | None]]
68+
),
6869
outer: str | ImagePixelMatrixT | None = None,
6970
precision: float = STD_PRECISION,
7071
) -> tuple[ImageT, XyCoordsT] | tuple[None, None]:
@@ -109,9 +110,9 @@ def wait_for(
109110

110111

111112
def wait_for_one_of(
112-
targets: list[ImageT]
113-
| tuple[list[ImageT], BboxT]
114-
| list[tuple[ImageT, BboxT | None]],
113+
targets: (
114+
list[ImageT] | tuple[list[ImageT], BboxT] | list[tuple[ImageT, BboxT | None]]
115+
),
115116
timeout: int | float = 5,
116117
interval: float = 0.4,
117118
precision: float = STD_PRECISION,
@@ -156,10 +157,8 @@ def get_find_raw(
156157
) -> tuple[float, XyCoordsT]:
157158
"""
158159
Find an image within a region of the screen or image, and return raw result.
159-
160160
Immediate function, wrap in lambda if setting as action of Tap.
161161
162-
163162
:param target: what to find. Path to an image, or image object(numpy array).
164163
:param bbox: bounding box of where to search in the outer.
165164
:param outer: Optional image in which to find, path or numpy array. If not specified, will search on screen.
@@ -169,11 +168,11 @@ def get_find_raw(
169168
return _find_util.api_find_raw(target, bbox, outer)
170169

171170

172-
# todo add param bool to overwrite existing on save to disk / add (2) etc
173171
def snip(
174172
prefix: str | None = "snip",
175173
bbox_to_name: bool = True,
176-
bbox_callback: Callable[[int, int, int, int], Any] | None = None,
174+
override_existing: bool = True,
175+
bbox_callback: Callable[[tuple[int, int, int, int]], Any] | None = None,
177176
picture_callback: Callable[[ImagePixelMatrixT], Any] | None = None,
178177
) -> Callable[[], None]:
179178
"""
@@ -184,8 +183,9 @@ def snip(
184183
has to be a path, absolute or relative to that dir.
185184
:param bbox_to_name: If true, will include in the name "-(BBOX_{x1}_{y1}_{x2}_{y2})", with actual coordinates.
186185
This is useful for precise-position search with `find` and `wait_for` methods.
186+
:param override_existing: Will override existing file if prefix exists, otherwise will save as prefix(2).png
187187
:param bbox_callback: Action to be applied to bbox coordinates when snip is taken.
188-
This is an alternative to bbox_in_name, if you want to supply it separately later.
188+
This is an alternative to bbox_to_name, if you want to supply it separately later.
189189
:param picture_callback: Action to be applied to the array of resulting picture RGB.
190190
:return: callable toggle, to be set into a Tap
191191
@@ -194,19 +194,25 @@ def snip(
194194
Mouseover a corner of desired snip, click "a", mouseover diagonal corner, click "a",
195195
and you'll get an image with default name and bounding box in the name in the working dir of the script.
196196
197-
{"a": img.snip("image", False, pyperclip.copy)}
198-
Same procedure to get an image, but this will be called "image.png" without bounding box in the name,
199-
instead it will be copied to your clipboard. Package pyperclip if required for this.
197+
{"a": img.snip(prefix=None, bbox_callback=pyperclip.copy)}
198+
This will only copy bounding box to your clipboard. Package pyperclip if required for this.
200199
"""
200+
_check_dependencies()
201201
return partial(
202-
_image_util.toggle_snip, prefix, bbox_to_name, bbox_callback, picture_callback
202+
_snip_util.toggle_snip,
203+
prefix=prefix,
204+
bbox_to_name=bbox_to_name,
205+
override_existing=override_existing,
206+
bbox_callback=bbox_callback,
207+
picture_callback=picture_callback,
203208
)
204209

205210

206211
def get_snip(
207212
bbox: BboxT | None,
208213
prefix: str | None = None,
209-
bbox_in_name: bool = True,
214+
bbox_to_name: bool = True,
215+
override_existing: bool = True,
210216
) -> ImagePixelMatrixT:
211217
"""
212218
Screenshot with specified bounding box, or entire screen. Optionally saves to disk.
@@ -216,15 +222,21 @@ def get_snip(
216222
:param bbox: Bounding box of the screenshot or image. If None, the whole screen or image is snipped.
217223
:param prefix: Optional name, may be a path of image to save, without extension. If not specified,
218224
will not be saved to disk.
219-
:param bbox_in_name: If true, will append to the name -(BBOX_{x1}_{y1}_{x2}_{y2}), with corner coordinates.
225+
:param bbox_to_name: If true, will append to the name "-BBOX({x1},{y1},{x2},{y2})", with corner coordinates.
226+
:param override_existing: Will override existing file if prefix exists, otherwise will save as prefix(2).png
220227
:return: Resulting image RGB, transformed to numpy array.
221228
222229
Usage:
223230
my_pic = img.get_snip(bbox=(100, 100, 200, 400))
224231
...
225232
img.wait_for(my_pic)
226233
"""
227-
return _image_util.finish_snip(prefix, bbox, bbox_in_name)[0]
234+
return _snip_util.finish_snip(
235+
bbox=bbox,
236+
prefix=prefix,
237+
bbox_to_name=bbox_to_name,
238+
override_existing=override_existing,
239+
)[0]
228240

229241

230242
def pixel_info(

tests/tapper/helper/image/test_img.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
def mock_get_sct() -> None:
2727
with patch(
2828
"tapper.helper._util.image.base.get_screenshot_if_none_and_cut"
29-
) as mock_get_sct:
30-
yield mock_get_sct
29+
) as mock_sct:
30+
yield mock_sct
3131

3232

3333
class TestFind:
Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,97 @@
1+
import os
2+
import random
3+
import shutil
4+
from pathlib import Path
5+
from string import ascii_uppercase
6+
from unittest.mock import call
7+
from unittest.mock import MagicMock
8+
from unittest.mock import patch
9+
10+
import img_test_util
11+
import numpy
12+
import pytest
13+
from tapper.helper import img
14+
15+
absolutes = img_test_util.absolutes()
16+
17+
18+
@pytest.fixture
19+
def mock_get_sct() -> MagicMock:
20+
with patch(
21+
"tapper.helper._util.image.base.get_screenshot_if_none_and_cut"
22+
) as mock_sct:
23+
yield mock_sct
24+
25+
26+
@pytest.fixture
27+
def temp_dir(tmpdir_factory) -> Path:
28+
temp_name = "".join(random.choice(ascii_uppercase) for i in range(12))
29+
my_tmpdir = tmpdir_factory.mktemp(temp_name)
30+
yield my_tmpdir
31+
shutil.rmtree(str(my_tmpdir))
32+
33+
34+
@pytest.fixture
35+
def mock_save_to_disk() -> MagicMock:
36+
with patch("tapper.helper._util.image.base.save_to_disk") as mock_save:
37+
yield mock_save
38+
39+
40+
@pytest.fixture
41+
def mock_mouse_pos() -> MagicMock:
42+
with patch("tapper.mouse.get_pos") as mock_mouse_get_pos:
43+
yield mock_mouse_get_pos
44+
145

246
class TestSnip:
3-
def test_simplest(self) -> None:
4-
pass
47+
def test_simplest(self, mock_get_sct) -> None:
48+
mock_get_sct.return_value = absolutes
49+
snipped = img.get_snip(None)
50+
assert numpy.array_equal(snipped, absolutes)
51+
52+
def test_saved_image_same_as_on_disk(self, temp_dir, mock_get_sct) -> None:
53+
mock_get_sct.return_value = absolutes
54+
get_name = lambda name: str(Path(temp_dir / name))
55+
img.get_snip(bbox=None, prefix=get_name("qwe"))
56+
on_disk = img.from_path(get_name("qwe.png"), cache=False)
57+
assert numpy.array_equal(on_disk, absolutes)
58+
59+
def test_bbox_to_name(self, mock_get_sct, mock_save_to_disk) -> None:
60+
mock_get_sct.return_value = absolutes
61+
img.get_snip(bbox=(0, 0, 20, 20), prefix="qwe", bbox_to_name=True)
62+
assert mock_save_to_disk.call_count == 1
63+
assert mock_save_to_disk.call_args == call(absolutes, "qwe-BBOX(0,0,20,20).png")
64+
65+
def test_no_override_creates_different_file(self, temp_dir, mock_get_sct) -> None:
66+
mock_get_sct.return_value = absolutes
67+
get_name = lambda name: str(Path(temp_dir / name))
68+
img.get_snip(bbox=None, prefix=get_name("qwe"))
69+
on_disk_0 = img.from_path(get_name("qwe.png"), cache=False)
70+
img.get_snip(bbox=None, prefix=get_name("qwe"), override_existing=False)
71+
on_disk_1 = img.from_path(get_name("qwe(1).png"), cache=False)
72+
assert numpy.array_equal(on_disk_0, on_disk_1)
73+
assert not os.path.exists(get_name("qwe(2).png"))
74+
75+
def test_override(self, temp_dir, mock_get_sct) -> None:
76+
get_name = lambda name: str(Path(temp_dir / name))
77+
mock_get_sct.return_value = absolutes
78+
img.get_snip(bbox=None, prefix=get_name("qwe"))
79+
on_disk_0 = img.from_path(get_name("qwe.png"), cache=False)
80+
81+
mock_get_sct.return_value = img_test_util.btn_yellow()
82+
img.get_snip(bbox=None, prefix=get_name("qwe"), override_existing=True)
83+
on_disk_1 = img.from_path(get_name("qwe.png"), cache=False)
84+
assert not os.path.exists(get_name("qwe(1).png"))
85+
assert len([name for name in os.listdir(temp_dir)]) == 1
86+
assert numpy.array_equal(on_disk_0, absolutes)
87+
assert numpy.array_equal(on_disk_1, img_test_util.btn_yellow())
588

6-
def test_saved_image_same_as_on_disk(self) -> None:
7-
pass
89+
def test_bbox_is_correct(
90+
self, mock_get_sct, mock_save_to_disk, mock_mouse_pos
91+
) -> None:
92+
snip_fn = img.snip()
93+
mock_mouse_pos.return_value = 100, 450
94+
snip_fn()
95+
mock_mouse_pos.return_value = 300, 0
96+
snip_fn()
97+
assert mock_get_sct.call_args == call(None, (100, 0, 300, 450))

0 commit comments

Comments
 (0)