imdiff 0.3.1__tar.gz → 0.4.1__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 (35) hide show
  1. {imdiff-0.3.1 → imdiff-0.4.1}/PKG-INFO +1 -1
  2. imdiff-0.4.1/imdiff/__init__.py +4 -0
  3. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/__main__.py +1 -1
  4. imdiff-0.4.1/imdiff/cli/__init__.py +0 -0
  5. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/cli/main.py +0 -3
  6. imdiff-0.4.1/imdiff/directory_comparator.py +45 -0
  7. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/file_list_frame.py +39 -36
  8. imdiff-0.4.1/imdiff/list_files.py +64 -0
  9. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/version.py +1 -1
  10. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/PKG-INFO +1 -1
  11. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/SOURCES.txt +1 -0
  12. imdiff-0.3.1/imdiff/__init__.py +0 -2
  13. imdiff-0.3.1/imdiff/cli/__init__.py +0 -1
  14. imdiff-0.3.1/imdiff/list_files.py +0 -57
  15. {imdiff-0.3.1 → imdiff-0.4.1}/LICENSE +0 -0
  16. {imdiff-0.3.1 → imdiff-0.4.1}/README.md +0 -0
  17. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/cli/dir_diff.py +0 -0
  18. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/cli/image_diff.py +0 -0
  19. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/__init__.py +0 -0
  20. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/dir_diff_main_window.py +0 -0
  21. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/image_canvas.py +0 -0
  22. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/image_diff_main_window.py +0 -0
  23. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/image_frame.py +0 -0
  24. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/image_scaling.py +0 -0
  25. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/status_bar.py +0 -0
  26. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/transient_menu.py +0 -0
  27. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/zoom_menu.py +0 -0
  28. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/image_comparator.py +0 -0
  29. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/util.py +0 -0
  30. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/dependency_links.txt +0 -0
  31. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/entry_points.txt +0 -0
  32. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/requires.txt +0 -0
  33. {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/top_level.txt +0 -0
  34. {imdiff-0.3.1 → imdiff-0.4.1}/pyproject.toml +0 -0
  35. {imdiff-0.3.1 → imdiff-0.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imdiff
3
- Version: 0.3.1
3
+ Version: 0.4.1
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
@@ -0,0 +1,4 @@
1
+ from .version import __version__, version_info
2
+
3
+ from .directory_comparator import dir_diff
4
+ from .image_comparator import ImageComparator
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
 
3
- from .cli import main
3
+ from .cli.main import main
4
4
 
5
5
 
6
6
  if __name__ == '__main__':
File without changes
@@ -1,10 +1,8 @@
1
1
  import argparse
2
- import logging
3
2
  import pathlib
4
3
 
5
4
  from . import dir_diff, image_diff
6
5
 
7
-
8
6
  def parse_args():
9
7
  parser = argparse.ArgumentParser(
10
8
  prog='imdiff',
@@ -31,7 +29,6 @@ def parse_args():
31
29
  args = parser.parse_args()
32
30
  return args
33
31
 
34
-
35
32
  def main():
36
33
  #logging.basicConfig(level=logging.DEBUG)
37
34
  args = parse_args()
@@ -0,0 +1,45 @@
1
+ import collections
2
+ import difflib
3
+ import pathlib
4
+
5
+ from .list_files import is_image, list_files
6
+ from .image_comparator import ImageComparator
7
+
8
+ def dir_diff(leftdir, rightdir):
9
+ leftdir = pathlib.Path(leftdir)
10
+ rightdir = pathlib.Path(rightdir)
11
+ if not leftdir.is_dir():
12
+ raise FileNotFoundError(leftdir)
13
+ if not rightdir.is_dir():
14
+ raise FileNotFoundError(rightdir)
15
+
16
+ rmse_threshold = 0.02
17
+
18
+ dinfo = collections.defaultdict(list)
19
+ for subpath, left, right in list_files(leftdir, rightdir):
20
+ if not left.is_file():
21
+ dinfo['missing'].append(subpath)
22
+ elif not right.is_file():
23
+ dinfo['new'].append(subpath)
24
+ elif is_image(left) and is_image(right):
25
+ icmp = ImageComparator(left, right)
26
+ if isinstance(icmp.diff_info, str):
27
+ dinfo[icmp.diff_info].append(subpath)
28
+ else:
29
+ assert isinstance(icmp.diff_info, float)
30
+ if icmp.diff_info > rmse_threshold:
31
+ dinfo['different'].append(subpath)
32
+ else:
33
+ dinfo['similar'].append(subpath)
34
+ else:
35
+ diff_output = list(difflib.unified_diff(
36
+ left.read_text().splitlines(),
37
+ right.read_text().splitlines(),
38
+ fromfile=str(left),
39
+ tofile=str(right),
40
+ lineterm=''))
41
+ if diff_output:
42
+ dinfo['different'].append(subpath)
43
+ else:
44
+ dinfo['identical'].append(subpath)
45
+ return dinfo
@@ -36,18 +36,17 @@ class IterableQueue(queue.Queue):
36
36
  return self
37
37
 
38
38
  def __next__(self):
39
- status = self.status
40
- if status == IterableQueue.Status.Inactive:
39
+ if self.status == IterableQueue.Status.Inactive:
41
40
  raise StopIteration
42
41
  while True:
43
42
  try:
44
- if item := self.get_nowait():
45
- return item
43
+ # Block briefly to avoid busy-waiting and allow UI updates to run
44
+ return self.get(timeout=0.05)
46
45
  except queue.Empty:
47
- if status == IterableQueue.Status.Complete:
46
+ # When the producer marks the queue Complete and it's empty, stop
47
+ if self.status == IterableQueue.Status.Complete:
48
48
  self.status = IterableQueue.Status.Inactive
49
49
  raise StopIteration
50
- time.sleep(0.01)
51
50
 
52
51
 
53
52
  class FileListFrame(ttk.Frame):
@@ -67,7 +66,7 @@ class FileListFrame(ttk.Frame):
67
66
  self.text_file_queue = IterableQueue()
68
67
  self.leftdir = None
69
68
  self.rightdir = None
70
- self.progress_var = tk.IntVar()
69
+
71
70
 
72
71
  self.button_choose_left_dir = ttk.Button(
73
72
  self, text=f'{self.left_label.capitalize()}...', command=self.askleftdir
@@ -79,9 +78,7 @@ class FileListFrame(ttk.Frame):
79
78
  self, text=f'Text Diff', command=self.textdiff
80
79
  )
81
80
  self.treeview_frame = tk.Frame(self)
82
- self.progress_bar = ttk.Progressbar(
83
- self.treeview_frame, orient=tk.HORIZONTAL, variable=self.progress_var
84
- )
81
+
85
82
  self.file_treeview = ttk.Treeview(self.treeview_frame, columns=('stat',))
86
83
  self.file_treeview.heading('#0', text='File (▲)')
87
84
  self.file_treeview.heading('stat', text='Stat')
@@ -220,22 +217,33 @@ class FileListFrame(ttk.Frame):
220
217
 
221
218
  def append_file_entry(self, file_path):
222
219
  self.file_entries.append(file_path)
223
- self.file_treeview.insert('', 'end', file_path, text=file_path)
224
- col_width = max(200, self.file_treeview.column('#0', 'width'))
225
- item_width = int(0.85 * tk.font.Font().measure(file_path))
226
- if item_width > col_width:
227
- self.file_treeview.column('#0', width=item_width)
220
+ self.after_idle(lambda: self.file_treeview.insert('', 'end', file_path, text=file_path))
228
221
 
229
222
  @separate_thread
230
223
  def append_files_to_list(self):
231
224
  assert self.file_processing_queue.status == IterableQueue.Status.Filling
225
+
226
+ BATCH_SIZE = 200
227
+ batch = []
228
+
229
+ def flush_batch(items):
230
+ for p in items:
231
+ self.file_treeview.insert('', 'end', p, text=p)
232
+
232
233
  for file_path in self.file_queue:
233
- self.append_file_entry(file_path)
234
- for file_path in self.file_entries:
235
- self.file_processing_queue.put(file_path)
234
+ self.file_entries.append(file_path)
235
+ self.file_processing_queue.put(file_path) # stream to processing queue
236
+ batch.append(file_path)
237
+ if len(batch) >= BATCH_SIZE:
238
+ self.after(0, flush_batch, batch[:])
239
+ batch.clear()
240
+
241
+ if batch:
242
+ self.after(0, flush_batch, batch[:])
243
+
236
244
  self.file_processing_queue.status = IterableQueue.Status.Complete
237
- self.file_treeview.heading('#0', command=lambda: self.sort_filepaths(0, True))
238
- self.file_treeview.heading('stat', command=lambda: self.sort_stat(False))
245
+ self.after_idle(lambda: self.file_treeview.heading('#0', command=lambda: self.sort_filepaths(0, True)))
246
+ self.after_idle(lambda: self.file_treeview.heading('stat', command=lambda: self.sort_stat(False)))
239
247
 
240
248
  @staticmethod
241
249
  def get_diff_info(file_path, comparator):
@@ -295,23 +303,11 @@ class FileListFrame(ttk.Frame):
295
303
  self.text_file_queue.status = IterableQueue.Status.Complete
296
304
  break
297
305
 
298
- def show_progress_bar(self, initial_value=0):
299
- self.progress_var.set(initial_value)
300
- self.progress_bar.grid(column=0, row=2, columnspan=2, sticky='sew')
301
-
302
- def update_progress_bar(self, value):
303
- self.progress_var.set(value)
304
-
305
- def hide_progress_bar(self):
306
- self.progress_bar.grid_forget()
307
-
308
306
  def set_file_properties(self, file_path, category, diff_info):
309
307
  try:
310
308
  self.file_treeview.set(file_path, 'stat', category)
311
309
  self.file_treeview.item(file_path, tags=(diff_info,))
312
- if self.progress_bar.grid_info():
313
- self.num_files_loaded += 1
314
- self.update_progress_bar(100 * self.num_files_loaded // len(self.files))
310
+
315
311
  except tk.TclError as e:
316
312
  log.debug(f'exception caught while setting file properties: {e}')
317
313
 
@@ -320,12 +316,19 @@ class FileListFrame(ttk.Frame):
320
316
  for item in self.file_properties_queue:
321
317
  file_path, category, diff_info = item
322
318
  self.set_file_properties(file_path, category, diff_info)
323
- self.hide_progress_bar()
319
+ def _remove_status_label(self=self):
320
+ if hasattr(self, 'status_label'):
321
+ self.status_label.destroy()
322
+ self.after(0, _remove_status_label)
324
323
 
325
324
  def load_files(self):
326
325
  self.clear_file_list()
327
- self.num_files_loaded = 0
328
- self.show_progress_bar(0)
326
+ # Minimal status indicator
327
+ if not hasattr(self, 'status_var'):
328
+ self.status_var = tk.StringVar(value='')
329
+ self.status_label = ttk.Label(self.treeview_frame, textvariable=self.status_var, anchor='w')
330
+ self.status_label.grid(column=0, row=2, columnspan=2, sticky='sew')
331
+ self.status_var.set('Loading...')
329
332
  self.file_queue.status = IterableQueue.Status.Filling
330
333
  self.text_file_queue.status = IterableQueue.Status.Filling
331
334
  self.queue_files()
@@ -0,0 +1,64 @@
1
+ import os
2
+ import pathlib
3
+
4
+ from PIL import Image
5
+
6
+ from .image_comparator import ImageComparator
7
+
8
+
9
+ # Get list of image file extensions from PIL
10
+ IMAGE_EXTENTIONS = {ex.lower() for ex, f in Image.registered_extensions().items() if f in Image.OPEN}
11
+ IMAGE_EXTENTIONS |= {'.bmp', '.png', '.jpg', '.ps', '.eps', '.cps', '.tif', '.tiff'}
12
+
13
+
14
+ def is_image(file_path):
15
+ return file_path.is_file() and file_path.suffix.lower() in IMAGE_EXTENTIONS
16
+
17
+
18
+ def list_files(left_topdir, right_topdir, subdir=pathlib.Path('.')):
19
+ if not (left_topdir.is_dir() and right_topdir.is_dir()):
20
+ return
21
+ def _scan_dir(p):
22
+ dirs, files = set(), set()
23
+ try:
24
+ with os.scandir(p) as it:
25
+ for entry in it:
26
+ name = entry.name
27
+ if name.startswith('.'):
28
+ # skip hidden files
29
+ continue
30
+ try:
31
+ if entry.is_dir(follow_symlinks=False):
32
+ dirs.add(name)
33
+ elif entry.is_file(follow_symlinks=False):
34
+ files.add(name)
35
+ except OSError:
36
+ # Ignore entries that cannot be accessed
37
+ continue
38
+ except FileNotFoundError:
39
+ pass
40
+ return dirs, files
41
+
42
+ stack = [pathlib.Path(subdir)]
43
+ while stack:
44
+ rel = stack.pop()
45
+ leftdir = left_topdir / rel
46
+ rightdir = right_topdir / rel
47
+
48
+ left_dirs, left_files = _scan_dir(leftdir) if leftdir.is_dir() else (set(), set())
49
+ right_dirs, right_files = _scan_dir(rightdir) if rightdir.is_dir() else (set(), set())
50
+
51
+ files = left_files | right_files
52
+ for fname in sorted(files):
53
+ yield str(rel / fname), leftdir / fname, rightdir / fname
54
+
55
+ subdirs = left_dirs | right_dirs
56
+ # Push in reverse-sorted order to process in ascending order (depth-first)
57
+ for dname in sorted(subdirs, reverse=True):
58
+ stack.append(rel / dname)
59
+
60
+
61
+ def list_image_files(left_topdir, right_topdir, subdir=pathlib.Path('.')):
62
+ for subpath, left, right in list_files(left_topdir, right_topdir, subdir):
63
+ if is_image(left) or is_image(right):
64
+ yield subpath, ImageComparator(left, right)
@@ -1,2 +1,2 @@
1
- __version__ = '0.3.1'
1
+ __version__ = '0.4.1'
2
2
  version_info = __version__.split('.')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imdiff
3
- Version: 0.3.1
3
+ Version: 0.4.1
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
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  imdiff/__init__.py
5
5
  imdiff/__main__.py
6
+ imdiff/directory_comparator.py
6
7
  imdiff/image_comparator.py
7
8
  imdiff/list_files.py
8
9
  imdiff/util.py
@@ -1,2 +0,0 @@
1
- from .version import __version__, version_info
2
- from . import cli, gui
@@ -1 +0,0 @@
1
- from .main import main
@@ -1,57 +0,0 @@
1
- import pathlib
2
-
3
- from PIL import Image
4
-
5
- from .image_comparator import ImageComparator
6
-
7
-
8
- # Get list of image file extensions from PIL
9
- IMAGE_EXTENTIONS = {ex.lower() for ex, f in Image.registered_extensions().items() if f in Image.OPEN}
10
- IMAGE_EXTENTIONS |= {'.bmp', '.png', '.jpg', '.ps', '.eps', '.cps', '.tif', '.tiff'}
11
-
12
-
13
- def is_image(file_path):
14
- return file_path.is_file() and file_path.suffix.lower() in IMAGE_EXTENTIONS
15
-
16
-
17
- def list_files(left_topdir, right_topdir, subdir=pathlib.Path('.')):
18
- if left_topdir.is_dir():
19
- leftdir = left_topdir / subdir
20
- if right_topdir.is_dir():
21
- rightdir = right_topdir / subdir
22
-
23
- dirs = set()
24
- files = set()
25
-
26
- if leftdir.is_dir():
27
- leftdir_paths = list(leftdir.glob('[!.]*'))
28
- dirs |= set(
29
- map(
30
- lambda p: p.relative_to(left_topdir),
31
- filter(pathlib.Path.is_dir, leftdir_paths),
32
- )
33
- )
34
- files |= set(map(lambda p: p.name, filter(pathlib.Path.is_file, leftdir_paths)))
35
-
36
- if rightdir.is_dir():
37
- rightdir_paths = list(rightdir.glob('[!.]*'))
38
- dirs |= set(
39
- map(
40
- lambda p: p.relative_to(right_topdir),
41
- filter(pathlib.Path.is_dir, rightdir_paths),
42
- )
43
- )
44
- files |= set(map(lambda p: p.name, filter(pathlib.Path.is_file, rightdir_paths)))
45
-
46
- for file in sorted(files):
47
- yield str(subdir / file), leftdir / file, rightdir / file
48
-
49
- for d in sorted(dirs):
50
- for item in list_files(left_topdir, right_topdir, d):
51
- yield item
52
-
53
-
54
- def list_image_files(left_topdir, right_topdir, subdir=pathlib.Path('.')):
55
- for subpath, left, right in list_files(left_topdir, right_topdir, subdir):
56
- if is_image(left) or is_image(right):
57
- yield subpath, ImageComparator(left, right)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes