shinestacker 0.3.1__py3-none-any.whl → 0.3.3__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.
- shinestacker/__init__.py +6 -6
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/balance.py +6 -7
- shinestacker/algorithms/noise_detection.py +2 -0
- shinestacker/app/open_frames.py +6 -4
- shinestacker/config/__init__.py +2 -1
- shinestacker/config/config.py +1 -0
- shinestacker/config/constants.py +1 -0
- shinestacker/config/gui_constants.py +1 -0
- shinestacker/core/__init__.py +4 -3
- shinestacker/core/colors.py +1 -0
- shinestacker/core/core_utils.py +6 -6
- shinestacker/core/exceptions.py +1 -0
- shinestacker/core/framework.py +2 -1
- shinestacker/gui/action_config.py +47 -42
- shinestacker/gui/actions_window.py +8 -5
- shinestacker/retouch/brush_preview.py +5 -6
- shinestacker/retouch/brush_tool.py +164 -0
- shinestacker/retouch/denoise_filter.py +56 -0
- shinestacker/retouch/display_manager.py +177 -0
- shinestacker/retouch/exif_data.py +2 -1
- shinestacker/retouch/filter_base.py +114 -0
- shinestacker/retouch/filter_manager.py +14 -0
- shinestacker/retouch/image_editor.py +104 -430
- shinestacker/retouch/image_editor_ui.py +32 -72
- shinestacker/retouch/image_filters.py +25 -349
- shinestacker/retouch/image_viewer.py +22 -14
- shinestacker/retouch/io_gui_handler.py +208 -0
- shinestacker/retouch/io_manager.py +9 -13
- shinestacker/retouch/layer_collection.py +65 -1
- shinestacker/retouch/unsharp_mask_filter.py +84 -0
- shinestacker/retouch/white_balance_filter.py +111 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/METADATA +3 -2
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/RECORD +38 -31
- shinestacker/retouch/brush_controller.py +0 -57
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/top_level.txt +0 -0
|
@@ -1,73 +1,68 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
from PySide6.
|
|
4
|
-
QVBoxLayout, QLabel, QDialog, QApplication)
|
|
5
|
-
from PySide6.QtGui import QPixmap, QPainter, QColor, QImage, QPen, QBrush, QGuiApplication, QCursor
|
|
6
|
-
from PySide6.QtCore import Qt, QTimer, QEvent, QPoint
|
|
1
|
+
from PySide6.QtWidgets import QMainWindow, QMessageBox, QAbstractItemView
|
|
2
|
+
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
|
|
3
|
+
from PySide6.QtCore import Qt, QPoint
|
|
7
4
|
from .. config.constants import constants
|
|
8
5
|
from .. config.gui_constants import gui_constants
|
|
9
|
-
from .brush import Brush
|
|
10
|
-
from .brush_controller import BrushController
|
|
11
6
|
from .undo_manager import UndoManager
|
|
12
|
-
from .file_loader import FileLoader
|
|
13
|
-
from .exif_data import ExifData
|
|
14
7
|
from .layer_collection import LayerCollection
|
|
15
|
-
from .
|
|
8
|
+
from .io_gui_handler import IOGuiHandler
|
|
16
9
|
from .brush_gradient import create_brush_gradient
|
|
10
|
+
from .display_manager import DisplayManager
|
|
11
|
+
from .brush_tool import BrushTool
|
|
17
12
|
|
|
18
13
|
|
|
19
14
|
class ImageEditor(QMainWindow):
|
|
20
15
|
def __init__(self):
|
|
21
16
|
super().__init__()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
self.view_mode = 'master'
|
|
25
|
-
self.temp_view_individual = False
|
|
26
|
-
self.modified = False
|
|
27
|
-
self.installEventFilter(self)
|
|
28
|
-
self.update_timer = QTimer(self)
|
|
29
|
-
self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
|
|
30
|
-
self.update_timer.timeout.connect(self.process_pending_updates)
|
|
31
|
-
self.needs_update = False
|
|
32
|
-
self.brush = Brush()
|
|
33
|
-
self.brush_controller = BrushController(self.brush)
|
|
17
|
+
layer_collection = LayerCollection()
|
|
18
|
+
layer_collection.add_to(self)
|
|
34
19
|
self.undo_manager = UndoManager()
|
|
35
20
|
self.undo_action = None
|
|
36
21
|
self.redo_action = None
|
|
37
22
|
self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
|
|
38
|
-
self.
|
|
23
|
+
self.io_gui_handler = None
|
|
24
|
+
self.display_manager = None
|
|
25
|
+
self.brush_tool = BrushTool()
|
|
26
|
+
self.modified = False
|
|
27
|
+
self.installEventFilter(self)
|
|
28
|
+
|
|
29
|
+
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)
|
|
32
|
+
self.io_gui_handler = IOGuiHandler(self.layer_collection, self.undo_manager, parent=self)
|
|
33
|
+
self.display_manager.status_message_requested.connect(self.show_status_message)
|
|
34
|
+
self.display_manager.cursor_preview_state_changed.connect(
|
|
35
|
+
lambda state: setattr(self.image_viewer, 'allow_cursor_preview', state))
|
|
36
|
+
self.io_gui_handler.status_message_requested.connect(self.show_status_message)
|
|
37
|
+
self.io_gui_handler.update_title_requested.connect(self.update_title)
|
|
38
|
+
self.brush_tool.setup_ui(self.brush, self.brush_preview, self.image_viewer,
|
|
39
|
+
self.brush_size_slider, self.hardness_slider, self.opacity_slider,
|
|
40
|
+
self.flow_slider)
|
|
41
|
+
self.image_viewer.brush = self.brush_tool.brush
|
|
42
|
+
self.brush_tool.update_brush_thumb()
|
|
43
|
+
self.io_gui_handler.setup_ui(self.display_manager, self.image_viewer)
|
|
44
|
+
self.image_viewer.display_manager = self.display_manager
|
|
45
|
+
|
|
46
|
+
def show_status_message(self, message):
|
|
47
|
+
self.statusBar().showMessage(message)
|
|
39
48
|
|
|
40
49
|
def keyPressEvent(self, event):
|
|
41
50
|
if self.image_viewer.empty:
|
|
42
51
|
return
|
|
43
|
-
|
|
44
|
-
self.decrease_brush_size()
|
|
52
|
+
if event.text() == '[':
|
|
53
|
+
self.brush_tool.decrease_brush_size()
|
|
45
54
|
return
|
|
46
|
-
|
|
47
|
-
self.increase_brush_size()
|
|
55
|
+
if event.text() == ']':
|
|
56
|
+
self.brush_tool.increase_brush_size()
|
|
48
57
|
return
|
|
49
|
-
|
|
50
|
-
self.decrease_brush_hardness()
|
|
58
|
+
if event.text() == '{':
|
|
59
|
+
self.brush_tool.decrease_brush_hardness()
|
|
51
60
|
return
|
|
52
|
-
|
|
53
|
-
self.increase_brush_hardness()
|
|
61
|
+
if event.text() == '}':
|
|
62
|
+
self.brush_tool.increase_brush_hardness()
|
|
54
63
|
return
|
|
55
64
|
super().keyPressEvent(event)
|
|
56
65
|
|
|
57
|
-
def process_pending_updates(self):
|
|
58
|
-
if self.needs_update:
|
|
59
|
-
self.display_master_layer()
|
|
60
|
-
self.needs_update = False
|
|
61
|
-
|
|
62
|
-
def eventFilter(self, obj, event):
|
|
63
|
-
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_X:
|
|
64
|
-
self.start_temp_view()
|
|
65
|
-
return True
|
|
66
|
-
elif event.type() == QEvent.KeyRelease and event.key() == Qt.Key_X:
|
|
67
|
-
self.end_temp_view()
|
|
68
|
-
return True
|
|
69
|
-
return super().eventFilter(obj, event)
|
|
70
|
-
|
|
71
66
|
def _check_unsaved_changes(self) -> bool:
|
|
72
67
|
if self.modified:
|
|
73
68
|
reply = QMessageBox.question(
|
|
@@ -78,328 +73,52 @@ class ImageEditor(QMainWindow):
|
|
|
78
73
|
if reply == QMessageBox.Save:
|
|
79
74
|
self.save_file()
|
|
80
75
|
return True
|
|
81
|
-
|
|
76
|
+
if reply == QMessageBox.Discard:
|
|
82
77
|
return True
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
else:
|
|
86
|
-
return True
|
|
78
|
+
return False
|
|
79
|
+
return True
|
|
87
80
|
|
|
88
81
|
def sort_layers(self, order):
|
|
89
|
-
self.
|
|
90
|
-
self.update_thumbnails()
|
|
91
|
-
self.change_layer(self.
|
|
82
|
+
self.sort_layers(order)
|
|
83
|
+
self.display_manager.update_thumbnails()
|
|
84
|
+
self.change_layer(self.current_layer())
|
|
92
85
|
|
|
93
86
|
def update_title(self):
|
|
94
87
|
title = constants.APP_TITLE
|
|
95
|
-
if self.
|
|
96
|
-
|
|
97
|
-
if
|
|
98
|
-
title += "
|
|
88
|
+
if self.io_gui_handler is not None:
|
|
89
|
+
path = self.io_gui_handler.io_manager.current_file_path
|
|
90
|
+
if path != '':
|
|
91
|
+
title += f" - {path.split('/')[-1]}"
|
|
92
|
+
if self.modified:
|
|
93
|
+
title += " *"
|
|
99
94
|
self.window().setWindowTitle(title)
|
|
100
95
|
|
|
101
|
-
def open_file(self, file_paths=None):
|
|
102
|
-
if file_paths is None:
|
|
103
|
-
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
104
|
-
self, "Open Image", "", "Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
|
|
105
|
-
if not file_paths:
|
|
106
|
-
return
|
|
107
|
-
if self.loader_thread and self.loader_thread.isRunning():
|
|
108
|
-
if not self.loader_thread.wait(10000):
|
|
109
|
-
raise RuntimeError("Loading timeout error.")
|
|
110
|
-
if isinstance(file_paths, list) and len(file_paths) > 1:
|
|
111
|
-
self.import_frames_from_files(file_paths)
|
|
112
|
-
return
|
|
113
|
-
path = file_paths[0] if isinstance(file_paths, list) else file_paths
|
|
114
|
-
self.io_manager.current_file_path = path
|
|
115
|
-
QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
|
|
116
|
-
self.loading_dialog = QDialog(self)
|
|
117
|
-
self.loading_dialog.setWindowTitle("Loading")
|
|
118
|
-
self.loading_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
|
|
119
|
-
self.loading_dialog.setModal(True)
|
|
120
|
-
layout = QVBoxLayout()
|
|
121
|
-
layout.addWidget(QLabel("File loading..."))
|
|
122
|
-
self.loading_dialog.setLayout(layout)
|
|
123
|
-
self.loading_timer = QTimer()
|
|
124
|
-
self.loading_timer.setSingleShot(True)
|
|
125
|
-
self.loading_timer.timeout.connect(self.loading_dialog.show)
|
|
126
|
-
self.loading_timer.start(100)
|
|
127
|
-
self.loader_thread = FileLoader(path)
|
|
128
|
-
self.loader_thread.finished.connect(self.on_file_loaded)
|
|
129
|
-
self.loader_thread.error.connect(self.on_file_error)
|
|
130
|
-
self.loader_thread.start()
|
|
131
|
-
|
|
132
|
-
def on_file_loaded(self, stack, labels, master_layer):
|
|
133
|
-
QApplication.restoreOverrideCursor()
|
|
134
|
-
self.loading_timer.stop()
|
|
135
|
-
self.loading_dialog.hide()
|
|
136
|
-
self.layer_collection.layer_stack = stack
|
|
137
|
-
if labels is None:
|
|
138
|
-
self.layer_collection.layer_labels = [f'Layer {i:03d}' for i in range(len(stack))]
|
|
139
|
-
else:
|
|
140
|
-
self.layer_collection.layer_labels = labels
|
|
141
|
-
self.layer_collection.master_layer = master_layer
|
|
142
|
-
self.modified = False
|
|
143
|
-
self.undo_manager.reset()
|
|
144
|
-
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
145
|
-
self.update_thumbnails()
|
|
146
|
-
self.image_viewer.setup_brush_cursor()
|
|
147
|
-
self.change_layer(0)
|
|
148
|
-
self.image_viewer.reset_zoom()
|
|
149
|
-
self.statusBar().showMessage(f"Loaded: {self.io_manager.current_file_path}")
|
|
150
|
-
self.thumbnail_list.setFocus()
|
|
151
|
-
self.update_title()
|
|
152
|
-
|
|
153
|
-
def on_file_error(self, error_msg):
|
|
154
|
-
QApplication.restoreOverrideCursor()
|
|
155
|
-
self.loading_timer.stop()
|
|
156
|
-
self.loading_dialog.accept()
|
|
157
|
-
self.loading_dialog.deleteLater()
|
|
158
|
-
QMessageBox.critical(self, "Error", error_msg)
|
|
159
|
-
self.statusBar().showMessage(f"Error loading: {self.io_manager.current_file_path}")
|
|
160
|
-
|
|
161
96
|
def mark_as_modified(self):
|
|
162
97
|
self.modified = True
|
|
163
98
|
self.update_title()
|
|
164
99
|
|
|
165
|
-
def import_frames(self):
|
|
166
|
-
file_paths, _ = QFileDialog.getOpenFileNames(self, "Select frames", "",
|
|
167
|
-
"Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
|
|
168
|
-
if file_paths:
|
|
169
|
-
self.import_frames_from_files(file_paths)
|
|
170
|
-
self.statusBar().showMessage("Imported selected frames")
|
|
171
|
-
|
|
172
|
-
def import_frames_from_files(self, file_paths):
|
|
173
|
-
try:
|
|
174
|
-
stack, labels, master = self.io_manager.import_frames(file_paths)
|
|
175
|
-
except Exception as e:
|
|
176
|
-
msg = QMessageBox()
|
|
177
|
-
msg.setIcon(QMessageBox.Critical)
|
|
178
|
-
msg.setWindowTitle("Import error")
|
|
179
|
-
msg.setText(str(e))
|
|
180
|
-
msg.exec()
|
|
181
|
-
return
|
|
182
|
-
if self.layer_collection.layer_stack is None and len(stack) > 0:
|
|
183
|
-
self.layer_collection.layer_stack = np.array(stack)
|
|
184
|
-
self.layer_collection.layer_labels = labels
|
|
185
|
-
self.layer_collection.master_layer = master
|
|
186
|
-
self.blank_layer = np.zeros(master.shape[:2])
|
|
187
|
-
else:
|
|
188
|
-
for img, label in zip(stack, labels):
|
|
189
|
-
self.layer_collection.layer_labels.append(label)
|
|
190
|
-
self.layer_collection.layer_stack = np.append(
|
|
191
|
-
self.layer_collection.layer_stack, [img], axis=0)
|
|
192
|
-
self.mark_as_modified()
|
|
193
|
-
self.change_layer(0)
|
|
194
|
-
self.image_viewer.reset_zoom()
|
|
195
|
-
self.thumbnail_list.setFocus()
|
|
196
|
-
self.update_thumbnails()
|
|
197
|
-
|
|
198
|
-
def save_file(self):
|
|
199
|
-
if self.save_master_only.isChecked():
|
|
200
|
-
self.save_master()
|
|
201
|
-
else:
|
|
202
|
-
self.save_multilayer()
|
|
203
|
-
|
|
204
|
-
def save_file_as(self):
|
|
205
|
-
if self.save_master_only.isChecked():
|
|
206
|
-
self.save_master_as()
|
|
207
|
-
else:
|
|
208
|
-
self.save_multilayer_as()
|
|
209
|
-
|
|
210
|
-
def save_multilayer(self):
|
|
211
|
-
if self.layer_collection.layer_stack is None:
|
|
212
|
-
return
|
|
213
|
-
if self.current_file_path != '':
|
|
214
|
-
extension = self.current_file_path.split('.')[-1]
|
|
215
|
-
if extension in ['tif', 'tiff']:
|
|
216
|
-
self.save_multilayer_to_path(self.current_file_path)
|
|
217
|
-
return
|
|
218
|
-
self.save_multilayer_file_as()
|
|
219
|
-
|
|
220
|
-
def save_multilayer_as(self):
|
|
221
|
-
if self.layer_collection.layer_stack is None:
|
|
222
|
-
return
|
|
223
|
-
path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
|
|
224
|
-
"TIFF Files (*.tif *.tiff);;All Files (*)")
|
|
225
|
-
if path:
|
|
226
|
-
if not path.lower().endswith(('.tif', '.tiff')):
|
|
227
|
-
path += '.tif'
|
|
228
|
-
self.save_multilayer_to_path(path)
|
|
229
|
-
|
|
230
|
-
def save_multilayer_to_path(self, path):
|
|
231
|
-
try:
|
|
232
|
-
self.io_manager.save_multilayer(path)
|
|
233
|
-
self.io_manager.current_file_path = path
|
|
234
|
-
self.modified = False
|
|
235
|
-
self.update_title()
|
|
236
|
-
self.statusBar().showMessage(f"Saved multilayer to: {path}")
|
|
237
|
-
except Exception as e:
|
|
238
|
-
traceback.print_tb(e.__traceback__)
|
|
239
|
-
QMessageBox.critical(self, "Save Error", f"Could not save file: {str(e)}")
|
|
240
|
-
|
|
241
|
-
def save_master(self):
|
|
242
|
-
if self.layer_collection.master_layer is None:
|
|
243
|
-
return
|
|
244
|
-
if self.current_file_path != '':
|
|
245
|
-
self.save_master_to_path(self.current_file_path)
|
|
246
|
-
return
|
|
247
|
-
self.save_master_as()
|
|
248
|
-
|
|
249
|
-
def save_master_as(self):
|
|
250
|
-
if self.layer_collection.layer_stack is None:
|
|
251
|
-
return
|
|
252
|
-
path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
|
|
253
|
-
"TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
|
|
254
|
-
if path:
|
|
255
|
-
self.save_master_to_path(path)
|
|
256
|
-
|
|
257
|
-
def save_master_to_path(self, path):
|
|
258
|
-
try:
|
|
259
|
-
self.io_manager.save_master(path)
|
|
260
|
-
self.io_manager.current_file_path = path
|
|
261
|
-
self.modified = False
|
|
262
|
-
self.update_title()
|
|
263
|
-
self.statusBar().showMessage(f"Saved master layer to: {path}")
|
|
264
|
-
except Exception as e:
|
|
265
|
-
traceback.print_tb(e.__traceback__)
|
|
266
|
-
QMessageBox.critical(self, "Save Error", f"Could not save file: {str(e)}")
|
|
267
|
-
|
|
268
|
-
def select_exif_path(self):
|
|
269
|
-
path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
|
|
270
|
-
if path:
|
|
271
|
-
self.io_manager.set_exif_data(path)
|
|
272
|
-
self.statusBar().showMessage(f"EXIF data extracted from {path}.")
|
|
273
|
-
self._exif_dialog = ExifData(self.io_manager.exif_data, self)
|
|
274
|
-
self._exif_dialog.exec()
|
|
275
|
-
|
|
276
|
-
def close_file(self):
|
|
277
|
-
if self._check_unsaved_changes():
|
|
278
|
-
self.layer_collection.master_layer = None
|
|
279
|
-
self.blank_layer = None
|
|
280
|
-
self.current_stack = None
|
|
281
|
-
self.layer_collection.master_layer = None
|
|
282
|
-
self.layer_collection.current_layer_idx = 0
|
|
283
|
-
self.io_manager.current_file_path = ''
|
|
284
|
-
self.modified = False
|
|
285
|
-
self.undo_manager.reset()
|
|
286
|
-
self.image_viewer.clear_image()
|
|
287
|
-
self.update_thumbnails()
|
|
288
|
-
self.update_title()
|
|
289
|
-
|
|
290
|
-
def set_view_master(self):
|
|
291
|
-
self.view_mode = 'master'
|
|
292
|
-
self.temp_view_individual = False
|
|
293
|
-
self.display_master_layer()
|
|
294
|
-
self.statusBar().showMessage("View mode: Master")
|
|
295
|
-
|
|
296
|
-
def set_view_individual(self):
|
|
297
|
-
self.view_mode = 'individual'
|
|
298
|
-
self.temp_view_individual = False
|
|
299
|
-
self.display_current_layer()
|
|
300
|
-
self.statusBar().showMessage("View mode: Individual layers")
|
|
301
|
-
|
|
302
|
-
def start_temp_view(self):
|
|
303
|
-
if not self.temp_view_individual and self.view_mode == 'master':
|
|
304
|
-
self.temp_view_individual = True
|
|
305
|
-
self.image_viewer.update_brush_cursor()
|
|
306
|
-
self.display_current_layer()
|
|
307
|
-
self.statusBar().showMessage("Temporary view: Individual layer (hold X)")
|
|
308
|
-
|
|
309
|
-
def end_temp_view(self):
|
|
310
|
-
if self.temp_view_individual:
|
|
311
|
-
self.temp_view_individual = False
|
|
312
|
-
self.image_viewer.update_brush_cursor()
|
|
313
|
-
self.display_master_layer()
|
|
314
|
-
self.statusBar().showMessage("View mode: Master")
|
|
315
|
-
|
|
316
|
-
def display_current_view(self):
|
|
317
|
-
if self.temp_view_individual or self.view_mode == 'individual':
|
|
318
|
-
self.display_current_layer()
|
|
319
|
-
else:
|
|
320
|
-
self.display_master_layer()
|
|
321
|
-
|
|
322
|
-
def display_master_layer(self):
|
|
323
|
-
if self.layer_collection.master_layer is None:
|
|
324
|
-
self.image_viewer.clear_image()
|
|
325
|
-
else:
|
|
326
|
-
qimage = self.numpy_to_qimage(self.layer_collection.master_layer)
|
|
327
|
-
self.image_viewer.set_image(qimage)
|
|
328
|
-
|
|
329
|
-
def create_thumbnail(self, layer, size):
|
|
330
|
-
if layer.dtype == np.uint16:
|
|
331
|
-
layer = (layer // 256).astype(np.uint8)
|
|
332
|
-
height, width = layer.shape[:2]
|
|
333
|
-
if layer.ndim == 3 and layer.shape[-1] == 3:
|
|
334
|
-
qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
|
|
335
|
-
else:
|
|
336
|
-
qimg = QImage(layer.data, width, height, width, QImage.Format_Grayscale8)
|
|
337
|
-
return QPixmap.fromImage(qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
|
|
338
|
-
|
|
339
|
-
def update_master_thumbnail(self):
|
|
340
|
-
if self.layer_collection.master_layer is None:
|
|
341
|
-
self.master_thumbnail_label.clear()
|
|
342
|
-
else:
|
|
343
|
-
thumb_size = gui_constants.UI_SIZES['thumbnail']
|
|
344
|
-
master_thumb = self.create_thumbnail(self.layer_collection.master_layer, thumb_size)
|
|
345
|
-
self.master_thumbnail_label.setPixmap(master_thumb)
|
|
346
|
-
|
|
347
|
-
def update_thumbnails(self):
|
|
348
|
-
self.update_master_thumbnail()
|
|
349
|
-
self.thumbnail_list.clear()
|
|
350
|
-
thumb_size = gui_constants.UI_SIZES['thumbnail']
|
|
351
|
-
if self.layer_collection.layer_stack is None:
|
|
352
|
-
return
|
|
353
|
-
for i, (layer, label) in enumerate(zip(self.layer_collection.layer_stack, self.layer_collection.layer_labels)):
|
|
354
|
-
thumbnail = self.create_thumbnail(layer, thumb_size)
|
|
355
|
-
self._add_thumbnail_item(thumbnail, label, i, i == self.layer_collection.current_layer_idx)
|
|
356
|
-
|
|
357
|
-
def _add_thumbnail_item(self, thumbnail, label, i, is_current):
|
|
358
|
-
pass
|
|
359
|
-
|
|
360
100
|
def change_layer(self, layer_idx):
|
|
361
|
-
if 0 <= layer_idx < self.
|
|
101
|
+
if 0 <= layer_idx < self.number_of_layers():
|
|
362
102
|
view_state = self.image_viewer.get_view_state()
|
|
363
|
-
self.
|
|
364
|
-
self.display_current_view()
|
|
103
|
+
self.set_current_layer_idx(layer_idx)
|
|
104
|
+
self.display_manager.display_current_view()
|
|
365
105
|
self.image_viewer.set_view_state(view_state)
|
|
366
106
|
self.thumbnail_list.setCurrentRow(layer_idx)
|
|
367
107
|
self.thumbnail_list.setFocus()
|
|
368
108
|
self.image_viewer.update_brush_cursor()
|
|
369
109
|
self.image_viewer.setFocus()
|
|
370
110
|
|
|
371
|
-
def display_current_layer(self):
|
|
372
|
-
if self.layer_collection.layer_stack is None:
|
|
373
|
-
return
|
|
374
|
-
layer = self.layer_collection.current_layer()
|
|
375
|
-
qimage = self.numpy_to_qimage(layer)
|
|
376
|
-
self.image_viewer.set_image(qimage)
|
|
377
|
-
|
|
378
|
-
def numpy_to_qimage(self, array):
|
|
379
|
-
if array.dtype == np.uint16:
|
|
380
|
-
array = np.right_shift(array, 8).astype(np.uint8)
|
|
381
|
-
|
|
382
|
-
if array.ndim == 2:
|
|
383
|
-
height, width = array.shape
|
|
384
|
-
return QImage(memoryview(array), width, height, width, QImage.Format_Grayscale8)
|
|
385
|
-
elif array.ndim == 3:
|
|
386
|
-
height, width, _ = array.shape
|
|
387
|
-
if not array.flags['C_CONTIGUOUS']:
|
|
388
|
-
array = np.ascontiguousarray(array)
|
|
389
|
-
return QImage(memoryview(array), width, height, 3 * width, QImage.Format_RGB888)
|
|
390
|
-
return QImage()
|
|
391
|
-
|
|
392
111
|
def prev_layer(self):
|
|
393
|
-
if self.
|
|
394
|
-
new_idx = max(0, self.
|
|
395
|
-
if new_idx != self.
|
|
112
|
+
if self.layer_stack() is not None:
|
|
113
|
+
new_idx = max(0, self.current_layer_idx() - 1)
|
|
114
|
+
if new_idx != self.current_layer_idx():
|
|
396
115
|
self.change_layer(new_idx)
|
|
397
116
|
self.highlight_thumbnail(new_idx)
|
|
398
117
|
|
|
399
118
|
def next_layer(self):
|
|
400
|
-
if self.
|
|
401
|
-
new_idx = min(self.
|
|
402
|
-
if new_idx != self.
|
|
119
|
+
if self.layer_stack() is not None:
|
|
120
|
+
new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
|
|
121
|
+
if new_idx != self.current_layer_idx():
|
|
403
122
|
self.change_layer(new_idx)
|
|
404
123
|
self.highlight_thumbnail(new_idx)
|
|
405
124
|
|
|
@@ -407,48 +126,34 @@ class ImageEditor(QMainWindow):
|
|
|
407
126
|
self.thumbnail_list.setCurrentRow(index)
|
|
408
127
|
self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
409
128
|
|
|
410
|
-
def
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
val = max(self.brush_size_slider.value() - amount, self.brush_size_slider.minimum())
|
|
428
|
-
self.brush_size_slider.setValue(val)
|
|
429
|
-
self.update_brush_size(val)
|
|
430
|
-
|
|
431
|
-
def increase_brush_hardness(self, amount=2):
|
|
432
|
-
val = min(self.hardness_slider.value() + amount, self.hardness_slider.maximum())
|
|
433
|
-
self.hardness_slider.setValue(val)
|
|
434
|
-
self.update_brush_hardness(val)
|
|
435
|
-
|
|
436
|
-
def decrease_brush_hardness(self, amount=2):
|
|
437
|
-
val = max(self.hardness_slider.value() - amount, self.hardness_slider.minimum())
|
|
438
|
-
self.hardness_slider.setValue(val)
|
|
439
|
-
self.update_brush_hardness(val)
|
|
440
|
-
|
|
441
|
-
def update_brush_hardness(self, hardness):
|
|
442
|
-
self.brush.hardness = hardness
|
|
443
|
-
self.update_brush_thumb()
|
|
444
|
-
|
|
445
|
-
def update_brush_opacity(self, opacity):
|
|
446
|
-
self.brush.opacity = opacity
|
|
447
|
-
self.update_brush_thumb()
|
|
129
|
+
def copy_layer_to_master(self):
|
|
130
|
+
if self.layer_stack() is None or self.master_layer() is None:
|
|
131
|
+
return
|
|
132
|
+
reply = QMessageBox.question(
|
|
133
|
+
self,
|
|
134
|
+
"Confirm Copy",
|
|
135
|
+
"Warning: the current master layer will be erased\n\nDo you want to continue?",
|
|
136
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
137
|
+
QMessageBox.No
|
|
138
|
+
)
|
|
139
|
+
if reply == QMessageBox.Yes:
|
|
140
|
+
self.set_master_layer(self.current_layer().copy())
|
|
141
|
+
self.master_layer().setflags(write=True)
|
|
142
|
+
self.display_manager.display_current_view()
|
|
143
|
+
self.display_manager.update_thumbnails()
|
|
144
|
+
self.mark_as_modified()
|
|
145
|
+
self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
|
|
448
146
|
|
|
449
|
-
def
|
|
450
|
-
self.
|
|
451
|
-
|
|
147
|
+
def copy_brush_area_to_master(self, view_pos):
|
|
148
|
+
if self.layer_stack() is None or self.number_of_layers() == 0 \
|
|
149
|
+
or not self.display_manager.allow_cursor_preview():
|
|
150
|
+
return
|
|
151
|
+
area = self.brush_tool.apply_brush_operation(
|
|
152
|
+
self.master_layer_copy(),
|
|
153
|
+
self.current_layer(),
|
|
154
|
+
self.master_layer(), self.mask_layer,
|
|
155
|
+
view_pos, self.image_viewer)
|
|
156
|
+
self.undo_manager.extend_undo_area(*area)
|
|
452
157
|
|
|
453
158
|
def update_brush_thumb(self):
|
|
454
159
|
width, height = gui_constants.UI_SIZES['brush_preview']
|
|
@@ -486,62 +191,31 @@ class ImageEditor(QMainWindow):
|
|
|
486
191
|
self.brush_preview.setPixmap(pixmap)
|
|
487
192
|
self.image_viewer.update_brush_cursor()
|
|
488
193
|
|
|
489
|
-
def allow_cursor_preview(self):
|
|
490
|
-
return self.view_mode == 'master' and not self.temp_view_individual
|
|
491
|
-
|
|
492
|
-
def copy_layer_to_master(self):
|
|
493
|
-
if self.layer_collection.layer_stack is None or self.layer_collection.master_layer is None:
|
|
494
|
-
return
|
|
495
|
-
reply = QMessageBox.question(
|
|
496
|
-
self,
|
|
497
|
-
"Confirm Copy",
|
|
498
|
-
"Warning: the current master layer will be erased\n\nDo you want to continue?",
|
|
499
|
-
QMessageBox.Yes | QMessageBox.No,
|
|
500
|
-
QMessageBox.No
|
|
501
|
-
)
|
|
502
|
-
if reply == QMessageBox.Yes:
|
|
503
|
-
self.layer_collection.master_layer = self.layer_collection.current_layer().copy()
|
|
504
|
-
self.layer_collection.master_layer.setflags(write=True)
|
|
505
|
-
self.display_current_view()
|
|
506
|
-
self.update_thumbnails()
|
|
507
|
-
self.mark_as_modified()
|
|
508
|
-
self.statusBar().showMessage(f"Copied layer {self.layer_collection.current_layer_idx + 1} to master")
|
|
509
|
-
|
|
510
|
-
def copy_brush_area_to_master(self, view_pos):
|
|
511
|
-
if self.layer_collection.layer_stack is None or self.layer_collection.number_of_layers() == 0 \
|
|
512
|
-
or self.view_mode != 'master' or self.temp_view_individual:
|
|
513
|
-
return
|
|
514
|
-
area = self.brush_controller.apply_brush_operation(self.layer_collection.master_layer_copy,
|
|
515
|
-
self.layer_collection.current_layer(),
|
|
516
|
-
self.layer_collection.master_layer, self.mask_layer,
|
|
517
|
-
view_pos, self.image_viewer)
|
|
518
|
-
self.undo_manager.extend_undo_area(*area)
|
|
519
|
-
|
|
520
194
|
def begin_copy_brush_area(self, pos):
|
|
521
|
-
if self.
|
|
522
|
-
self.mask_layer = self.blank_layer.copy()
|
|
523
|
-
self.
|
|
195
|
+
if self.display_manager.allow_cursor_preview():
|
|
196
|
+
self.mask_layer = self.io_gui_handler.blank_layer.copy()
|
|
197
|
+
self.copy_master_layer()
|
|
524
198
|
self.undo_manager.reset_undo_area()
|
|
525
199
|
self.copy_brush_area_to_master(pos)
|
|
526
|
-
self.needs_update = True
|
|
527
|
-
if not self.update_timer.isActive():
|
|
528
|
-
self.update_timer.start()
|
|
200
|
+
self.display_manager.needs_update = True
|
|
201
|
+
if not self.display_manager.update_timer.isActive():
|
|
202
|
+
self.display_manager.update_timer.start()
|
|
529
203
|
self.mark_as_modified()
|
|
530
204
|
|
|
531
205
|
def continue_copy_brush_area(self, pos):
|
|
532
|
-
if self.
|
|
206
|
+
if self.display_manager.allow_cursor_preview():
|
|
533
207
|
self.copy_brush_area_to_master(pos)
|
|
534
|
-
self.needs_update = True
|
|
535
|
-
if not self.update_timer.isActive():
|
|
536
|
-
self.update_timer.start()
|
|
208
|
+
self.display_manager.needs_update = True
|
|
209
|
+
if not self.display_manager.update_timer.isActive():
|
|
210
|
+
self.display_manager.update_timer.start()
|
|
537
211
|
self.mark_as_modified()
|
|
538
212
|
|
|
539
213
|
def end_copy_brush_area(self):
|
|
540
|
-
if self.update_timer.isActive():
|
|
541
|
-
self.display_master_layer()
|
|
542
|
-
self.update_master_thumbnail()
|
|
543
|
-
self.undo_manager.save_undo_state(self.
|
|
544
|
-
self.update_timer.stop()
|
|
214
|
+
if self.display_manager.update_timer.isActive():
|
|
215
|
+
self.display_manager.display_master_layer()
|
|
216
|
+
self.display_manager.update_master_thumbnail()
|
|
217
|
+
self.undo_manager.save_undo_state(self.master_layer_copy(), 'Brush Stroke')
|
|
218
|
+
self.display_manager.update_timer.stop()
|
|
545
219
|
self.mark_as_modified()
|
|
546
220
|
|
|
547
221
|
def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
|