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.
Files changed (46) hide show
  1. {imdiff-2.0.0 → imdiff-2.1.0}/PKG-INFO +53 -8
  2. {imdiff-2.0.0 → imdiff-2.1.0}/README.md +51 -2
  3. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/__init__.py +1 -1
  4. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/dir_diff.py +11 -5
  5. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/image_diff.py +11 -5
  6. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/main.py +50 -6
  7. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/directory_comparator.py +6 -3
  8. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/_types.py +1 -1
  9. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/dir_diff_main_window.py +8 -2
  10. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/file_list_frame.py +4 -2
  11. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/image_diff_main_window.py +8 -2
  12. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/image_frame.py +4 -2
  13. imdiff-2.1.0/imdiff/image_comparator.py +266 -0
  14. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/version.py +1 -1
  15. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/PKG-INFO +53 -8
  16. {imdiff-2.0.0 → imdiff-2.1.0}/pyproject.toml +9 -6
  17. {imdiff-2.0.0 → imdiff-2.1.0}/test/test_cli_diff.py +2 -2
  18. {imdiff-2.0.0 → imdiff-2.1.0}/test/test_cli_main.py +40 -9
  19. {imdiff-2.0.0 → imdiff-2.1.0}/test/test_directory_comparator.py +17 -0
  20. {imdiff-2.0.0 → imdiff-2.1.0}/test/test_image_comparator.py +140 -1
  21. imdiff-2.0.0/imdiff/image_comparator.py +0 -221
  22. {imdiff-2.0.0 → imdiff-2.1.0}/LICENSE +0 -0
  23. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/__main__.py +0 -0
  24. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/_compat.py +0 -0
  25. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/_types.py +0 -0
  26. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/__init__.py +0 -0
  27. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/cli/_gui_runtime.py +0 -0
  28. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/__init__.py +0 -0
  29. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/image_canvas.py +0 -0
  30. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/image_scaling.py +0 -0
  31. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/status_bar.py +0 -0
  32. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/transient_menu.py +0 -0
  33. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/gui/zoom_menu.py +0 -0
  34. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/list_files.py +0 -0
  35. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff/util.py +0 -0
  36. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/SOURCES.txt +0 -0
  37. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/dependency_links.txt +0 -0
  38. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/entry_points.txt +0 -0
  39. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/requires.txt +0 -0
  40. {imdiff-2.0.0 → imdiff-2.1.0}/imdiff.egg-info/top_level.txt +0 -0
  41. {imdiff-2.0.0 → imdiff-2.1.0}/setup.cfg +0 -0
  42. {imdiff-2.0.0 → imdiff-2.1.0}/test/test_file_list_frame.py +0 -0
  43. {imdiff-2.0.0 → imdiff-2.1.0}/test/test_image_scaling.py +0 -0
  44. {imdiff-2.0.0 → imdiff-2.1.0}/test/test_list_files.py +0 -0
  45. {imdiff-2.0.0 → imdiff-2.1.0}/test/test_module_main.py +0 -0
  46. {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.0.0
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 :: 4 - Beta
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` contains the lazy comparison model shared by the
98
- CLI and GUI.
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` contains the lazy comparison model shared by the
62
- CLI and GUI.
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.
@@ -4,5 +4,5 @@
4
4
  # ruff: noqa: F401
5
5
 
6
6
  from .directory_comparator import dir_diff
7
- from .image_comparator import ImageComparator
7
+ from .image_comparator import ImageComparator, RmseWindow, Window, normalized_rmse
8
8
  from .version import __version__, version_info
@@ -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, rightdir: 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(leftdir: pathlib.Path, rightdir: pathlib.Path) -> int:
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, right: 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(left: pathlib.Path, right: pathlib.Path) -> int:
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, rightdir: 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__(self, left_label: str = 'left', right_label: str = 'right') -> None:
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__(self, left_label: str = 'left', right_label: str = 'right') -> None:
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: