imdiff 0.4.2__tar.gz → 0.4.4__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 (32) hide show
  1. {imdiff-0.4.2 → imdiff-0.4.4}/PKG-INFO +1 -1
  2. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/file_list_frame.py +5 -3
  3. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/image_canvas.py +28 -0
  4. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/image_frame.py +108 -67
  5. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/zoom_menu.py +3 -3
  6. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/version.py +1 -1
  7. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff.egg-info/PKG-INFO +1 -1
  8. {imdiff-0.4.2 → imdiff-0.4.4}/LICENSE +0 -0
  9. {imdiff-0.4.2 → imdiff-0.4.4}/README.md +0 -0
  10. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/__init__.py +0 -0
  11. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/__main__.py +0 -0
  12. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/cli/__init__.py +0 -0
  13. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/cli/dir_diff.py +0 -0
  14. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/cli/image_diff.py +0 -0
  15. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/cli/main.py +0 -0
  16. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/directory_comparator.py +0 -0
  17. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/__init__.py +0 -0
  18. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/dir_diff_main_window.py +0 -0
  19. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/image_diff_main_window.py +0 -0
  20. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/image_scaling.py +0 -0
  21. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/status_bar.py +0 -0
  22. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/gui/transient_menu.py +0 -0
  23. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/image_comparator.py +0 -0
  24. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/list_files.py +0 -0
  25. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff/util.py +0 -0
  26. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff.egg-info/SOURCES.txt +0 -0
  27. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff.egg-info/dependency_links.txt +0 -0
  28. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff.egg-info/entry_points.txt +0 -0
  29. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff.egg-info/requires.txt +0 -0
  30. {imdiff-0.4.2 → imdiff-0.4.4}/imdiff.egg-info/top_level.txt +0 -0
  31. {imdiff-0.4.2 → imdiff-0.4.4}/pyproject.toml +0 -0
  32. {imdiff-0.4.2 → imdiff-0.4.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imdiff
3
- Version: 0.4.2
3
+ Version: 0.4.4
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
@@ -270,7 +270,7 @@ class FileListFrame(ttk.Frame):
270
270
  diff_info = 'similar' if nrmse < 0.02 else 'different'
271
271
  category = '~' if nrmse < 0.02 else 'D'
272
272
 
273
- return file_path, category, diff_info
273
+ return file_path, category, diff_info, comparator
274
274
 
275
275
  @separate_thread
276
276
  def process_files(self):
@@ -286,7 +286,8 @@ class FileListFrame(ttk.Frame):
286
286
  jobs.append(executor.submit(
287
287
  FileListFrame.get_diff_info, file_path, self.files[file_path]))
288
288
  for job in concurrent.futures.as_completed(jobs):
289
- file_path, category, diff_info = job.result()
289
+ file_path, category, diff_info, icmp = job.result()
290
+ self.files[file_path] = icmp
290
291
  self.file_properties_queue.put((file_path, category, diff_info))
291
292
  self.file_properties_queue.status = IterableQueue.Status.Complete
292
293
  finally:
@@ -393,7 +394,8 @@ class FileListFrame(ttk.Frame):
393
394
  return
394
395
 
395
396
  res = FileListFrame.get_diff_info(file_path, self.files[file_path])
396
- file_path, category, diff_info = res
397
+ file_path, category, diff_info, icmp = res
398
+ self.files[file_path] = icmp
397
399
  self.set_file_properties(file_path, category, diff_info)
398
400
 
399
401
  def sort_filepaths(self, nparts, reverse):
@@ -61,6 +61,31 @@ class ImageCanvas(tk.Canvas):
61
61
  self.image = image
62
62
  self.after_idle(self.update_image)
63
63
 
64
+ def swap_image(self, image):
65
+ """Swap displayed image without clearing canvas, to avoid flicker."""
66
+ log.debug(f'[{self.label}] swap image (setting to {image})')
67
+ if image is None:
68
+ self.clear()
69
+ return
70
+ self.image = image
71
+ # Reset scaled image to force recalculation
72
+ self.scaled_image = None
73
+ # Synchronously update display
74
+ self.update_scaled_image()
75
+ # Create PhotoImage before modifying canvas
76
+ if self.scaled_image:
77
+ new_tkimage = ImageTk.PhotoImage(self.scaled_image)
78
+ if self.image_id:
79
+ # Update existing canvas item - this is atomic and won't flicker
80
+ self.itemconfig(self.image_id, image=new_tkimage)
81
+ else:
82
+ self.image_id = self.create_image(
83
+ self.border, self.border, image=new_tkimage, anchor='nw'
84
+ )
85
+ # Keep reference to prevent garbage collection
86
+ self.tkimage = new_tkimage
87
+ self.update_layout()
88
+
64
89
  def update_scaled_image(self):
65
90
  log.debug(f'[{self.label}] update scaled image (image: {self.image})')
66
91
  self.scaled_image_updated = False
@@ -155,3 +180,6 @@ class ImageCanvas(tk.Canvas):
155
180
  if pos is not None:
156
181
  self.position = pos
157
182
  self.update_image()
183
+
184
+ def raise_to_front(self):
185
+ self.tk.call('raise', self)
@@ -1,7 +1,5 @@
1
1
  import logging
2
2
  import platform
3
- import multiprocessing
4
- import time
5
3
 
6
4
  import tkinter as tk
7
5
  import ttkbootstrap as ttk
@@ -80,11 +78,18 @@ class ImageFrame(ttk.Frame):
80
78
  )
