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.
- {imdiff-0.3.1 → imdiff-0.4.1}/PKG-INFO +1 -1
- imdiff-0.4.1/imdiff/__init__.py +4 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/__main__.py +1 -1
- imdiff-0.4.1/imdiff/cli/__init__.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/cli/main.py +0 -3
- imdiff-0.4.1/imdiff/directory_comparator.py +45 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/file_list_frame.py +39 -36
- imdiff-0.4.1/imdiff/list_files.py +64 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/version.py +1 -1
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/PKG-INFO +1 -1
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/SOURCES.txt +1 -0
- imdiff-0.3.1/imdiff/__init__.py +0 -2
- imdiff-0.3.1/imdiff/cli/__init__.py +0 -1
- imdiff-0.3.1/imdiff/list_files.py +0 -57
- {imdiff-0.3.1 → imdiff-0.4.1}/LICENSE +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/README.md +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/cli/dir_diff.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/cli/image_diff.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/__init__.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/dir_diff_main_window.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/image_canvas.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/image_diff_main_window.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/image_frame.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/image_scaling.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/status_bar.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/transient_menu.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/gui/zoom_menu.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/image_comparator.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff/util.py +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/dependency_links.txt +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/entry_points.txt +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/requires.txt +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/imdiff.egg-info/top_level.txt +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/pyproject.toml +0 -0
- {imdiff-0.3.1 → imdiff-0.4.1}/setup.cfg +0 -0
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
328
|
-
self
|
|
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.
|
|
1
|
+
__version__ = '0.4.1'
|
|
2
2
|
version_info = __version__.split('.')
|
imdiff-0.3.1/imdiff/__init__.py
DELETED
|
@@ -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
|
|
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
|