shinestacker 0.2.2__py3-none-any.whl → 0.3.1__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/_version.py +1 -1
- shinestacker/algorithms/denoise.py +9 -0
- shinestacker/algorithms/sharpen.py +22 -0
- shinestacker/algorithms/stack.py +2 -2
- shinestacker/algorithms/utils.py +4 -0
- shinestacker/algorithms/white_balance.py +13 -0
- shinestacker/gui/new_project.py +1 -0
- shinestacker/retouch/brush_controller.py +4 -4
- shinestacker/retouch/brush_gradient.py +20 -0
- shinestacker/retouch/brush_preview.py +11 -14
- shinestacker/retouch/image_editor.py +114 -202
- shinestacker/retouch/image_editor_ui.py +42 -13
- shinestacker/retouch/image_filters.py +391 -0
- shinestacker/retouch/image_viewer.py +13 -21
- shinestacker/retouch/io_manager.py +57 -0
- shinestacker/retouch/layer_collection.py +54 -0
- shinestacker/retouch/undo_manager.py +49 -11
- {shinestacker-0.2.2.dist-info → shinestacker-0.3.1.dist-info}/METADATA +27 -3
- {shinestacker-0.2.2.dist-info → shinestacker-0.3.1.dist-info}/RECORD +23 -16
- {shinestacker-0.2.2.dist-info → shinestacker-0.3.1.dist-info}/WHEEL +0 -0
- {shinestacker-0.2.2.dist-info → shinestacker-0.3.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.2.2.dist-info → shinestacker-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.2.2.dist-info → shinestacker-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -1,61 +1,28 @@
|
|
|
1
1
|
import traceback
|
|
2
2
|
import numpy as np
|
|
3
|
-
import cv2
|
|
4
3
|
from PySide6.QtWidgets import (QMainWindow, QFileDialog, QMessageBox, QAbstractItemView,
|
|
5
4
|
QVBoxLayout, QLabel, QDialog, QApplication)
|
|
6
|
-
from PySide6.QtGui import QPixmap, QPainter, QColor, QImage, QPen, QBrush,
|
|
5
|
+
from PySide6.QtGui import QPixmap, QPainter, QColor, QImage, QPen, QBrush, QGuiApplication, QCursor
|
|
7
6
|
from PySide6.QtCore import Qt, QTimer, QEvent, QPoint
|
|
8
7
|
from .. config.constants import constants
|
|
9
8
|
from .. config.gui_constants import gui_constants
|
|
10
|
-
from .. core.exceptions import ShapeError, BitDepthError
|
|
11
|
-
from .. algorithms.exif import get_exif, write_image_with_exif_data
|
|
12
|
-
from .. algorithms.multilayer import write_multilayer_tiff_from_images
|
|
13
|
-
from .. algorithms.utils import read_img, validate_image, get_img_metadata
|
|
14
9
|
from .brush import Brush
|
|
15
10
|
from .brush_controller import BrushController
|
|
16
11
|
from .undo_manager import UndoManager
|
|
17
12
|
from .file_loader import FileLoader
|
|
18
13
|
from .exif_data import ExifData
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
normalized = slider_val / gui_constants.BRUSH_SIZE_SLIDER_MAX
|
|
23
|
-
size = gui_constants.BRUSH_SIZES['min'] + gui_constants.BRUSH_SIZES['max'] * (normalized ** gui_constants.BRUSH_GAMMA)
|
|
24
|
-
return max(gui_constants.BRUSH_SIZES['min'], min(gui_constants.BRUSH_SIZES['max'], size))
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def create_brush_gradient(center_x, center_y, radius, hardness, inner_color=None, outer_color=None, opacity=100):
|
|
28
|
-
gradient = QRadialGradient(center_x, center_y, float(radius))
|
|
29
|
-
inner = inner_color if inner_color is not None else QColor(*gui_constants.BRUSH_COLORS['inner'])
|
|
30
|
-
outer = outer_color if outer_color is not None else QColor(*gui_constants.BRUSH_COLORS['gradient_end'])
|
|
31
|
-
inner_with_opacity = QColor(inner)
|
|
32
|
-
inner_with_opacity.setAlpha(int(float(inner.alpha()) * float(opacity) / 100.0))
|
|
33
|
-
if hardness < 100:
|
|
34
|
-
hardness_normalized = float(hardness) / 100.0
|
|
35
|
-
gradient.setColorAt(0.0, inner_with_opacity)
|
|
36
|
-
gradient.setColorAt(hardness_normalized, inner_with_opacity)
|
|
37
|
-
gradient.setColorAt(1.0, outer)
|
|
38
|
-
else:
|
|
39
|
-
gradient.setColorAt(0.0, inner_with_opacity)
|
|
40
|
-
gradient.setColorAt(1.0, inner_with_opacity)
|
|
41
|
-
return gradient
|
|
14
|
+
from .layer_collection import LayerCollection
|
|
15
|
+
from .io_manager import IOManager
|
|
16
|
+
from .brush_gradient import create_brush_gradient
|
|
42
17
|
|
|
43
18
|
|
|
44
19
|
class ImageEditor(QMainWindow):
|
|
45
20
|
def __init__(self):
|
|
46
21
|
super().__init__()
|
|
47
|
-
self.
|
|
48
|
-
self.
|
|
49
|
-
self.current_labels = None
|
|
50
|
-
self.current_layer = 0
|
|
51
|
-
self.shape = None
|
|
52
|
-
self.dtype = None
|
|
53
|
-
self._brush_mask_cache = {}
|
|
22
|
+
self.layer_collection = LayerCollection()
|
|
23
|
+
self.io_manager = IOManager(self.layer_collection)
|
|
54
24
|
self.view_mode = 'master'
|
|
55
25
|
self.temp_view_individual = False
|
|
56
|
-
self.current_file_path = ''
|
|
57
|
-
self.exif_path = ''
|
|
58
|
-
self.exif_data = None
|
|
59
26
|
self.modified = False
|
|
60
27
|
self.installEventFilter(self)
|
|
61
28
|
self.update_timer = QTimer(self)
|
|
@@ -65,6 +32,9 @@ class ImageEditor(QMainWindow):
|
|
|
65
32
|
self.brush = Brush()
|
|
66
33
|
self.brush_controller = BrushController(self.brush)
|
|
67
34
|
self.undo_manager = UndoManager()
|
|
35
|
+
self.undo_action = None
|
|
36
|
+
self.redo_action = None
|
|
37
|
+
self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
|
|
68
38
|
self.loader_thread = None
|
|
69
39
|
|
|
70
40
|
def keyPressEvent(self, event):
|
|
@@ -116,43 +86,14 @@ class ImageEditor(QMainWindow):
|
|
|
116
86
|
return True
|
|
117
87
|
|
|
118
88
|
def sort_layers(self, order):
|
|
119
|
-
|
|
120
|
-
return
|
|
121
|
-
master_index = -1
|
|
122
|
-
master_label = None
|
|
123
|
-
master_layer = None
|
|
124
|
-
for i, label in enumerate(self.current_labels):
|
|
125
|
-
if label.lower() == "master":
|
|
126
|
-
master_index = i
|
|
127
|
-
master_label = self.current_labels.pop(i)
|
|
128
|
-
master_layer = self.current_stack[i]
|
|
129
|
-
self.current_stack = np.delete(self.current_stack, i, axis=0)
|
|
130
|
-
break
|
|
131
|
-
if order == 'asc':
|
|
132
|
-
self.sorted_indices = sorted(range(len(self.current_labels)),
|
|
133
|
-
key=lambda i: self.current_labels[i].lower())
|
|
134
|
-
elif order == 'desc':
|
|
135
|
-
self.sorted_indices = sorted(range(len(self.current_labels)),
|
|
136
|
-
key=lambda i: self.current_labels[i].lower(),
|
|
137
|
-
reverse=True)
|
|
138
|
-
else:
|
|
139
|
-
raise ValueError(f"Invalid sorting order: {order}")
|
|
140
|
-
self.current_labels = [self.current_labels[i] for i in self.sorted_indices]
|
|
141
|
-
self.current_stack = self.current_stack[self.sorted_indices]
|
|
142
|
-
if master_index != -1:
|
|
143
|
-
self.current_labels.insert(0, master_label)
|
|
144
|
-
self.current_stack = np.insert(self.current_stack, 0, master_layer, axis=0)
|
|
145
|
-
self.master_layer = master_layer.copy()
|
|
146
|
-
self.master_layer.setflags(write=True)
|
|
89
|
+
self.layer_collection.sort_layers(order)
|
|
147
90
|
self.update_thumbnails()
|
|
148
|
-
|
|
149
|
-
self.current_layer = len(self.current_stack) - 1
|
|
150
|
-
self.change_layer(self.current_layer)
|
|
91
|
+
self.change_layer(self.layer_collection.current_layer)
|
|
151
92
|
|
|
152
93
|
def update_title(self):
|
|
153
94
|
title = constants.APP_TITLE
|
|
154
|
-
if self.current_file_path:
|
|
155
|
-
title += f" - {self.current_file_path.split('/')[-1]}"
|
|
95
|
+
if self.io_manager.current_file_path:
|
|
96
|
+
title += f" - {self.io_manager.current_file_path.split('/')[-1]}"
|
|
156
97
|
if self.modified:
|
|
157
98
|
title += " *"
|
|
158
99
|
self.window().setWindowTitle(title)
|
|
@@ -170,7 +111,7 @@ class ImageEditor(QMainWindow):
|
|
|
170
111
|
self.import_frames_from_files(file_paths)
|
|
171
112
|
return
|
|
172
113
|
path = file_paths[0] if isinstance(file_paths, list) else file_paths
|
|
173
|
-
self.current_file_path = path
|
|
114
|
+
self.io_manager.current_file_path = path
|
|
174
115
|
QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
|
|
175
116
|
self.loading_dialog = QDialog(self)
|
|
176
117
|
self.loading_dialog.setWindowTitle("Loading")
|
|
@@ -192,21 +133,20 @@ class ImageEditor(QMainWindow):
|
|
|
192
133
|
QApplication.restoreOverrideCursor()
|
|
193
134
|
self.loading_timer.stop()
|
|
194
135
|
self.loading_dialog.hide()
|
|
195
|
-
self.
|
|
136
|
+
self.layer_collection.layer_stack = stack
|
|
196
137
|
if labels is None:
|
|
197
|
-
self.
|
|
138
|
+
self.layer_collection.layer_labels = [f'Layer {i:03d}' for i in range(len(stack))]
|
|
198
139
|
else:
|
|
199
|
-
self.
|
|
200
|
-
self.master_layer = master_layer
|
|
201
|
-
self.shape = np.array(master_layer).shape
|
|
202
|
-
self.dtype = master_layer.dtype
|
|
140
|
+
self.layer_collection.layer_labels = labels
|
|
141
|
+
self.layer_collection.master_layer = master_layer
|
|
203
142
|
self.modified = False
|
|
143
|
+
self.undo_manager.reset()
|
|
204
144
|
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
205
145
|
self.update_thumbnails()
|
|
206
146
|
self.image_viewer.setup_brush_cursor()
|
|
207
147
|
self.change_layer(0)
|
|
208
148
|
self.image_viewer.reset_zoom()
|
|
209
|
-
self.statusBar().showMessage(f"Loaded: {self.current_file_path}")
|
|
149
|
+
self.statusBar().showMessage(f"Loaded: {self.io_manager.current_file_path}")
|
|
210
150
|
self.thumbnail_list.setFocus()
|
|
211
151
|
self.update_title()
|
|
212
152
|
|
|
@@ -216,7 +156,7 @@ class ImageEditor(QMainWindow):
|
|
|
216
156
|
self.loading_dialog.accept()
|
|
217
157
|
self.loading_dialog.deleteLater()
|
|
218
158
|
QMessageBox.critical(self, "Error", error_msg)
|
|
219
|
-
self.statusBar().showMessage(f"Error loading: {self.current_file_path}")
|
|
159
|
+
self.statusBar().showMessage(f"Error loading: {self.io_manager.current_file_path}")
|
|
220
160
|
|
|
221
161
|
def mark_as_modified(self):
|
|
222
162
|
self.modified = True
|
|
@@ -227,68 +167,32 @@ class ImageEditor(QMainWindow):
|
|
|
227
167
|
"Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
|
|
228
168
|
if file_paths:
|
|
229
169
|
self.import_frames_from_files(file_paths)
|
|
170
|
+
self.statusBar().showMessage("Imported selected frames")
|
|
230
171
|
|
|
231
172
|
def import_frames_from_files(self, file_paths):
|
|
232
|
-
|
|
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()
|
|
233
181
|
return
|
|
234
|
-
if self.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
self.
|
|
238
|
-
self.
|
|
239
|
-
label = path.split("/")[-1].split(".")[0]
|
|
240
|
-
self.current_labels = [label]
|
|
241
|
-
if self.master_layer is None:
|
|
242
|
-
self.master_layer = img.copy()
|
|
243
|
-
self.blank_layer = np.zeros(self.master_layer.shape[:2])
|
|
244
|
-
next_paths = file_paths[1:]
|
|
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])
|
|
245
187
|
else:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
|
|
251
|
-
try:
|
|
252
|
-
validate_image(img, self.shape, self.dtype)
|
|
253
|
-
except ShapeError as e:
|
|
254
|
-
msg = QMessageBox()
|
|
255
|
-
msg.setIcon(QMessageBox.Critical)
|
|
256
|
-
msg.setWindowTitle("Import error")
|
|
257
|
-
msg.setText(f"All files must have the same shape.\n{str(e)}")
|
|
258
|
-
msg.exec()
|
|
259
|
-
return
|
|
260
|
-
except BitDepthError as e:
|
|
261
|
-
msg = QMessageBox()
|
|
262
|
-
msg.setIcon(QMessageBox.Critical)
|
|
263
|
-
msg.setWindowTitle("Import error")
|
|
264
|
-
msg.setText(f"All flies must have the same bit depth.\n{str(e)}")
|
|
265
|
-
msg.exec()
|
|
266
|
-
return
|
|
267
|
-
except Exception as e:
|
|
268
|
-
traceback.print_tb(e.__traceback__)
|
|
269
|
-
raise e
|
|
270
|
-
return
|
|
271
|
-
label_x = label
|
|
272
|
-
i = 0
|
|
273
|
-
while label_x in self.current_labels:
|
|
274
|
-
i += 1
|
|
275
|
-
label_x = f"{label} ({i})"
|
|
276
|
-
self.current_labels.append(label_x)
|
|
277
|
-
self.current_stack = np.append(self.current_stack, [img], axis=0)
|
|
278
|
-
except Exception as e:
|
|
279
|
-
traceback.print_tb(e.__traceback__)
|
|
280
|
-
msg = QMessageBox()
|
|
281
|
-
msg.setIcon(QMessageBox.Critical)
|
|
282
|
-
msg.setWindowTitle("Import error")
|
|
283
|
-
msg.setText(f"Error loading file: {path}.\n{str(e)}")
|
|
284
|
-
msg.exec()
|
|
285
|
-
self.statusBar().showMessage(f"Error loading file: {path}")
|
|
286
|
-
break
|
|
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)
|
|
287
192
|
self.mark_as_modified()
|
|
288
193
|
self.change_layer(0)
|
|
289
194
|
self.image_viewer.reset_zoom()
|
|
290
195
|
self.thumbnail_list.setFocus()
|
|
291
|
-
|
|
292
196
|
self.update_thumbnails()
|
|
293
197
|
|
|
294
198
|
def save_file(self):
|
|
@@ -304,7 +208,7 @@ class ImageEditor(QMainWindow):
|
|
|
304
208
|
self.save_multilayer_as()
|
|
305
209
|
|
|
306
210
|
def save_multilayer(self):
|
|
307
|
-
if self.
|
|
211
|
+
if self.layer_collection.layer_stack is None:
|
|
308
212
|
return
|
|
309
213
|
if self.current_file_path != '':
|
|
310
214
|
extension = self.current_file_path.split('.')[-1]
|
|
@@ -314,7 +218,7 @@ class ImageEditor(QMainWindow):
|
|
|
314
218
|
self.save_multilayer_file_as()
|
|
315
219
|
|
|
316
220
|
def save_multilayer_as(self):
|
|
317
|
-
if self.
|
|
221
|
+
if self.layer_collection.layer_stack is None:
|
|
318
222
|
return
|
|
319
223
|
path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
|
|
320
224
|
"TIFF Files (*.tif *.tiff);;All Files (*)")
|
|
@@ -325,10 +229,8 @@ class ImageEditor(QMainWindow):
|
|
|
325
229
|
|
|
326
230
|
def save_multilayer_to_path(self, path):
|
|
327
231
|
try:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
write_multilayer_tiff_from_images({**master_layer, **individual_layers}, path, exif_path=self.exif_path)
|
|
331
|
-
self.current_file_path = path
|
|
232
|
+
self.io_manager.save_multilayer(path)
|
|
233
|
+
self.io_manager.current_file_path = path
|
|
332
234
|
self.modified = False
|
|
333
235
|
self.update_title()
|
|
334
236
|
self.statusBar().showMessage(f"Saved multilayer to: {path}")
|
|
@@ -337,7 +239,7 @@ class ImageEditor(QMainWindow):
|
|
|
337
239
|
QMessageBox.critical(self, "Save Error", f"Could not save file: {str(e)}")
|
|
338
240
|
|
|
339
241
|
def save_master(self):
|
|
340
|
-
if self.master_layer is None:
|
|
242
|
+
if self.layer_collection.master_layer is None:
|
|
341
243
|
return
|
|
342
244
|
if self.current_file_path != '':
|
|
343
245
|
self.save_master_to_path(self.current_file_path)
|
|
@@ -345,7 +247,7 @@ class ImageEditor(QMainWindow):
|
|
|
345
247
|
self.save_master_as()
|
|
346
248
|
|
|
347
249
|
def save_master_as(self):
|
|
348
|
-
if self.
|
|
250
|
+
if self.layer_collection.layer_stack is None:
|
|
349
251
|
return
|
|
350
252
|
path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
|
|
351
253
|
"TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
|
|
@@ -354,8 +256,8 @@ class ImageEditor(QMainWindow):
|
|
|
354
256
|
|
|
355
257
|
def save_master_to_path(self, path):
|
|
356
258
|
try:
|
|
357
|
-
|
|
358
|
-
self.current_file_path = path
|
|
259
|
+
self.io_manager.save_master(path)
|
|
260
|
+
self.io_manager.current_file_path = path
|
|
359
261
|
self.modified = False
|
|
360
262
|
self.update_title()
|
|
361
263
|
self.statusBar().showMessage(f"Saved master layer to: {path}")
|
|
@@ -366,21 +268,21 @@ class ImageEditor(QMainWindow):
|
|
|
366
268
|
def select_exif_path(self):
|
|
367
269
|
path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
|
|
368
270
|
if path:
|
|
369
|
-
self.
|
|
370
|
-
self.exif_data = get_exif(path)
|
|
271
|
+
self.io_manager.set_exif_data(path)
|
|
371
272
|
self.statusBar().showMessage(f"EXIF data extracted from {path}.")
|
|
372
|
-
self._exif_dialog = ExifData(self.exif_data, self)
|
|
273
|
+
self._exif_dialog = ExifData(self.io_manager.exif_data, self)
|
|
373
274
|
self._exif_dialog.exec()
|
|
374
275
|
|
|
375
276
|
def close_file(self):
|
|
376
277
|
if self._check_unsaved_changes():
|
|
377
|
-
self.master_layer = None
|
|
278
|
+
self.layer_collection.master_layer = None
|
|
378
279
|
self.blank_layer = None
|
|
379
280
|
self.current_stack = None
|
|
380
|
-
self.master_layer = None
|
|
381
|
-
self.
|
|
382
|
-
self.current_file_path = ''
|
|
281
|
+
self.layer_collection.master_layer = None
|
|
282
|
+
self.layer_collection.current_layer_idx = 0
|
|
283
|
+
self.io_manager.current_file_path = ''
|
|
383
284
|
self.modified = False
|
|
285
|
+
self.undo_manager.reset()
|
|
384
286
|
self.image_viewer.clear_image()
|
|
385
287
|
self.update_thumbnails()
|
|
386
288
|
self.update_title()
|
|
@@ -418,10 +320,10 @@ class ImageEditor(QMainWindow):
|
|
|
418
320
|
self.display_master_layer()
|
|
419
321
|
|
|
420
322
|
def display_master_layer(self):
|
|
421
|
-
if self.master_layer is None:
|
|
323
|
+
if self.layer_collection.master_layer is None:
|
|
422
324
|
self.image_viewer.clear_image()
|
|
423
325
|
else:
|
|
424
|
-
qimage = self.numpy_to_qimage(self.master_layer)
|
|
326
|
+
qimage = self.numpy_to_qimage(self.layer_collection.master_layer)
|
|
425
327
|
self.image_viewer.set_image(qimage)
|
|
426
328
|
|
|
427
329
|
def create_thumbnail(self, layer, size):
|
|
@@ -435,30 +337,30 @@ class ImageEditor(QMainWindow):
|
|
|
435
337
|
return QPixmap.fromImage(qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
|
|
436
338
|
|
|
437
339
|
def update_master_thumbnail(self):
|
|
438
|
-
if self.master_layer is None:
|
|
340
|
+
if self.layer_collection.master_layer is None:
|
|
439
341
|
self.master_thumbnail_label.clear()
|
|
440
342
|
else:
|
|
441
343
|
thumb_size = gui_constants.UI_SIZES['thumbnail']
|
|
442
|
-
master_thumb = self.create_thumbnail(self.master_layer, thumb_size)
|
|
344
|
+
master_thumb = self.create_thumbnail(self.layer_collection.master_layer, thumb_size)
|
|
443
345
|
self.master_thumbnail_label.setPixmap(master_thumb)
|
|
444
346
|
|
|
445
347
|
def update_thumbnails(self):
|
|
446
348
|
self.update_master_thumbnail()
|
|
447
349
|
self.thumbnail_list.clear()
|
|
448
350
|
thumb_size = gui_constants.UI_SIZES['thumbnail']
|
|
449
|
-
if self.
|
|
351
|
+
if self.layer_collection.layer_stack is None:
|
|
450
352
|
return
|
|
451
|
-
for i, (layer, label) in enumerate(zip(self.
|
|
353
|
+
for i, (layer, label) in enumerate(zip(self.layer_collection.layer_stack, self.layer_collection.layer_labels)):
|
|
452
354
|
thumbnail = self.create_thumbnail(layer, thumb_size)
|
|
453
|
-
self._add_thumbnail_item(thumbnail, label, i, i == self.
|
|
355
|
+
self._add_thumbnail_item(thumbnail, label, i, i == self.layer_collection.current_layer_idx)
|
|
454
356
|
|
|
455
357
|
def _add_thumbnail_item(self, thumbnail, label, i, is_current):
|
|
456
358
|
pass
|
|
457
359
|
|
|
458
360
|
def change_layer(self, layer_idx):
|
|
459
|
-
if 0 <= layer_idx <
|
|
361
|
+
if 0 <= layer_idx < self.layer_collection.number_of_layers():
|
|
460
362
|
view_state = self.image_viewer.get_view_state()
|
|
461
|
-
self.
|
|
363
|
+
self.layer_collection.current_layer_idx = layer_idx
|
|
462
364
|
self.display_current_view()
|
|
463
365
|
self.image_viewer.set_view_state(view_state)
|
|
464
366
|
self.thumbnail_list.setCurrentRow(layer_idx)
|
|
@@ -466,14 +368,10 @@ class ImageEditor(QMainWindow):
|
|
|
466
368
|
self.image_viewer.update_brush_cursor()
|
|
467
369
|
self.image_viewer.setFocus()
|
|
468
370
|
|
|
469
|
-
def change_layer_item(self, item):
|
|
470
|
-
layer_idx = self.thumbnail_list.row(item)
|
|
471
|
-
self.change_layer(layer_idx)
|
|
472
|
-
|
|
473
371
|
def display_current_layer(self):
|
|
474
|
-
if self.
|
|
372
|
+
if self.layer_collection.layer_stack is None:
|
|
475
373
|
return
|
|
476
|
-
layer = self.
|
|
374
|
+
layer = self.layer_collection.current_layer()
|
|
477
375
|
qimage = self.numpy_to_qimage(layer)
|
|
478
376
|
self.image_viewer.set_image(qimage)
|
|
479
377
|
|
|
@@ -492,65 +390,65 @@ class ImageEditor(QMainWindow):
|
|
|
492
390
|
return QImage()
|
|
493
391
|
|
|
494
392
|
def prev_layer(self):
|
|
495
|
-
if self.
|
|
496
|
-
new_idx = max(0, self.
|
|
497
|
-
if new_idx != self.
|
|
393
|
+
if self.layer_collection.layer_stack is not None:
|
|
394
|
+
new_idx = max(0, self.layer_collection.current_layer_idx - 1)
|
|
395
|
+
if new_idx != self.layer_collection.current_layer_idx:
|
|
498
396
|
self.change_layer(new_idx)
|
|
499
397
|
self.highlight_thumbnail(new_idx)
|
|
500
398
|
|
|
501
399
|
def next_layer(self):
|
|
502
|
-
if self.
|
|
503
|
-
new_idx = min(
|
|
504
|
-
if new_idx != self.
|
|
400
|
+
if self.layer_collection.layer_stack is not None:
|
|
401
|
+
new_idx = min(self.layer_collection.number_of_layers() - 1, self.layer_collection.current_layer_idx + 1)
|
|
402
|
+
if new_idx != self.layer_collection.current_layer_idx:
|
|
505
403
|
self.change_layer(new_idx)
|
|
506
404
|
self.highlight_thumbnail(new_idx)
|
|
507
405
|
|
|
508
406
|
def highlight_thumbnail(self, index):
|
|
509
407
|
self.thumbnail_list.setCurrentRow(index)
|
|
510
|
-
self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index),
|
|
511
|
-
QAbstractItemView.PositionAtCenter)
|
|
408
|
+
self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
512
409
|
|
|
513
410
|
def update_brush_size(self, slider_val):
|
|
411
|
+
|
|
412
|
+
def slider_to_brush_size(slider_val):
|
|
413
|
+
normalized = slider_val / gui_constants.BRUSH_SIZE_SLIDER_MAX
|
|
414
|
+
size = gui_constants.BRUSH_SIZES['min'] + \
|
|
415
|
+
gui_constants.BRUSH_SIZES['max'] * (normalized ** gui_constants.BRUSH_GAMMA)
|
|
416
|
+
return max(gui_constants.BRUSH_SIZES['min'], min(gui_constants.BRUSH_SIZES['max'], size))
|
|
417
|
+
|
|
514
418
|
self.brush.size = slider_to_brush_size(slider_val)
|
|
515
419
|
self.update_brush_thumb()
|
|
516
|
-
self.image_viewer.update_brush_cursor()
|
|
517
|
-
self.clear_brush_cache()
|
|
518
420
|
|
|
519
421
|
def increase_brush_size(self, amount=5):
|
|
520
|
-
val = self.brush_size_slider.value()
|
|
521
|
-
self.brush_size_slider.setValue(
|
|
422
|
+
val = min(self.brush_size_slider.value() + amount, self.brush_size_slider.maximum())
|
|
423
|
+
self.brush_size_slider.setValue(val)
|
|
522
424
|
self.update_brush_size(val)
|
|
523
425
|
|
|
524
426
|
def decrease_brush_size(self, amount=5):
|
|
525
|
-
val = self.brush_size_slider.value()
|
|
526
|
-
self.brush_size_slider.setValue(
|
|
427
|
+
val = max(self.brush_size_slider.value() - amount, self.brush_size_slider.minimum())
|
|
428
|
+
self.brush_size_slider.setValue(val)
|
|
527
429
|
self.update_brush_size(val)
|
|
528
430
|
|
|
529
431
|
def increase_brush_hardness(self, amount=2):
|
|
530
|
-
val = self.hardness_slider.value()
|
|
531
|
-
self.hardness_slider.setValue(
|
|
432
|
+
val = min(self.hardness_slider.value() + amount, self.hardness_slider.maximum())
|
|
433
|
+
self.hardness_slider.setValue(val)
|
|
532
434
|
self.update_brush_hardness(val)
|
|
533
435
|
|
|
534
436
|
def decrease_brush_hardness(self, amount=2):
|
|
535
|
-
val = self.hardness_slider.value()
|
|
536
|
-
self.hardness_slider.setValue(
|
|
437
|
+
val = max(self.hardness_slider.value() - amount, self.hardness_slider.minimum())
|
|
438
|
+
self.hardness_slider.setValue(val)
|
|
537
439
|
self.update_brush_hardness(val)
|
|
538
440
|
|
|
539
441
|
def update_brush_hardness(self, hardness):
|
|
540
442
|
self.brush.hardness = hardness
|
|
541
443
|
self.update_brush_thumb()
|
|
542
|
-
self.image_viewer.update_brush_cursor()
|
|
543
|
-
self.clear_brush_cache()
|
|
544
444
|
|
|
545
445
|
def update_brush_opacity(self, opacity):
|
|
546
446
|
self.brush.opacity = opacity
|
|
547
447
|
self.update_brush_thumb()
|
|
548
|
-
self.image_viewer.update_brush_cursor()
|
|
549
448
|
|
|
550
449
|
def update_brush_flow(self, flow):
|
|
551
450
|
self.brush.flow = flow
|
|
552
451
|
self.update_brush_thumb()
|
|
553
|
-
self.image_viewer.update_brush_cursor()
|
|
554
452
|
|
|
555
453
|
def update_brush_thumb(self):
|
|
556
454
|
width, height = gui_constants.UI_SIZES['brush_preview']
|
|
@@ -586,15 +484,13 @@ class ImageEditor(QMainWindow):
|
|
|
586
484
|
painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
|
|
587
485
|
painter.end()
|
|
588
486
|
self.brush_preview.setPixmap(pixmap)
|
|
589
|
-
|
|
590
|
-
def clear_brush_cache(self):
|
|
591
|
-
self._brush_mask_cache.clear()
|
|
487
|
+
self.image_viewer.update_brush_cursor()
|
|
592
488
|
|
|
593
489
|
def allow_cursor_preview(self):
|
|
594
490
|
return self.view_mode == 'master' and not self.temp_view_individual
|
|
595
491
|
|
|
596
492
|
def copy_layer_to_master(self):
|
|
597
|
-
if self.
|
|
493
|
+
if self.layer_collection.layer_stack is None or self.layer_collection.master_layer is None:
|
|
598
494
|
return
|
|
599
495
|
reply = QMessageBox.question(
|
|
600
496
|
self,
|
|
@@ -604,27 +500,27 @@ class ImageEditor(QMainWindow):
|
|
|
604
500
|
QMessageBox.No
|
|
605
501
|
)
|
|
606
502
|
if reply == QMessageBox.Yes:
|
|
607
|
-
self.master_layer = self.
|
|
608
|
-
self.master_layer.setflags(write=True)
|
|
503
|
+
self.layer_collection.master_layer = self.layer_collection.current_layer().copy()
|
|
504
|
+
self.layer_collection.master_layer.setflags(write=True)
|
|
609
505
|
self.display_current_view()
|
|
610
506
|
self.update_thumbnails()
|
|
611
507
|
self.mark_as_modified()
|
|
612
|
-
self.statusBar().showMessage(f"Copied layer {self.
|
|
508
|
+
self.statusBar().showMessage(f"Copied layer {self.layer_collection.current_layer_idx + 1} to master")
|
|
613
509
|
|
|
614
510
|
def copy_brush_area_to_master(self, view_pos):
|
|
615
|
-
if self.
|
|
511
|
+
if self.layer_collection.layer_stack is None or self.layer_collection.number_of_layers() == 0 \
|
|
616
512
|
or self.view_mode != 'master' or self.temp_view_individual:
|
|
617
513
|
return
|
|
618
|
-
area = self.brush_controller.apply_brush_operation(self.master_layer_copy,
|
|
619
|
-
self.
|
|
620
|
-
self.master_layer, self.mask_layer,
|
|
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,
|
|
621
517
|
view_pos, self.image_viewer)
|
|
622
518
|
self.undo_manager.extend_undo_area(*area)
|
|
623
519
|
|
|
624
520
|
def begin_copy_brush_area(self, pos):
|
|
625
521
|
if self.view_mode == 'master' and not self.temp_view_individual:
|
|
626
522
|
self.mask_layer = self.blank_layer.copy()
|
|
627
|
-
self.
|
|
523
|
+
self.layer_collection.copy_master_layer()
|
|
628
524
|
self.undo_manager.reset_undo_area()
|
|
629
525
|
self.copy_brush_area_to_master(pos)
|
|
630
526
|
self.needs_update = True
|
|
@@ -644,6 +540,22 @@ class ImageEditor(QMainWindow):
|
|
|
644
540
|
if self.update_timer.isActive():
|
|
645
541
|
self.display_master_layer()
|
|
646
542
|
self.update_master_thumbnail()
|
|
647
|
-
self.undo_manager.save_undo_state(self.master_layer_copy)
|
|
543
|
+
self.undo_manager.save_undo_state(self.layer_collection.master_layer_copy, 'Brush Stroke')
|
|
648
544
|
self.update_timer.stop()
|
|
649
545
|
self.mark_as_modified()
|
|
546
|
+
|
|
547
|
+
def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
|
|
548
|
+
if self.undo_action:
|
|
549
|
+
if has_undo:
|
|
550
|
+
self.undo_action.setText(f"Undo {undo_desc}")
|
|
551
|
+
self.undo_action.setEnabled(True)
|
|
552
|
+
else:
|
|
553
|
+
self.undo_action.setText("Undo")
|
|
554
|
+
self.undo_action.setEnabled(False)
|
|
555
|
+
if self.redo_action:
|
|
556
|
+
if has_redo:
|
|
557
|
+
self.redo_action.setText(f"Redo {redo_desc}")
|
|
558
|
+
self.redo_action.setEnabled(True)
|
|
559
|
+
else:
|
|
560
|
+
self.redo_action.setText("Redo")
|
|
561
|
+
self.redo_action.setEnabled(False)
|