81
79
 
82
80
  self.image_frame = ttk.Frame(self)
83
- self.images = {
81
+ self.canvases = {
84
82
  'left': ImageCanvas(self.image_frame, 'left'),
85
83
  'right': ImageCanvas(self.image_frame, 'right'),
86
84
  'diff': ImageCanvas(self.image_frame, 'diff'),
87
85
  }
86
+ self.images = {
87
+ 'left': None,
88
+ 'right': None,
89
+ 'diff': None,
90
+ }
91
+
92
+ self.current_top_image = 'left'
88
93
 
89
94
  self.comparator = None
90
95
  self.button_zoom_fit_if_larger.invoke()
@@ -145,15 +150,15 @@ class ImageFrame(ttk.Frame):
145
150
 
146
151
  def layout_images(self):
147
152
  if platform.system() == 'Linux':
148
- mouse_wheel_seqs = ('<Button-4>', '<Button-5>')
153
+ mouse_wheel_seqs = ('<Button-4>', '<Button-5>', '<MouseWheel>',)
149
154
  else:
150
155
  mouse_wheel_seqs = ('<MouseWheel>',)
151
156
 
152
157
  n = self.image_layout_style.get()
153
158
  if n == 3:
154
- self.images['left'].grid(column=0, row=0, sticky='nsew')
155
- self.images['right'].grid(column=1, row=0, sticky='nsew')
156
- self.images['diff'].grid(column=2, row=0, sticky='nsew')
159
+ self.canvases['left'].grid(column=0, row=0, sticky='nsew')
160
+ self.canvases['right'].grid(column=1, row=0, sticky='nsew')
161
+ self.canvases['diff'].grid(column=2, row=0, sticky='nsew')
157
162
 
158
163
  self.image_frame.columnconfigure(0, weight=1)
159
164
  self.image_frame.columnconfigure(1, weight=1)
@@ -161,78 +166,81 @@ class ImageFrame(ttk.Frame):
161
166
 
162
167
  for seq in mouse_wheel_seqs:
163
168
  self.image_frame.unbind(seq)
164
- for image in self.images.values():
165
- image.unbind(seq)
169
+ for canvas in self.canvases.values():
170
+ canvas.unbind(seq)
166
171
 
167
172
  elif n == 2:
168
- self.images['left'].grid(column=0, row=0, sticky='nsew')
169
- self.images['right'].grid_forget()
170
- self.images['diff'].grid(column=1, row=0, sticky='nsew')
173
+ # Use single 'left' canvas for swapping left/right, plus diff canvas
174
+ self.canvases['left'].grid(column=0, row=0, sticky='nsew')
175
+ self.canvases['right'].grid_forget()
176
+ self.canvases['diff'].grid(column=1, row=0, sticky='nsew')
171
177
 
172
178
  self.image_frame.columnconfigure(0, weight=1)
173
179
  self.image_frame.columnconfigure(1, weight=1)
174
180
  self.image_frame.columnconfigure(2, weight=0)
175
181
 
182
+ if self.current_top_image not in ('left', 'right'):
183
+ self.current_top_image = 'left'
184
+ self.canvases['left'].swap_image(self.images[self.current_top_image])
185
+
176
186
  for seq in mouse_wheel_seqs:
177
187
  self.image_frame.bind(seq, self.on_mouse_wheel)
178
- for image in self.images.values():
179
- image.bind(seq, self.on_mouse_wheel)
188
+ for canvas in self.canvases.values():
189
+ canvas.bind(seq, self.on_mouse_wheel, add='+')
180
190
 
181
191
  else:
182
192
  assert n == 1
183
- self.images['left'].grid(column=0, row=0, sticky='nsew')
184
- self.images['right'].grid_forget()
185
- self.images['diff'].grid_forget()
193
+ # Use single 'left' canvas for swapping all three images
194
+ self.canvases['left'].grid(column=0, row=0, sticky='nsew')
195
+ self.canvases['right'].grid_forget()
196
+ self.canvases['diff'].grid_forget()
186
197
 
187
198
  self.image_frame.columnconfigure(0, weight=1)
188
199
  self.image_frame.columnconfigure(1, weight=0)
189
200
  self.image_frame.columnconfigure(2, weight=0)
190
201
 
202
+ self.canvases['left'].swap_image(self.images[self.current_top_image])
203
+
191
204
  for seq in mouse_wheel_seqs:
192
205
  self.image_frame.bind(seq, self.on_mouse_wheel)
193
- for image in self.images.values():
194
- image.bind(seq, self.on_mouse_wheel)
206
+ for canvas in self.canvases.values():
207
+ canvas.bind(seq, self.on_mouse_wheel, add='+')
195
208
 
196
209
  def bind_events(self):
197
- for key in self.images.keys():
198
- for otherkey in set(self.images.keys()) - {key}:
199
- self.images[key].bind(
200
- '<Button1-Motion>', self.images[otherkey].on_motion, add='+'
210
+ for key in self.canvases.keys():
211
+ for otherkey in set(self.canvases.keys()) - {key}:
212
+ self.canvases[key].bind(
213
+ '<Button1-Motion>', self.canvases[otherkey].on_motion, add='+'
201
214
  )
202
- self.images[key].bind(
203
- '<ButtonRelease-1>', self.images[otherkey].on_left_up, add='+'
215
+ self.canvases[key].bind(
216
+ '<ButtonRelease-1>', self.canvases[otherkey].on_left_up, add='+'
204
217
  )
205
218
 
206
219
  def on_mouse_wheel(self, evt):
207
- if platform.system() == 'Linux':
208
- mouse_wheel_up = evt.num == 5
209
- else:
220
+ if evt.type == tk.EventType.MouseWheel:
210
221
  mouse_wheel_up = evt.delta > 0
222
+ else:
223
+ mouse_wheel_up = evt.num == 5
211
224
 
212
225
  n = self.image_layout_style.get()
213
226
  if n == 2:
214
- if self.images['left'].grid_info():
215
- self.images['left'].grid_forget()
216
- self.images['right'].grid(column=0, row=0, sticky='nsew')
227
+ # Toggle between left and right
228
+ if self.current_top_image == 'left':
229
+ self.current_top_image = 'right'
217
230
  else:
218
- assert self.images['right'].grid_info()
219
- self.images['left'].grid(column=0, row=0, sticky='nsew')
220
- self.images['right'].grid_forget()
231
+ self.current_top_image = 'left'
221
232
  else:
222
233
  assert n == 1
223
- if self.images['left'].grid_info():
224
- next_image = 'right' if mouse_wheel_up else 'diff'
225
- self.images['left'].grid_forget()
226
- self.images[next_image].grid(column=0, row=0, sticky='nsew')
227
- elif self.images['right'].grid_info():
228
- next_image = 'diff' if mouse_wheel_up else 'left'
229
- self.images['right'].grid_forget()
230
- self.images[next_image].grid(column=0, row=0, sticky='nsew')
234
+ # Cycle through left -> right -> diff -> left (or reverse)
235
+ cycle = ['left', 'right', 'diff']
236
+ current_idx = cycle.index(self.current_top_image)
237
+ if mouse_wheel_up:
238
+ next_idx = (current_idx + 1) % 3
231
239
  else:
232
- assert self.images['diff'].grid_info()
233
- next_image = 'left' if mouse_wheel_up else 'right'
234
- self.images['diff'].grid_forget()
235
- self.images[next_image].grid(column=0, row=0, sticky='nsew')
240
+ next_idx = (current_idx - 1) % 3
241
+ self.current_top_image = cycle[next_idx]
242
+
243
+ self.canvases['left'].swap_image(self.images[self.current_top_image])
236
244
 
237
245
  def minsize(self):
238
246
  width = (
@@ -251,8 +259,8 @@ class ImageFrame(ttk.Frame):
251
259
  self.zoom(ImageScaling.Mode.Fit, factor=1, shrink=True, expand=False)
252
260
 
253
261
  def zoom(self, mode, factor=1, shrink=True, expand=True):
254
- for image in self.images.values():
255
- image.zoom(mode, factor=factor, shrink=shrink, expand=expand)
262
+ for canvas in self.canvases.values():
263
+ canvas.zoom(mode, factor=factor, shrink=shrink, expand=expand)
256
264
 
257
265
  def post_zoom_menu(self):
258
266
  pos = Coordinates(
@@ -264,30 +272,40 @@ class ImageFrame(ttk.Frame):
264
272
  def show_diff(self):
265
273
  if self.high_contrast.get():
266
274
  if self.comparator:
267
- self.images['diff'].scaled_image = None
268
- self.images['diff'].load_image(self.comparator.high_contrast_diff)
275
+ self.images['diff'] = self.comparator.high_contrast_diff
276
+ self.canvases['diff'].scaled_image = None
277
+ self.canvases['diff'].swap_image(self.images['diff'])
269
278
  else:
270
279
  if self.comparator:
271
- self.images['diff'].scaled_image = None
272
- self.images['diff'].load_image(self.comparator.diff)
273
-
274
- @separate_thread
275
- def load_images(self, left, right):
276
- self.comparator = ImageComparator(left, right)
277
- self.images['left'].load_image(self.comparator.left)
278
- self.images['right'].load_image(self.comparator.right)
279
- self.show_diff()
280
+ self.images['diff'] = self.comparator.diff
281
+ self.canvases['diff'].scaled_image = None
282
+ self.canvases['diff'].swap_image(self.images['diff'])
283
+ # In mode 1, if diff is currently shown on the left canvas, update it
284
+ n = self.image_layout_style.get()
285
+ if n == 1 and self.current_top_image == 'diff':
286
+ self.canvases['left'].swap_image(self.images[self.current_top_image])
280
287
 
281
288
  @separate_thread
282
289
  def load_comparator(self, comparator):
283
290
  self.comparator = comparator
284
- self.images['left'].load_image(self.comparator.left)
285
- self.images['right'].load_image(self.comparator.right)
291
+ self.images['left'] = self.comparator.left
292
+ self.images['right'] = self.comparator.right
286
293
  self.show_diff()
294
+ # In modes 1 and 2, update the displayed image on the left canvas
295
+ n = self.image_layout_style.get()
296
+ if n in (1, 2):
297
+ self.canvases['left'].swap_image(self.images[self.current_top_image])
298
+ else:
299
+ for loc in ('left', 'right', 'diff'):
300
+ self.canvases[loc].swap_image(self.images[loc])
301
+
302
+ def load_images(self, left, right):
303
+ comparator = ImageComparator(left, right)
304
+ self.load_comparator(comparator)
287
305
 
288
306
  def clear(self):
289
- for image in self.images.values():
290
- self.after_idle(image.clear)
307
+ for canvas in self.canvases.values():
308
+ self.after_idle(canvas.clear)
291
309
 
292
310
  def delete(self):
293
311
  if self.comparator:
@@ -311,8 +329,20 @@ class ImageFrame(ttk.Frame):
311
329
  log.info(f'COPY: {left_file} -> {right_file}')
312
330
  self.comparator.copy_left_to_right()
313
331
  if self.comparator.right:
314
- self.images['right'].load_image(self.comparator.right)
315
- self.images['diff'].load_image(self.comparator.diff)
332
+ self.images['right'] = self.comparator.right
333
+ if self.high_contrast:
334
+ self.images['diff'] = self.comparator.high_contrast_diff
335
+ else:
336
+ self.images['diff'] = self.comparator.diff
337
+ self.canvases['right'].swap_image(self.images['right'])
338
+ self.canvases['diff'].swap_image(self.images['diff'])
339
+ # In modes 1 and 2, update the displayed image on the left canvas
340
+ n = self.image_layout_style.get()
341
+ if n in (1, 2) and self.current_top_image in ('right', 'diff'):
342
+ self.canvases['left'].swap_image(self.images[self.current_top_image])
343
+ else:
344
+ for loc in ('right', 'diff'):
345
+ self.canvases[loc].swap_image(self.images[loc])
316
346
  if self.update_item_command:
317
347
  self.update_item_command(right_file, self.comparator)
318
348
 
@@ -325,7 +355,18 @@ class ImageFrame(ttk.Frame):
325
355
  log.info(f'COPY: {right_file} -> {left_file}')
326
356
  self.comparator.copy_right_to_left()
327
357
  if self.comparator.left:
328
- self.images['left'].load_image(self.comparator.left)
329
- self.images['diff'].load_image(self.comparator.diff)
358
+ self.images['left'] = self.comparator.left
359
+ if self.high_contrast:
360
+ self.images['diff'] = self.comparator.high_contrast_diff
361
+ else:
362
+ self.images['diff'] = self.comparator.diff
363
+ self.canvases['diff'].load_image(self.comparator.diff)
364
+ # In modes 1 and 2, update the displayed image on the left canvas
365
+ n = self.image_layout_style.get()
366
+ if n in (1, 2) and self.current_top_image in ('left', 'diff'):
367
+ self.canvases['left'].swap_image(self.images[self.current_top_image])
368
+ else:
369
+ for loc in ('left', 'diff'):
370
+ self.canvases[loc].swap_image(self.images[loc])
330
371
  if self.update_item_command:
331
372
  self.update_item_command(left_file, self.comparator)
@@ -62,8 +62,8 @@ class ZoomMenu(TransientMenu):
62
62
  )
63
63
 
64
64
  def post(self, pos):
65
- scaling = self.parent.images['left'].scaling
66
- scaling_percent = self.parent.images['left'].scaling_percent
65
+ scaling = self.parent.canvases['left'].scaling
66
+ scaling_percent = self.parent.canvases['left'].scaling_percent
67
67
  self.mode_var.set('FitIfLarger')
68
68
  if scaling.mode == ImageScaling.Mode.Original:
69
69
  self.mode_var.set('Original')
@@ -112,4 +112,4 @@ class ZoomMenu(TransientMenu):
112
112
  self.mode_var.set('Scaled')
113
113
 
114
114
  def update_scale_var(self):
115
- self.scale_var.set(self.parent.images['left'].scaling_percent)
115
+ self.scale_var.set(self.parent.canvases['left'].scaling_percent)
@@ -1,2 +1,2 @@
1
- __version__ = '0.4.2'
1
+ __version__ = '0.4.4'
2
2
  version_info = __version__.split('.')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imdiff
3
- Version: 0.4.2
3
+ Version: 0.4.4
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
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