shinestacker 0.3.3__py3-none-any.whl → 0.3.5__py3-none-any.whl

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.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (71) hide show
  1. shinestacker/__init__.py +2 -1
  2. shinestacker/_version.py +1 -1
  3. shinestacker/algorithms/__init__.py +3 -2
  4. shinestacker/algorithms/align.py +102 -64
  5. shinestacker/algorithms/balance.py +89 -42
  6. shinestacker/algorithms/base_stack_algo.py +41 -0
  7. shinestacker/algorithms/core_utils.py +6 -6
  8. shinestacker/algorithms/denoise.py +4 -1
  9. shinestacker/algorithms/depth_map.py +28 -39
  10. shinestacker/algorithms/exif.py +43 -38
  11. shinestacker/algorithms/multilayer.py +48 -28
  12. shinestacker/algorithms/noise_detection.py +34 -26
  13. shinestacker/algorithms/pyramid.py +42 -42
  14. shinestacker/algorithms/sharpen.py +1 -0
  15. shinestacker/algorithms/stack.py +42 -42
  16. shinestacker/algorithms/stack_framework.py +118 -66
  17. shinestacker/algorithms/utils.py +12 -11
  18. shinestacker/algorithms/vignetting.py +52 -25
  19. shinestacker/algorithms/white_balance.py +1 -0
  20. shinestacker/app/about_dialog.py +6 -2
  21. shinestacker/app/app_config.py +1 -0
  22. shinestacker/app/gui_utils.py +20 -0
  23. shinestacker/app/help_menu.py +2 -1
  24. shinestacker/app/main.py +9 -18
  25. shinestacker/app/open_frames.py +5 -4
  26. shinestacker/app/project.py +5 -16
  27. shinestacker/app/retouch.py +5 -17
  28. shinestacker/core/colors.py +4 -4
  29. shinestacker/core/core_utils.py +1 -1
  30. shinestacker/core/exceptions.py +2 -1
  31. shinestacker/core/framework.py +46 -33
  32. shinestacker/core/logging.py +9 -10
  33. shinestacker/gui/action_config.py +253 -197
  34. shinestacker/gui/actions_window.py +36 -35
  35. shinestacker/gui/colors.py +1 -0
  36. shinestacker/gui/gui_images.py +7 -3
  37. shinestacker/gui/gui_logging.py +3 -2
  38. shinestacker/gui/gui_run.py +53 -38
  39. shinestacker/gui/main_window.py +69 -25
  40. shinestacker/gui/new_project.py +35 -2
  41. shinestacker/gui/project_converter.py +21 -20
  42. shinestacker/gui/project_editor.py +51 -52
  43. shinestacker/gui/project_model.py +15 -23
  44. shinestacker/retouch/{filter_base.py → base_filter.py} +7 -4
  45. shinestacker/retouch/brush.py +1 -0
  46. shinestacker/retouch/brush_gradient.py +17 -3
  47. shinestacker/retouch/brush_preview.py +14 -10
  48. shinestacker/retouch/brush_tool.py +28 -19
  49. shinestacker/retouch/denoise_filter.py +3 -2
  50. shinestacker/retouch/display_manager.py +11 -5
  51. shinestacker/retouch/exif_data.py +1 -0
  52. shinestacker/retouch/file_loader.py +13 -9
  53. shinestacker/retouch/filter_manager.py +1 -0
  54. shinestacker/retouch/image_editor.py +14 -48
  55. shinestacker/retouch/image_editor_ui.py +10 -5
  56. shinestacker/retouch/image_filters.py +4 -2
  57. shinestacker/retouch/image_viewer.py +33 -31
  58. shinestacker/retouch/io_gui_handler.py +25 -13
  59. shinestacker/retouch/io_manager.py +3 -2
  60. shinestacker/retouch/layer_collection.py +79 -23
  61. shinestacker/retouch/shortcuts_help.py +1 -0
  62. shinestacker/retouch/undo_manager.py +7 -0
  63. shinestacker/retouch/unsharp_mask_filter.py +3 -2
  64. shinestacker/retouch/white_balance_filter.py +11 -6
  65. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/METADATA +18 -6
  66. shinestacker-0.3.5.dist-info/RECORD +86 -0
  67. shinestacker-0.3.3.dist-info/RECORD +0 -85
  68. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/WHEEL +0 -0
  69. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/entry_points.txt +0 -0
  70. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/licenses/LICENSE +0 -0
  71. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0913, R0917, R0914
1
2
  import numpy as np
2
3
  from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
3
4
  from PySide6.QtCore import Qt, QPoint
4
- from .brush_gradient import create_brush_gradient
5
+ from .brush_gradient import create_default_brush_gradient
5
6
  from .. config.gui_constants import gui_constants
6
7
  from .. config.constants import constants
7
8
  from .brush_preview import create_brush_mask
@@ -42,7 +43,8 @@ class BrushTool:
42
43
  normalized = slider_val / gui_constants.BRUSH_SIZE_SLIDER_MAX
43
44
  size = gui_constants.BRUSH_SIZES['min'] + \
44
45
  gui_constants.BRUSH_SIZES['max'] * (normalized ** gui_constants.BRUSH_GAMMA)
45
- return max(gui_constants.BRUSH_SIZES['min'], min(gui_constants.BRUSH_SIZES['max'], size))
46
+ return max(gui_constants.BRUSH_SIZES['min'],
47
+ min(gui_constants.BRUSH_SIZES['max'], size))
46
48
 
47
49
  self.brush.size = slider_to_brush_size(slider_val)
48
50
  self.update_brush_thumb()
@@ -89,21 +91,21 @@ class BrushTool:
89
91
  center_x, center_y = width // 2, height // 2
90
92
  radius = preview_size // 2
91
93
  if self.image_viewer.cursor_style == 'preview':
92
- gradient = create_brush_gradient(
93
- center_x, center_y, radius,
94
- self.brush.hardness,
95
- inner_color=QColor(*gui_constants.BRUSH_COLORS['inner']),
96
- outer_color=QColor(*gui_constants.BRUSH_COLORS['gradient_end']),
97
- opacity=self.brush.opacity
98
- )
94
+ gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
99
95
  painter.setBrush(QBrush(gradient))
100
- painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['outer']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
96
+ painter.setPen(
97
+ QPen(QColor(*gui_constants.BRUSH_COLORS['outer']),
98
+ gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
101
99
  elif self.image_viewer.cursor_style == 'outline':
102
100
  painter.setBrush(Qt.NoBrush)
103
- painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['outer']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
101
+ painter.setPen(
102
+ QPen(QColor(*gui_constants.BRUSH_COLORS['outer']),
103
+ gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
104
104
  else:
105
105
  painter.setBrush(QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner'])))
106
- painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
106
+ painter.setPen(
107
+ QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
108
+ gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
107
109
  painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
108
110
  if self.image_viewer.cursor_style == 'preview':
109
111
  painter.setPen(QPen(QColor(0, 0, 160)))
@@ -115,7 +117,8 @@ class BrushTool:
115
117
  self.brush_preview.setPixmap(pixmap)
116
118
  self.image_viewer.update_brush_cursor()
117
119
 
118
- def apply_brush_operation(self, master_layer, source_layer, dest_layer, mask_layer, view_pos, image_viewer):
120
+ def apply_brush_operation(self, master_layer, source_layer, dest_layer, mask_layer,
121
+ view_pos, image_viewer):
119
122
  if master_layer is None or source_layer is None:
120
123
  return False
121
124
  if dest_layer is None:
@@ -136,14 +139,17 @@ class BrushTool:
136
139
  source_area = source_layer[y_start:y_end, x_start:x_end]
137
140
  dest_area = dest_layer[y_start:y_end, x_start:x_end]
138
141
  mask_layer_area = mask_layer[y_start:y_end, x_start:x_end]
139
- mask_area = mask[y_start - (y_center - radius):y_end - (y_center - radius), x_start - (x_center - radius):x_end - (x_center - radius)]
140
- mask_layer_area[:] = np.clip(mask_layer_area + mask_area * self.brush.flow / 100.0, 0.0, 1.0) # np.maximum(mask_layer_area, mask_area)
142
+ mask_area = mask[y_start - (y_center - radius):y_end - (y_center - radius),
143
+ x_start - (x_center - radius):x_end - (x_center - radius)]
144
+ mask_layer_area[:] = np.clip(
145
+ mask_layer_area + mask_area * self.brush.flow / 100.0, 0.0,
146
+ 1.0)
141
147
  self.apply_mask(master_area, source_area, mask_layer_area, dest_area)
142
148
  return x_start, y_start, x_end, y_end
143
149
 
144
150
  def get_brush_mask(self, radius):
145
151
  mask_key = (radius, self.brush.hardness)
146
- if mask_key not in self._brush_mask_cache.keys():
152
+ if mask_key not in self._brush_mask_cache:
147
153
  full_mask = create_brush_mask(size=radius * 2 + 1, hardness_percent=self.brush.hardness,
148
154
  opacity_percent=self.brush.opacity)
149
155
  self._brush_mask_cache[mask_key] = full_mask
@@ -155,10 +161,13 @@ class BrushTool:
155
161
  dtype = master_area.dtype
156
162
  max_px_value = constants.MAX_UINT16 if dtype == np.uint16 else constants.MAX_UINT8
157
163
  if master_area.ndim == 3:
158
- dest_area[:] = np.clip(master_area * (1 - effective_mask[..., np.newaxis]) + source_area * # noqa
159
- effective_mask[..., np.newaxis], 0, max_px_value).astype(dtype)
164
+ dest_area[:] = np.clip(
165
+ master_area * (1 - effective_mask[..., np.newaxis]) +
166
+ source_area * effective_mask[..., np.newaxis], 0, max_px_value).astype(dtype)
160
167
  else:
161
- dest_area[:] = np.clip(master_area * (1 - effective_mask) + source_area * effective_mask, 0, max_px_value).astype(dtype)
168
+ dest_area[:] = np.clip(
169
+ master_area * (1 - effective_mask) + source_area * effective_mask, 0,
170
+ max_px_value).astype(dtype)
162
171
 
163
172
  def clear_cache(self):
164
173
  self._brush_mask_cache.clear()
@@ -1,6 +1,8 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0221
1
2
  from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QCheckBox, QDialogButtonBox
2
3
  from PySide6.QtCore import Qt, QTimer
3
- from .filter_base import BaseFilter
4
+ from .base_filter import BaseFilter
5
+ from .. algorithms.denoise import denoise
4
6
 
5
7
 
6
8
  class DenoiseFilter(BaseFilter):
@@ -52,5 +54,4 @@ class DenoiseFilter(BaseFilter):
52
54
  return (self.max_value * self.slider.value() / self.max_range,)
53
55
 
54
56
  def apply(self, image, strength):
55
- from .. algorithms.denoise import denoise
56
57
  return denoise(image, strength)
@@ -1,8 +1,10 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121
1
2
  import numpy as np
2
3
  from PySide6.QtWidgets import QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog
3
4
  from PySide6.QtGui import QPixmap, QImage
4
5
  from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
5
6
  from .. config.gui_constants import gui_constants
7
+ from .layer_collection import LayerCollectionHandler
6
8
 
7
9
 
8
10
  class ClickableLabel(QLabel):
@@ -12,20 +14,22 @@ class ClickableLabel(QLabel):
12
14
  super().__init__(text, parent)
13
15
  self.setMouseTracking(True)
14
16
 
17
+ # pylint: disable=C0103
15
18
  def mouseDoubleClickEvent(self, event):
16
19
  if event.button() == Qt.LeftButton:
17
20
  self.double_clicked.emit()
18
21
  super().mouseDoubleClickEvent(event)
22
+ # pylint: enable=C0103
19
23
 
20
24
 
21
- class DisplayManager(QObject):
25
+ class DisplayManager(QObject, LayerCollectionHandler):
22
26
  status_message_requested = Signal(str)
23
27
  cursor_preview_state_changed = Signal(bool)
24
28
 
25
29
  def __init__(self, layer_collection, image_viewer, master_thumbnail_label,
26
30
  thumbnail_list, parent=None):
27
- super().__init__(parent)
28
- layer_collection.add_to(self)
31
+ QObject.__init__(self, parent)
32
+ LayerCollectionHandler.__init__(self, layer_collection)
29
33
  self.image_viewer = image_viewer
30
34
  self.master_thumbnail_label = master_thumbnail_label
31
35
  self.thumbnail_list = thumbnail_list
@@ -67,7 +71,8 @@ class DisplayManager(QObject):
67
71
  qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
68
72
  else:
69
73
  qimg = QImage(layer.data, width, height, width, QImage.Format_Grayscale8)
70
- return QPixmap.fromImage(qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
74
+ return QPixmap.fromImage(
75
+ qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
71
76
 
72
77
  def update_thumbnails(self):
73
78
  self.update_master_thumbnail()
@@ -112,7 +117,8 @@ class DisplayManager(QObject):
112
117
  label_widget.setAlignment(Qt.AlignCenter)
113
118
 
114
119
  def rename_label(label_widget, old_label, i):
115
- new_label, ok = QInputDialog.getText(self.thumbnail_list, "Rename Label", "New label name:", text=old_label)
120
+ new_label, ok = QInputDialog.getText(
121
+ self.thumbnail_list, "Rename Label", "New label name:", text=old_label)
116
122
  if ok and new_label and new_label != old_label:
117
123
  label_widget.setText(new_label)
118
124
  self.set_layer_labels(i, new_label)
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611
1
2
  import os
2
3
  from PIL.TiffImagePlugin import IFDRational
3
4
  from PySide6.QtWidgets import QFormLayout, QHBoxLayout, QPushButton, QDialog, QLabel
@@ -1,7 +1,8 @@
1
- import numpy as np
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0718, R0914, E1101, R0911, R0912
2
+ import os
2
3
  import traceback
4
+ import numpy as np
3
5
  import cv2
4
- import os
5
6
  from psdtags import PsdChannelId
6
7
  from PySide6.QtCore import QThread, Signal
7
8
  from .. algorithms.utils import read_img
@@ -20,10 +21,11 @@ class FileLoader(QThread):
20
21
  try:
21
22
  current_stack, current_labels = self.load_stack(self.path)
22
23
  if current_stack is None or len(current_stack) == 0:
23
- self.error.emit("Empty or invalid stack")
24
+ self.error.emit(f"The file {self.path} does not contain a valid image.")
24
25
  return
25
26
  if current_labels:
26
- master_indices = [i for i, label in enumerate(current_labels) if label.lower() == "master"]
27
+ master_indices = [i for i, label in enumerate(current_labels)
28
+ if label.lower() == "master"]
27
29
  else:
28
30
  master_indices = []
29
31
  master_index = -1 if len(master_indices) == 0 else master_indices[0]
@@ -76,7 +78,8 @@ class FileLoader(QThread):
76
78
  if layers:
77
79
  stack = np.array(layers)
78
80
  if labels:
79
- master_indices = [i for i, label in enumerate(labels) if label.lower() == "master"]
81
+ master_indices = [i for i, label in enumerate(labels)
82
+ if label.lower() == "master"]
80
83
  if master_indices:
81
84
  master_index = master_indices[0]
82
85
  master_label = labels.pop(master_index)
@@ -86,8 +89,9 @@ class FileLoader(QThread):
86
89
  stack = np.insert(stack, 0, master_layer, axis=0)
87
90
  return stack, labels
88
91
  return stack, labels
89
- except ValueError as e:
90
- if str(e) == "TIFF file contains no ImageSourceData tag":
92
+ return None, None
93
+ except ValueError as val_err:
94
+ if str(val_err) == "TIFF file contains no ImageSourceData tag":
91
95
  try:
92
96
  stack = np.array([cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)])
93
97
  return stack, [path.split('/')[-1].split('.')[0]]
@@ -95,8 +99,8 @@ class FileLoader(QThread):
95
99
  traceback.print_tb(e.__traceback__)
96
100
  return None, None
97
101
  else:
98
- traceback.print_tb(e.__traceback__)
99
- raise e
102
+ traceback.print_tb(val_err.__traceback__)
103
+ raise val_err
100
104
  except Exception as e:
101
105
  traceback.print_tb(e.__traceback__)
102
106
  return None, None
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, C0116
1
2
  class FilterManager:
2
3
  def __init__(self, editor):
3
4
  self.editor = editor
@@ -1,21 +1,18 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902
1
2
  from PySide6.QtWidgets import QMainWindow, QMessageBox, QAbstractItemView
2
- from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
3
- from PySide6.QtCore import Qt, QPoint
4
3
  from .. config.constants import constants
5
- from .. config.gui_constants import gui_constants
6
4
  from .undo_manager import UndoManager
7
5
  from .layer_collection import LayerCollection
8
6
  from .io_gui_handler import IOGuiHandler
9
- from .brush_gradient import create_brush_gradient
10
7
  from .display_manager import DisplayManager
11
8
  from .brush_tool import BrushTool
9
+ from .layer_collection import LayerCollectionHandler
12
10
 
13
11
 
14
- class ImageEditor(QMainWindow):
12
+ class ImageEditor(QMainWindow, LayerCollectionHandler):
15
13
  def __init__(self):
16
- super().__init__()
17
- layer_collection = LayerCollection()
18
- layer_collection.add_to(self)
14
+ QMainWindow.__init__(self)
15
+ LayerCollectionHandler.__init__(self, LayerCollection())
19
16
  self.undo_manager = UndoManager()
20
17
  self.undo_action = None
21
18
  self.redo_action = None
@@ -25,10 +22,12 @@ class ImageEditor(QMainWindow):
25
22
  self.brush_tool = BrushTool()
26
23
  self.modified = False
27
24
  self.installEventFilter(self)
25
+ self.mask_layer = None
28
26
 
29
27
  def setup_ui(self):
30
- self.display_manager = DisplayManager(self.layer_collection, self.image_viewer,
31
- self.master_thumbnail_label, self.thumbnail_list, parent=self)
28
+ self.display_manager = DisplayManager(
29
+ self.layer_collection, self.image_viewer,
30
+ self.master_thumbnail_label, self.thumbnail_list, parent=self)
32
31
  self.io_gui_handler = IOGuiHandler(self.layer_collection, self.undo_manager, parent=self)
33
32
  self.display_manager.status_message_requested.connect(self.show_status_message)
34
33
  self.display_manager.cursor_preview_state_changed.connect(
@@ -46,6 +45,7 @@ class ImageEditor(QMainWindow):
46
45
  def show_status_message(self, message):
47
46
  self.statusBar().showMessage(message)
48
47
 
48
+ # pylint: disable=C0103
49
49
  def keyPressEvent(self, event):
50
50
  if self.image_viewer.empty:
51
51
  return
@@ -62,8 +62,9 @@ class ImageEditor(QMainWindow):
62
62
  self.brush_tool.increase_brush_hardness()
63
63
  return
64
64
  super().keyPressEvent(event)
65
+ # pylint: enable=C0103
65
66
 
66
- def _check_unsaved_changes(self) -> bool:
67
+ def check_unsaved_changes(self) -> bool:
67
68
  if self.modified:
68
69
  reply = QMessageBox.question(
69
70
  self, "Unsaved Changes",
@@ -124,7 +125,8 @@ class ImageEditor(QMainWindow):
124
125
 
125
126
  def highlight_thumbnail(self, index):
126
127
  self.thumbnail_list.setCurrentRow(index)
127
- self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
128
+ self.thumbnail_list.scrollToItem(
129
+ self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
128
130
 
129
131
  def copy_layer_to_master(self):
130
132
  if self.layer_stack() is None or self.master_layer() is None:
@@ -155,42 +157,6 @@ class ImageEditor(QMainWindow):
155
157
  view_pos, self.image_viewer)
156
158
  self.undo_manager.extend_undo_area(*area)
157
159
 
158
- def update_brush_thumb(self):
159
- width, height = gui_constants.UI_SIZES['brush_preview']
160
- pixmap = QPixmap(width, height)
161
- pixmap.fill(Qt.transparent)
162
- painter = QPainter(pixmap)
163
- painter.setRenderHint(QPainter.Antialiasing)
164
- preview_size = min(self.brush.size, width + 30, height + 30)
165
- center_x, center_y = width // 2, height // 2
166
- radius = preview_size // 2
167
- if self.image_viewer.cursor_style == 'preview':
168
- gradient = create_brush_gradient(
169
- center_x, center_y, radius,
170
- self.brush.hardness,
171
- inner_color=QColor(*gui_constants.BRUSH_COLORS['inner']),
172
- outer_color=QColor(*gui_constants.BRUSH_COLORS['gradient_end']),
173
- opacity=self.brush.opacity
174
- )
175
- painter.setBrush(QBrush(gradient))
176
- painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['outer']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
177
- elif self.image_viewer.cursor_style == 'outline':
178
- painter.setBrush(Qt.NoBrush)
179
- painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['outer']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
180
- else:
181
- painter.setBrush(QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner'])))
182
- painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
183
- painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
184
- if self.image_viewer.cursor_style == 'preview':
185
- painter.setPen(QPen(QColor(0, 0, 160)))
186
- painter.drawText(0, 10, f"Size: {int(self.brush.size)}px")
187
- painter.drawText(0, 25, f"Hardness: {self.brush.hardness}%")
188
- painter.drawText(0, 40, f"Opacity: {self.brush.opacity}%")
189
- painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
190
- painter.end()
191
- self.brush_preview.setPixmap(pixmap)
192
- self.image_viewer.update_brush_cursor()
193
-
194
160
  def begin_copy_brush_area(self, pos):
195
161
  if self.display_manager.allow_cursor_preview():
196
162
  self.mask_layer = self.io_gui_handler.blank_layer.copy()
@@ -1,4 +1,6 @@
1
- from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QListWidget, QSlider
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915
2
+ from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel,
3
+ QListWidget, QSlider)
2
4
  from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
3
5
  from PySide6.QtCore import Qt
4
6
  from PySide6.QtGui import QGuiApplication
@@ -14,7 +16,8 @@ def brush_size_to_slider(size):
14
16
  return 0
15
17
  if size >= gui_constants.BRUSH_SIZES['max']:
16
18
  return gui_constants.BRUSH_SIZE_SLIDER_MAX
17
- normalized = ((size - gui_constants.BRUSH_SIZES['min']) / gui_constants.BRUSH_SIZES['max']) ** (1 / gui_constants.BRUSH_GAMMA)
19
+ normalized = ((size - gui_constants.BRUSH_SIZES['min']) /
20
+ gui_constants.BRUSH_SIZES['max']) ** (1 / gui_constants.BRUSH_GAMMA)
18
21
  return int(normalized * gui_constants.BRUSH_SIZE_SLIDER_MAX)
19
22
 
20
23
 
@@ -25,6 +28,7 @@ class ImageEditorUI(ImageFilters):
25
28
  self.setup_ui()
26
29
  self.setup_menu()
27
30
  self.setup_shortcuts()
31
+ self._dialog = None
28
32
 
29
33
  def setup_shortcuts(self):
30
34
  prev_layer = QShortcut(QKeySequence(Qt.Key_Up), self, context=Qt.ApplicationShortcut)
@@ -129,7 +133,8 @@ class ImageEditorUI(ImageFilters):
129
133
  master_thumbnail_layout.setContentsMargins(2, 2, 2, 2)
130
134
  self.master_thumbnail_label = QLabel()
131
135
  self.master_thumbnail_label.setAlignment(Qt.AlignCenter)
132
- self.master_thumbnail_label.setFixedSize(gui_constants.THUMB_WIDTH, gui_constants.THUMB_HEIGHT)
136
+ self.master_thumbnail_label.setFixedSize(
137
+ gui_constants.THUMB_WIDTH, gui_constants.THUMB_HEIGHT)
133
138
  self.master_thumbnail_label.mousePressEvent = lambda e: self.set_view_master()
134
139
  master_thumbnail_layout.addWidget(self.master_thumbnail_label)
135
140
  side_layout.addWidget(self.master_thumbnail_frame)
@@ -313,7 +318,7 @@ class ImageEditorUI(ImageFilters):
313
318
  filter_menu = menubar.addMenu("&Filter")
314
319
  filter_menu.setObjectName("Filter")
315
320
  denoise_action = QAction("Denoise", self)
316
- denoise_action.triggered.connect(self.denoise)
321
+ denoise_action.triggered.connect(self.denoise_filter)
317
322
  filter_menu.addAction(denoise_action)
318
323
  unsharp_mask_action = QAction("Unsharp Mask", self)
319
324
  unsharp_mask_action.triggered.connect(self.unsharp_mask)
@@ -339,7 +344,7 @@ class ImageEditorUI(ImageFilters):
339
344
  self.window().showNormal()
340
345
 
341
346
  def quit(self):
342
- if self._check_unsaved_changes():
347
+ if self.check_unsaved_changes():
343
348
  self.close()
344
349
 
345
350
  def undo(self):
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, C0116, R0914
1
2
  import numpy as np
2
3
  from .image_editor import ImageEditor
3
4
  from .filter_manager import FilterManager
@@ -14,7 +15,7 @@ class ImageFilters(ImageEditor):
14
15
  self.filter_manager.register_filter("unsharp_mask", UnsharpMaskFilter)
15
16
  self.filter_manager.register_filter("white_balance", WhiteBalanceFilter)
16
17
 
17
- def denoise(self):
18
+ def denoise_filter(self):
18
19
  self.filter_manager.apply("denoise")
19
20
 
20
21
  def unsharp_mask(self):
@@ -47,7 +48,8 @@ class ImageFilters(ImageEditor):
47
48
  x1 = min(master_layer.shape[1], x + radius + 1)
48
49
  y0 = max(0, y - radius)
49
50
  y1 = min(master_layer.shape[0], y + radius + 1)
50
- mask = mask[radius - (y - y0): radius + (y1 - y), radius - (x - x0): radius + (x1 - x)]
51
+ mask = mask[radius - (y - y0): radius + (y1 - y),
52
+ radius - (x - x0): radius + (x1 - x)]
51
53
  region = master_layer[y0:y1, x0:x1]
52
54
  if region.size == 0:
53
55
  pixel = master_layer[y, x]
@@ -1,13 +1,15 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914
1
2
  import math
2
3
  from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
3
4
  from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence
4
5
  from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal
5
6
  from .. config.gui_constants import gui_constants
6
7
  from .brush_preview import BrushPreviewItem
7
- from .brush_gradient import create_brush_gradient
8
+ from .brush_gradient import create_default_brush_gradient
9
+ from .layer_collection import LayerCollectionHandler
8
10
 
9
11
 
10
- class ImageViewer(QGraphicsView):
12
+ class ImageViewer(QGraphicsView, LayerCollectionHandler):
11
13
  temp_view_requested = Signal(bool)
12
14
  brush_operation_started = Signal(QPoint)
13
15
  brush_operation_continued = Signal(QPoint)
@@ -15,7 +17,8 @@ class ImageViewer(QGraphicsView):
15
17
  brush_size_change_requested = Signal(int) # +1 or -1
16
18
 
17
19
  def __init__(self, layer_collection, parent=None):
18
- super().__init__(parent)
20
+ QGraphicsView.__init__(self, parent)
21
+ LayerCollectionHandler.__init__(self)
19
22
  self.display_manager = None
20
23
  self.layer_collection = layer_collection
21
24
  self.brush = None
@@ -43,11 +46,12 @@ class ImageViewer(QGraphicsView):
43
46
  self.scrolling = False
44
47
  self.dragging = False
45
48
  self.last_update_time = QTime.currentTime()
46
- self.brush_preview = BrushPreviewItem()
47
- self.layer_collection.add_to(self.brush_preview)
49
+ self.set_layer_collection(layer_collection)
50
+ self.brush_preview = BrushPreviewItem(self.layer_collection)
48
51
  self.scene.addItem(self.brush_preview)
49
52
  self.empty = True
50
53
  self.allow_cursor_preview = True
54
+ self.last_brush_pos = None
51
55
 
52
56
  def set_image(self, qimage):
53
57
  pixmap = QPixmap.fromImage(qimage)
@@ -73,12 +77,13 @@ class ImageViewer(QGraphicsView):
73
77
  self.scene.addItem(self.pixmap_item)
74
78
  self.zoom_factor = 1.0
75
79
  self.setup_brush_cursor()
76
- self.brush_preview = BrushPreviewItem()
80
+ self.brush_preview = BrushPreviewItem(self.layer_collection)
77
81
  self.scene.addItem(self.brush_preview)
78
82
  self.setCursor(Qt.ArrowCursor)
79
83
  self.brush_cursor.hide()
80
84
  self.empty = True
81
85
 
86
+ # pylint: disable=C0103
82
87
  def keyPressEvent(self, event):
83
88
  if self.empty:
84
89
  return
@@ -140,7 +145,8 @@ class ImageViewer(QGraphicsView):
140
145
  if self.dragging and event.buttons() & Qt.LeftButton:
141
146
  current_time = QTime.currentTime()
142
147
  if self.last_update_time.msecsTo(current_time) >= gui_constants.PAINT_REFRESH_TIMER:
143
- min_step = brush_size * gui_constants.MIN_MOUSE_STEP_BRUSH_FRACTION * self.zoom_factor
148
+ min_step = brush_size * \
149
+ gui_constants.MIN_MOUSE_STEP_BRUSH_FRACTION * self.zoom_factor
144
150
  x, y = position.x(), position.y()
145
151
  xp, yp = self.last_brush_pos.x(), self.last_brush_pos.y()
146
152
  distance = math.sqrt((x - xp)**2 + (y - yp)**2)
@@ -207,11 +213,29 @@ class ImageViewer(QGraphicsView):
207
213
  self.zoom_factor = new_scale
208
214
  self.update_brush_cursor()
209
215
 
216
+ def enterEvent(self, event):
217
+ self.activateWindow()
218
+ self.setFocus()
219
+ if not self.empty:
220
+ self.setCursor(Qt.BlankCursor)
221
+ if self.brush_cursor:
222
+ self.brush_cursor.show()
223
+ super().enterEvent(event)
224
+
225
+ def leaveEvent(self, event):
226
+ if not self.empty:
227
+ self.setCursor(Qt.ArrowCursor)
228
+ if self.brush_cursor:
229
+ self.brush_cursor.hide()
230
+ super().leaveEvent(event)
231
+ # pylint: enable=C0103
232
+
210
233
  def setup_brush_cursor(self):
211
234
  self.setCursor(Qt.BlankCursor)
212
235
  pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
213
236
  brush = QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner']))
214
- self.brush_cursor = self.scene.addEllipse(0, 0, self.brush.size, self.brush.size, pen, brush)
237
+ self.brush_cursor = self.scene.addEllipse(
238
+ 0, 0, self.brush.size, self.brush.size, pen, brush)
215
239
  self.brush_cursor.setZValue(1000)
216
240
  self.brush_cursor.hide()
217
241
 
@@ -256,33 +280,11 @@ class ImageViewer(QGraphicsView):
256
280
  self.brush_cursor.setBrush(Qt.NoBrush)
257
281
 
258
282
  def _setup_simple_brush_style(self, center_x, center_y, radius):
259
- gradient = create_brush_gradient(
260
- center_x, center_y, radius,
261
- self.brush.hardness,
262
- inner_color=QColor(*gui_constants.BRUSH_COLORS['inner']),
263
- outer_color=QColor(*gui_constants.BRUSH_COLORS['gradient_end']),
264
- opacity=self.brush.opacity
265
- )
283
+ gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
266
284
  self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
267
285
  gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor))
268
286
  self.brush_cursor.setBrush(QBrush(gradient))
269
287
 
270
- def enterEvent(self, event):
271
- self.activateWindow()
272
- self.setFocus()
273
- if not self.empty:
274
- self.setCursor(Qt.BlankCursor)
275
- if self.brush_cursor:
276
- self.brush_cursor.show()
277
- super().enterEvent(event)
278
-
279
- def leaveEvent(self, event):
280
- if not self.empty:
281
- self.setCursor(Qt.ArrowCursor)
282
- if self.brush_cursor:
283
- self.brush_cursor.hide()
284
- super().leaveEvent(event)
285
-
286
288
  def setup_shortcuts(self):
287
289
  prev_layer = QShortcut(QKeySequence(Qt.Key_Up), self, context=Qt.ApplicationShortcut)
288
290
  prev_layer.activated.connect(self.prev_layer)
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718
1
2
  import traceback
2
3
  import numpy as np
3
4
  from PySide6.QtWidgets import QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog, QApplication
@@ -6,18 +7,27 @@ from PySide6.QtCore import Qt, QObject, QTimer, Signal
6
7
  from .file_loader import FileLoader
7
8
  from .exif_data import ExifData
8
9
  from .io_manager import IOManager
10
+ from .layer_collection import LayerCollectionHandler
9
11
 
10
12
 
11
- class IOGuiHandler(QObject):
13
+ class IOGuiHandler(QObject, LayerCollectionHandler):
12
14
  status_message_requested = Signal(str)
13
15
  update_title_requested = Signal()
14
16
 
15
17
  def __init__(self, layer_collection, undo_manager, parent):
16
- super().__init__(parent)
18
+ QObject.__init__(self, parent)
19
+ LayerCollectionHandler.__init__(self)
17
20
  self.io_manager = IOManager(layer_collection)
18
21
  self.undo_manager = undo_manager
19
- layer_collection.add_to(self)
22
+ self.set_layer_collection(layer_collection)
20
23
  self.loader_thread = None
24
+ self.display_manager = None
25
+ self.image_viewer = None
26
+ self.modified = None
27
+ self.blank_layer = None
28
+ self.loading_dialog = None
29
+ self.loading_timer = None
30
+ self.exif_dialog = None
21
31
 
22
32
  def setup_ui(self, display_manager, image_viewer):
23
33
  self.display_manager = display_manager
@@ -49,13 +59,14 @@ class IOGuiHandler(QObject):
49
59
  self.loading_timer.stop()
50
60
  self.loading_dialog.accept()
51
61
  self.loading_dialog.deleteLater()
52
- QMessageBox.critical(self, "Error", error_msg)
62
+ QMessageBox.critical(self.parent(), "Error", error_msg)
53
63
  self.status_message_requested.emit(f"Error loading: {self.io_manager.current_file_path}")
54
64
 
55
65
  def open_file(self, file_paths=None):
56
66
  if file_paths is None:
57
67
  file_paths, _ = QFileDialog.getOpenFileNames(
58
- self.parent(), "Open Image", "", "Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
68
+ self.parent(), "Open Image", "",
69
+ "Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
59
70
  if not file_paths:
60
71
  return
61
72
  if self.loader_thread and self.loader_thread.isRunning():
@@ -84,8 +95,9 @@ class IOGuiHandler(QObject):
84
95
  self.loader_thread.start()
85
96
 
86
97
  def import_frames(self):
87
- file_paths, _ = QFileDialog.getOpenFileNames(self.parent(), "Select frames", "",
88
- "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
98
+ file_paths, _ = QFileDialog.getOpenFileNames(
99
+ self.parent(), "Select frames", "",
100
+ "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
89
101
  if file_paths:
90
102
  self.import_frames_from_files(file_paths)
91
103
  self.status_message_requested.emit("Imported selected frames")
@@ -168,8 +180,9 @@ class IOGuiHandler(QObject):
168
180
  def save_master_as(self):
169
181
  if self.layer_stack() is None:
170
182
  return
171
- path, _ = QFileDialog.getSaveFileName(self.parent(), "Save Image", "",
172
- "TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
183
+ path, _ = QFileDialog.getSaveFileName(
184
+ self.parent(), "Save Image", "",
185
+ "TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
173
186
  if path:
174
187
  self.save_master_to_path(path)
175
188
 
@@ -189,14 +202,13 @@ class IOGuiHandler(QObject):
189
202
  if path:
190
203
  self.io_manager.set_exif_data(path)
191
204
  self.status_message_requested.emit(f"EXIF data extracted from {path}.")
192
- self._exif_dialog = ExifData(self.io_manager.exif_data, self.parent())
193
- self._exif_dialog.exec()
205
+ self.exif_dialog = ExifData(self.io_manager.exif_data, self.parent())
206
+ self.exif_dialog.exec()
194
207
 
195
208
  def close_file(self):
196
- if self.parent()._check_unsaved_changes():
209
+ if self.parent().check_unsaved_changes():
197
210
  self.set_master_layer(None)
198
211
  self.blank_layer = None
199
- self.current_stack = None
200
212
  self.layer_collection.reset()
201
213
  self.io_manager.current_file_path = ''
202
214
  self.modified = False