imdiff 2.0.0__tar.gz → 2.1.0__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.
- {imdiff-2.0.0 → imdiff-2.1.0}/PKG-INFO +53 -8
- {imdiff-2.0.0 → imdiff-2.1.0}/README.md +51 -2
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/__init__.py +1 -1
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/dir_diff.py +11 -5
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/image_diff.py +11 -5
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/main.py +50 -6
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/directory_comparator.py +6 -3
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/_types.py +1 -1
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/dir_diff_main_window.py +8 -2
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/file_list_frame.py +4 -2
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/image_diff_main_window.py +8 -2
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/image_frame.py +4 -2
- imdiff-2.1.0/imdiff/image_comparator.py +266 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/version.py +1 -1
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/PKG-INFO +53 -8
- {imdiff-2.0.0 → imdiff-2.1.0}/pyproject.toml +9 -6
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_cli_diff.py +2 -2
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_cli_main.py +40 -9
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_directory_comparator.py +17 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_image_comparator.py +140 -1
- imdiff-2.0.0/imdiff/image_comparator.py +0 -221
- {imdiff-2.0.0 → imdiff-2.1.0}/LICENSE +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/__main__.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/_compat.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/_types.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/__init__.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/_gui_runtime.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/__init__.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/image_canvas.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/image_scaling.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/status_bar.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/transient_menu.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/zoom_menu.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/list_files.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/util.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/SOURCES.txt +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/dependency_links.txt +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/entry_points.txt +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/requires.txt +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/top_level.txt +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/setup.cfg +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_file_list_frame.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_image_scaling.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_list_files.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_module_main.py +0 -0
- {imdiff-2.0.0 → imdiff-2.1.0}/test/test_util.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imdiff
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Compare image files in different directories
|
|
5
5
|
Author-email: "John T. Goetz" <theodore.goetz@gmail.com>
|
|
6
6
|
Project-URL: homepage, https://gitlab.com/johngoetz/imdiff
|
|
7
7
|
Keywords: image-diff,directory-comparison,visual-regression,golden-files,test-artifacts,qa
|
|
8
|
-
Classifier: Development Status ::
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
9
|
Classifier: Environment :: Console
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Intended Audience :: End Users/Desktop
|
|
@@ -14,10 +14,6 @@ Classifier: Natural Language :: English
|
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
17
|
Classifier: Topic :: Multimedia :: Graphics :: Viewers
|
|
22
18
|
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
23
19
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
@@ -87,15 +83,64 @@ Compare two directories and print a CI-friendly summary:
|
|
|
87
83
|
python -m imdiff --summary tests/baseline-images tests/generated-images
|
|
88
84
|
```
|
|
89
85
|
|
|
86
|
+
By default the normalized RMSE is measured over the whole image, which tolerates
|
|
87
|
+
the many tiny GPU/driver rendering variations that show up across large images.
|
|
88
|
+
To instead catch small but concentrated differences, select a sliding-window
|
|
89
|
+
size with `--window`; the reported score becomes the worst-scoring window of that
|
|
90
|
+
size:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -m imdiff --window medium tests/baseline-images tests/generated-images
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The available window sizes are `global` (the default, whole image), `small`
|
|
97
|
+
(32x32), `medium` (64x64), `large` (128x128), and `xlarge` (256x256). You can
|
|
98
|
+
also pass an explicit pixel size as `WIDTH,HEIGHT`, for example
|
|
99
|
+
`--window 1024,1024`, when none of the presets fit. A window larger than the
|
|
100
|
+
image is clamped to the image size. The flag applies to both the interactive
|
|
101
|
+
windows and `--summary` mode.
|
|
102
|
+
|
|
90
103
|
When the GUI cannot be imported or Tk cannot start, the default interactive
|
|
91
104
|
command warns on `stderr` and continues in the same summary mode used by
|
|
92
105
|
`--summary`. This makes headless CI environments safe without changing the CLI
|
|
93
106
|
command line.
|
|
94
107
|
|
|
108
|
+
## Programmatic comparison
|
|
109
|
+
|
|
110
|
+
The same comparison core is available to other Python code, such as test
|
|
111
|
+
harnesses that compare a captured screenshot against a stored reference:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from imdiff import ImageComparator, RmseWindow
|
|
115
|
+
|
|
116
|
+
comparator = ImageComparator(reference_png, actual_png, RmseWindow.Medium)
|
|
117
|
+
|
|
118
|
+
status = comparator.diff_info # 'identical', 'missing', 'different-size', ...
|
|
119
|
+
if isinstance(status, float): # a same-size pair: status is the normalized RMSE
|
|
120
|
+
assert status <= 0.03, f'normalized RMSE {status:g} exceeded threshold'
|
|
121
|
+
|
|
122
|
+
comparator.diff # grayscale difference image (PIL.Image)
|
|
123
|
+
comparator.high_contrast_diff # histogram-stretched difference image
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`diff_info` returns a symbolic status string for presence, size, and load
|
|
127
|
+
problems, or the normalized RMSE (range `[0, 1]`) when both images decode at the
|
|
128
|
+
same size. The metric honors the `window` passed to the constructor: `Global`
|
|
129
|
+
scores the whole image, while any other `RmseWindow` reports the largest score
|
|
130
|
+
found across every window position of that size. The `window` may also be an
|
|
131
|
+
explicit `(width, height)` tuple instead of a preset. A window larger than the
|
|
132
|
+
image is clamped to the image size. Decoded images and derived results are
|
|
133
|
+
cached on first use and recomputed after `copy_left_to_right`,
|
|
134
|
+
`copy_right_to_left`, `delete_files`, or an explicit `clear`.
|
|
135
|
+
|
|
136
|
+
The `normalized_rmse(left_data, right_data, window)` function is also exported
|
|
137
|
+
for callers that already hold equally sized RGBA arrays.
|
|
138
|
+
|
|
95
139
|
## Repository Layout
|
|
96
140
|
|
|
97
|
-
- `imdiff/image_comparator.py`
|
|
98
|
-
|
|
141
|
+
- `imdiff/image_comparator.py` is the comparison core: the `RmseWindow` sizes,
|
|
142
|
+
the `normalized_rmse` metric, and the lazy `ImageComparator` model shared by
|
|
143
|
+
the CLI and GUI.
|
|
99
144
|
- `imdiff/list_files.py` pairs files across directory trees.
|
|
100
145
|
- `imdiff/cli/` contains the command-line entry points and summary printing.
|
|
101
146
|
- `imdiff/gui/` contains the Tk windows, canvases, menus, and directory browser.
|
|
@@ -51,15 +51,64 @@ Compare two directories and print a CI-friendly summary:
|
|
|
51
51
|
python -m imdiff --summary tests/baseline-images tests/generated-images
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
By default the normalized RMSE is measured over the whole image, which tolerates
|
|
55
|
+
the many tiny GPU/driver rendering variations that show up across large images.
|
|
56
|
+
To instead catch small but concentrated differences, select a sliding-window
|
|
57
|
+
size with `--window`; the reported score becomes the worst-scoring window of that
|
|
58
|
+
size:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m imdiff --window medium tests/baseline-images tests/generated-images
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The available window sizes are `global` (the default, whole image), `small`
|
|
65
|
+
(32x32), `medium` (64x64), `large` (128x128), and `xlarge` (256x256). You can
|
|
66
|
+
also pass an explicit pixel size as `WIDTH,HEIGHT`, for example
|
|
67
|
+
`--window 1024,1024`, when none of the presets fit. A window larger than the
|
|
68
|
+
image is clamped to the image size. The flag applies to both the interactive
|
|
69
|
+
windows and `--summary` mode.
|
|
70
|
+
|
|
54
71
|
When the GUI cannot be imported or Tk cannot start, the default interactive
|
|
55
72
|
command warns on `stderr` and continues in the same summary mode used by
|
|
56
73
|
`--summary`. This makes headless CI environments safe without changing the CLI
|
|
57
74
|
command line.
|
|
58
75
|
|
|
76
|
+
## Programmatic comparison
|
|
77
|
+
|
|
78
|
+
The same comparison core is available to other Python code, such as test
|
|
79
|
+
harnesses that compare a captured screenshot against a stored reference:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from imdiff import ImageComparator, RmseWindow
|
|
83
|
+
|
|
84
|
+
comparator = ImageComparator(reference_png, actual_png, RmseWindow.Medium)
|
|
85
|
+
|
|
86
|
+
status = comparator.diff_info # 'identical', 'missing', 'different-size', ...
|
|
87
|
+
if isinstance(status, float): # a same-size pair: status is the normalized RMSE
|
|
88
|
+
assert status <= 0.03, f'normalized RMSE {status:g} exceeded threshold'
|
|
89
|
+
|
|
90
|
+
comparator.diff # grayscale difference image (PIL.Image)
|
|
91
|
+
comparator.high_contrast_diff # histogram-stretched difference image
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`diff_info` returns a symbolic status string for presence, size, and load
|
|
95
|
+
problems, or the normalized RMSE (range `[0, 1]`) when both images decode at the
|
|
96
|
+
same size. The metric honors the `window` passed to the constructor: `Global`
|
|
97
|
+
scores the whole image, while any other `RmseWindow` reports the largest score
|
|
98
|
+
found across every window position of that size. The `window` may also be an
|
|
99
|
+
explicit `(width, height)` tuple instead of a preset. A window larger than the
|
|
100
|
+
image is clamped to the image size. Decoded images and derived results are
|
|
101
|
+
cached on first use and recomputed after `copy_left_to_right`,
|
|
102
|
+
`copy_right_to_left`, `delete_files`, or an explicit `clear`.
|
|
103
|
+
|
|
104
|
+
The `normalized_rmse(left_data, right_data, window)` function is also exported
|
|
105
|
+
for callers that already hold equally sized RGBA arrays.
|
|
106
|
+
|
|
59
107
|
## Repository Layout
|
|
60
108
|
|
|
61
|
-
- `imdiff/image_comparator.py`
|
|
62
|
-
|
|
109
|
+
- `imdiff/image_comparator.py` is the comparison core: the `RmseWindow` sizes,
|
|
110
|
+
the `normalized_rmse` metric, and the lazy `ImageComparator` model shared by
|
|
111
|
+
the CLI and GUI.
|
|
63
112
|
- `imdiff/list_files.py` pairs files across directory trees.
|
|
64
113
|
- `imdiff/cli/` contains the command-line entry points and summary printing.
|
|
65
114
|
- `imdiff/gui/` contains the Tk windows, canvases, menus, and directory browser.
|
|
@@ -7,7 +7,7 @@ import importlib
|
|
|
7
7
|
import pathlib
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
from ..image_comparator import ImageComparator
|
|
10
|
+
from ..image_comparator import ImageComparator, RmseWindow, Window
|
|
11
11
|
from ..list_files import is_image, list_files
|
|
12
12
|
from ._gui_runtime import (
|
|
13
13
|
normalize_gui_import_error,
|
|
@@ -16,7 +16,9 @@ from ._gui_runtime import (
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def app(
|
|
19
|
-
leftdir: Optional[pathlib.Path] = None,
|
|
19
|
+
leftdir: Optional[pathlib.Path] = None,
|
|
20
|
+
rightdir: Optional[pathlib.Path] = None,
|
|
21
|
+
window: Window = RmseWindow.Global,
|
|
20
22
|
) -> None:
|
|
21
23
|
"""Open the directory comparison window for the requested roots."""
|
|
22
24
|
try:
|
|
@@ -28,7 +30,7 @@ def app(
|
|
|
28
30
|
raise gui_error from error
|
|
29
31
|
|
|
30
32
|
try:
|
|
31
|
-
win = DirDiffMainWindow()
|
|
33
|
+
win = DirDiffMainWindow(window=window)
|
|
32
34
|
except Exception as error:
|
|
33
35
|
gui_error = normalize_gui_startup_error(error)
|
|
34
36
|
if gui_error is not None:
|
|
@@ -39,7 +41,11 @@ def app(
|
|
|
39
41
|
win.mainloop()
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def print_diff(
|
|
44
|
+
def print_diff(
|
|
45
|
+
leftdir: pathlib.Path,
|
|
46
|
+
rightdir: pathlib.Path,
|
|
47
|
+
window: Window = RmseWindow.Global,
|
|
48
|
+
) -> int:
|
|
43
49
|
"""Print a summary of directory differences and return the diff count."""
|
|
44
50
|
ndiffs = 0
|
|
45
51
|
assert leftdir.is_dir() and rightdir.is_dir(), 'Items must both be directories'
|
|
@@ -56,7 +62,7 @@ def print_diff(leftdir: pathlib.Path, rightdir: pathlib.Path) -> int:
|
|
|
56
62
|
ndiffs += 1
|
|
57
63
|
print(f'Only in {left.parent}: {subpath}')
|
|
58
64
|
elif is_image(left) and is_image(right):
|
|
59
|
-
icmp = ImageComparator(left, right)
|
|
65
|
+
icmp = ImageComparator(left, right, window)
|
|
60
66
|
diff_info = icmp.diff_info
|
|
61
67
|
if diff_info != 'identical':
|
|
62
68
|
if isinstance(diff_info, str):
|
|
@@ -7,7 +7,7 @@ import importlib
|
|
|
7
7
|
import pathlib
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
from ..image_comparator import ImageComparator
|
|
10
|
+
from ..image_comparator import ImageComparator, RmseWindow, Window
|
|
11
11
|
from ..list_files import is_image
|
|
12
12
|
from ._gui_runtime import (
|
|
13
13
|
normalize_gui_import_error,
|
|
@@ -16,7 +16,9 @@ from ._gui_runtime import (
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def app(
|
|
19
|
-
left: Optional[pathlib.Path] = None,
|
|
19
|
+
left: Optional[pathlib.Path] = None,
|
|
20
|
+
right: Optional[pathlib.Path] = None,
|
|
21
|
+
window: Window = RmseWindow.Global,
|
|
20
22
|
) -> None:
|
|
21
23
|
"""Open the single-image comparison window for the requested files."""
|
|
22
24
|
try:
|
|
@@ -28,7 +30,7 @@ def app(
|
|
|
28
30
|
raise gui_error from error
|
|
29
31
|
|
|
30
32
|
try:
|
|
31
|
-
win = ImageDiffMainWindow()
|
|
33
|
+
win = ImageDiffMainWindow(window=window)
|
|
32
34
|
except Exception as error:
|
|
33
35
|
gui_error = normalize_gui_startup_error(error)
|
|
34
36
|
if gui_error is not None:
|
|
@@ -39,14 +41,18 @@ def app(
|
|
|
39
41
|
win.mainloop()
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def print_diff(
|
|
44
|
+
def print_diff(
|
|
45
|
+
left: pathlib.Path,
|
|
46
|
+
right: pathlib.Path,
|
|
47
|
+
window: Window = RmseWindow.Global,
|
|
48
|
+
) -> int:
|
|
43
49
|
"""Print a summary of a single file comparison and return the diff count."""
|
|
44
50
|
ndiffs = 0
|
|
45
51
|
assert left.is_file() and right.is_file(), 'Items must both be files'
|
|
46
52
|
|
|
47
53
|
rmse_threshold = 0.02
|
|
48
54
|
if is_image(left) and is_image(right):
|
|
49
|
-
icmp = ImageComparator(left, right)
|
|
55
|
+
icmp = ImageComparator(left, right, window)
|
|
50
56
|
diff_info = icmp.diff_info
|
|
51
57
|
if diff_info != 'identical':
|
|
52
58
|
ndiffs += 1
|
|
@@ -7,19 +7,51 @@ import pathlib
|
|
|
7
7
|
import sys
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
+
from ..image_comparator import RmseWindow, Window
|
|
10
11
|
from ..version import __version__
|
|
11
12
|
from . import dir_diff, image_diff
|
|
12
13
|
from ._gui_runtime import GuiUnavailableError, format_gui_warning
|
|
13
14
|
|
|
15
|
+
# CLI tokens map to the preset window sizes; the keys are what users type.
|
|
16
|
+
WINDOW_CHOICES = {window.name.lower(): window for window in RmseWindow}
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
class ParsedArgs(argparse.Namespace):
|
|
16
20
|
"""Typed namespace so downstream code can treat parsed paths precisely."""
|
|
17
21
|
|
|
18
22
|
summary: bool = False
|
|
23
|
+
window: Window = RmseWindow.Global
|
|
19
24
|
left: pathlib.Path = pathlib.Path('.')
|
|
20
25
|
right: pathlib.Path = pathlib.Path('.')
|
|
21
26
|
|
|
22
27
|
|
|
28
|
+
def parse_window(value: str) -> Window:
|
|
29
|
+
"""Resolve a ``--window`` token to a preset or an explicit pixel size.
|
|
30
|
+
|
|
31
|
+
Accepts a preset name (``global``, ``small``, ...) or two positive integers
|
|
32
|
+
such as ``1024,1024`` for an arbitrary window. Anything else is reported as
|
|
33
|
+
an argparse error so the user sees the valid forms.
|
|
34
|
+
"""
|
|
35
|
+
name = value.strip().lower()
|
|
36
|
+
if name in WINDOW_CHOICES:
|
|
37
|
+
return WINDOW_CHOICES[name]
|
|
38
|
+
|
|
39
|
+
parts = value.split(',')
|
|
40
|
+
if len(parts) == 2:
|
|
41
|
+
try:
|
|
42
|
+
width, height = (int(part) for part in parts)
|
|
43
|
+
except ValueError:
|
|
44
|
+
width = height = -1
|
|
45
|
+
if width > 0 and height > 0:
|
|
46
|
+
return (width, height)
|
|
47
|
+
|
|
48
|
+
presets = ', '.join(WINDOW_CHOICES)
|
|
49
|
+
raise argparse.ArgumentTypeError(
|
|
50
|
+
f'invalid window {value!r}; expected one of {presets} '
|
|
51
|
+
+ 'or two positive integers like 1024,1024'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
23
55
|
def parse_args() -> ParsedArgs:
|
|
24
56
|
"""Parse the shared CLI flags for file and directory comparison modes."""
|
|
25
57
|
parser = argparse.ArgumentParser(
|
|
@@ -39,6 +71,17 @@ def parse_args() -> ParsedArgs:
|
|
|
39
71
|
'Exits with non-zero if there are differences.'
|
|
40
72
|
),
|
|
41
73
|
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
'--window',
|
|
76
|
+
type=parse_window,
|
|
77
|
+
default=RmseWindow.Global,
|
|
78
|
+
metavar='{global,small,medium,large,xlarge | WIDTH,HEIGHT}',
|
|
79
|
+
help=(
|
|
80
|
+
'Normalized RMSE window. "global" scores the whole image; a preset '
|
|
81
|
+
'or an explicit WIDTH,HEIGHT (e.g. 1024,1024) reports the '
|
|
82
|
+
'worst-scoring window of that size.'
|
|
83
|
+
),
|
|
84
|
+
)
|
|
42
85
|
parser.add_argument(
|
|
43
86
|
'left',
|
|
44
87
|
default='.',
|
|
@@ -57,20 +100,21 @@ def parse_args() -> ParsedArgs:
|
|
|
57
100
|
def main() -> Optional[int]:
|
|
58
101
|
"""Dispatch to GUI or summary workflows, with summary fallback for headless use."""
|
|
59
102
|
args = parse_args()
|
|
103
|
+
window = args.window
|
|
60
104
|
if args.left.is_dir():
|
|
61
105
|
if not args.right.is_dir():
|
|
62
106
|
raise NotADirectoryError(
|
|
63
107
|
'Paths must be either both image files or both directories'
|
|
64
108
|
)
|
|
65
109
|
if args.summary:
|
|
66
|
-
ndiffs = dir_diff.print_diff(args.left, args.right)
|
|
110
|
+
ndiffs = dir_diff.print_diff(args.left, args.right, window)
|
|
67
111
|
return int(ndiffs > 0)
|
|
68
112
|
try:
|
|
69
|
-
dir_diff.app(args.left, args.right)
|
|
113
|
+
dir_diff.app(args.left, args.right, window)
|
|
70
114
|
return None
|
|
71
115
|
except GuiUnavailableError as error:
|
|
72
116
|
print(format_gui_warning(error), file=sys.stderr)
|
|
73
|
-
ndiffs = dir_diff.print_diff(args.left, args.right)
|
|
117
|
+
ndiffs = dir_diff.print_diff(args.left, args.right, window)
|
|
74
118
|
return int(ndiffs > 0)
|
|
75
119
|
|
|
76
120
|
if args.left.is_file():
|
|
@@ -78,14 +122,14 @@ def main() -> Optional[int]:
|
|
|
78
122
|
'Paths must be either both image files or both directories'
|
|
79
123
|
)
|
|
80
124
|
if args.summary:
|
|
81
|
-
ndiffs = image_diff.print_diff(args.left, args.right)
|
|
125
|
+
ndiffs = image_diff.print_diff(args.left, args.right, window)
|
|
82
126
|
return int(ndiffs > 0)
|
|
83
127
|
try:
|
|
84
|
-
image_diff.app(args.left, args.right)
|
|
128
|
+
image_diff.app(args.left, args.right, window)
|
|
85
129
|
return None
|
|
86
130
|
except GuiUnavailableError as error:
|
|
87
131
|
print(format_gui_warning(error), file=sys.stderr)
|
|
88
|
-
ndiffs = image_diff.print_diff(args.left, args.right)
|
|
132
|
+
ndiffs = image_diff.print_diff(args.left, args.right, window)
|
|
89
133
|
return int(ndiffs > 0)
|
|
90
134
|
|
|
91
135
|
raise OSError(f'{args.left} is not a file or directory')
|
|
@@ -8,12 +8,14 @@ import filecmp
|
|
|
8
8
|
import pathlib
|
|
9
9
|
|
|
10
10
|
from ._types import PathInput
|
|
11
|
-
from .image_comparator import ImageComparator
|
|
11
|
+
from .image_comparator import ImageComparator, RmseWindow, Window, check_window
|
|
12
12
|
from .list_files import is_image, list_files
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def dir_diff(
|
|
16
|
-
leftdir: PathInput,
|
|
16
|
+
leftdir: PathInput,
|
|
17
|
+
rightdir: PathInput,
|
|
18
|
+
window: Window = RmseWindow.Global,
|
|
17
19
|
) -> collections.defaultdict[str, list[str]]:
|
|
18
20
|
"""Group directory entries by comparison status for summary-style reporting.
|
|
19
21
|
|
|
@@ -22,6 +24,7 @@ def dir_diff(
|
|
|
22
24
|
It mirrors the GUI thresholds so command-line output and interactive review
|
|
23
25
|
describe the same files as identical, similar, or different.
|
|
24
26
|
"""
|
|
27
|
+
check_window(window)
|
|
25
28
|
leftdir_path = pathlib.Path(leftdir)
|
|
26
29
|
rightdir_path = pathlib.Path(rightdir)
|
|
27
30
|
if not leftdir_path.is_dir():
|
|
@@ -42,7 +45,7 @@ def dir_diff(
|
|
|
42
45
|
elif filecmp.cmp(left, right, shallow=False):
|
|
43
46
|
dinfo['identical'].append(subpath)
|
|
44
47
|
elif is_image(left) and is_image(right):
|
|
45
|
-
icmp = ImageComparator(left, right)
|
|
48
|
+
icmp = ImageComparator(left, right, window)
|
|
46
49
|
diff_info = icmp.diff_info
|
|
47
50
|
if isinstance(diff_info, str):
|
|
48
51
|
dinfo[diff_info].append(subpath)
|
|
@@ -13,7 +13,7 @@ from .image_scaling import ImageScaling
|
|
|
13
13
|
|
|
14
14
|
CanvasName = Literal['left', 'right', 'diff']
|
|
15
15
|
ImageData = Optional[Image.Image]
|
|
16
|
-
Event = tk.Event[tk.Misc]
|
|
16
|
+
Event = tk.Event # [tk.Misc]
|
|
17
17
|
SelectCallback = Callable[[Optional[ImageComparator]], None]
|
|
18
18
|
UpdateItemPath = Union[str, pathlib.Path, None]
|
|
19
19
|
UpdateItemCallback = Callable[[UpdateItemPath, Optional[ImageComparator]], None]
|
|
@@ -11,7 +11,7 @@ from typing import Optional, final
|
|
|
11
11
|
import ttkbootstrap as ttk
|
|
12
12
|
|
|
13
13
|
from .._compat import override
|
|
14
|
-
from ..image_comparator import ImageComparator
|
|
14
|
+
from ..image_comparator import ImageComparator, RmseWindow, Window
|
|
15
15
|
from ._types import Event, UpdateItemPath
|
|
16
16
|
from .file_list_frame import FileListFrame
|
|
17
17
|
from .image_frame import ImageFrame
|
|
@@ -24,7 +24,12 @@ log = logging.getLogger(__name__)
|
|
|
24
24
|
class DirDiffMainWindow(ttk.Window):
|
|
25
25
|
"""Combine the directory browser, shared image frame, and status bar."""
|
|
26
26
|
|
|
27
|
-
def __init__(
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
left_label: str = 'left',
|
|
30
|
+
right_label: str = 'right',
|
|
31
|
+
window: Window = RmseWindow.Global,
|
|
32
|
+
) -> None:
|
|
28
33
|
super().__init__(title='Image Comparator', themename='sandstone')
|
|
29
34
|
self.pane_window = tk.PanedWindow(self, orient=tk.HORIZONTAL, sashwidth=8)
|
|
30
35
|
self.file_list_frame = FileListFrame(
|
|
@@ -32,6 +37,7 @@ class DirDiffMainWindow(ttk.Window):
|
|
|
32
37
|
on_select=self.on_select,
|
|
33
38
|
left_label=left_label,
|
|
34
39
|
right_label=right_label,
|
|
40
|
+
window=window,
|
|
35
41
|
)
|
|
36
42
|
self.image_frame = ImageFrame(self.pane_window, update_item=self.update_item)
|
|
37
43
|
self.image_frame.add_standard_operate_buttons(
|
|
@@ -15,7 +15,7 @@ from typing import Generic, Optional, TypeVar, final
|
|
|
15
15
|
|
|
16
16
|
import ttkbootstrap as ttk
|
|
17
17
|
|
|
18
|
-
from ..image_comparator import ImageComparator
|
|
18
|
+
from ..image_comparator import ImageComparator, RmseWindow, Window
|
|
19
19
|
from ..list_files import is_image, list_files
|
|
20
20
|
from ..util import Size, separate_thread
|
|
21
21
|
from ._types import Event, SelectCallback
|
|
@@ -77,12 +77,14 @@ class FileListFrame(ttk.Frame):
|
|
|
77
77
|
on_select: Optional[SelectCallback] = None,
|
|
78
78
|
left_label: str = 'left',
|
|
79
79
|
right_label: str = 'right',
|
|
80
|
+
window: Window = RmseWindow.Global,
|
|
80
81
|
) -> None:
|
|
81
82
|
super().__init__(parent)
|
|
82
83
|
|
|
83
84
|
self.on_select_command = on_select
|
|
84
85
|
self.left_label = left_label
|
|
85
86
|
self.right_label = right_label
|
|
87
|
+
self.window = window
|
|
86
88
|
|
|
87
89
|
self.files: dict[str, ImageComparator] = {}
|
|
88
90
|
self.file_entries: list[str] = []
|
|
@@ -259,7 +261,7 @@ class FileListFrame(ttk.Frame):
|
|
|
259
261
|
if self.leftdir is not None and self.rightdir is not None:
|
|
260
262
|
for file_path, left, right in list_files(self.leftdir, self.rightdir):
|
|
261
263
|
if is_image(left) or is_image(right):
|
|
262
|
-
file_cmp = ImageComparator(left, right)
|
|
264
|
+
file_cmp = ImageComparator(left, right, self.window)
|
|
263
265
|
self.files[file_path] = file_cmp
|
|
264
266
|
self.file_queue.put(file_path)
|
|
265
267
|
elif self.text_file_queue.status == IterableQueue.Status.Filling:
|
|
@@ -9,6 +9,7 @@ import tkinter as tk
|
|
|
9
9
|
from typing import Optional, final
|
|
10
10
|
|
|
11
11
|
from .._compat import override
|
|
12
|
+
from ..image_comparator import RmseWindow, Window
|
|
12
13
|
from ._types import Event
|
|
13
14
|
from .image_frame import ImageFrame
|
|
14
15
|
from .status_bar import StatusBar
|
|
@@ -20,11 +21,16 @@ log = logging.getLogger(__name__)
|
|
|
20
21
|
class ImageDiffMainWindow(tk.Tk):
|
|
21
22
|
"""Compose the shared image frame and status bar for single-file use."""
|
|
22
23
|
|
|
23
|
-
def __init__(
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
left_label: str = 'left',
|
|
27
|
+
right_label: str = 'right',
|
|
28
|
+
window: Window = RmseWindow.Global,
|
|
29
|
+
) -> None:
|
|
24
30
|
super().__init__()
|
|
25
31
|
self.title('Image Comparator')
|
|
26
32
|
self.image_frame = ImageFrame(
|
|
27
|
-
self, left_label=left_label, right_label=right_label
|
|
33
|
+
self, left_label=left_label, right_label=right_label, window=window
|
|
28
34
|
)
|
|
29
35
|
self.image_frame.add_standard_operate_buttons()
|
|
30
36
|
self.status_bar = StatusBar(self)
|
|
@@ -10,7 +10,7 @@ from typing import Optional, final
|
|
|
10
10
|
|
|
11
11
|
import ttkbootstrap as ttk
|
|
12
12
|
|
|
13
|
-
from ..image_comparator import ImageComparator
|
|
13
|
+
from ..image_comparator import ImageComparator, RmseWindow, Window
|
|
14
14
|
from ..util import Coordinates, Size, separate_thread
|
|
15
15
|
from ._types import ActionCallback, CanvasName, Event, ImageData, UpdateItemCallback
|
|
16
16
|
from .image_canvas import ImageCanvas
|
|
@@ -35,9 +35,11 @@ class ImageFrame(ttk.Frame):
|
|
|
35
35
|
update_item: Optional[UpdateItemCallback] = None,
|
|
36
36
|
left_label: str = 'left',
|
|
37
37
|
right_label: str = 'right',
|
|
38
|
+
window: Window = RmseWindow.Global,
|
|
38
39
|
) -> None:
|
|
39
40
|
super().__init__(parent)
|
|
40
41
|
self.update_item_command = update_item
|
|
42
|
+
self.window = window
|
|
41
43
|
self.left_label = tk.StringVar(value=left_label)
|
|
42
44
|
self.right_label = tk.StringVar(value=right_label)
|
|
43
45
|
|
|
@@ -343,7 +345,7 @@ class ImageFrame(ttk.Frame):
|
|
|
343
345
|
self, left: Optional[pathlib.Path], right: Optional[pathlib.Path]
|
|
344
346
|
) -> None:
|
|
345
347
|
"""Construct a comparator for a file pair and hand it to the loader."""
|
|
346
|
-
comparator = ImageComparator(left, right)
|
|
348
|
+
comparator = ImageComparator(left, right, self.window)
|
|
347
349
|
self.load_comparator(comparator)
|
|
348
350
|
|
|
349
351
|
def clear(self) -> None:
|