shinestacker 0.3.0__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/utils.py +4 -0
- shinestacker/algorithms/white_balance.py +13 -0
- shinestacker/gui/new_project.py +1 -0
- shinestacker/retouch/brush_gradient.py +20 -0
- shinestacker/retouch/brush_preview.py +11 -14
- shinestacker/retouch/image_editor.py +93 -202
- shinestacker/retouch/image_editor_ui.py +15 -8
- shinestacker/retouch/image_filters.py +307 -379
- shinestacker/retouch/image_viewer.py +13 -21
- shinestacker/retouch/io_manager.py +57 -0
- shinestacker/retouch/layer_collection.py +54 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/METADATA +1 -1
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/RECORD +18 -14
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.0.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)
|
|
@@ -119,43 +86,14 @@ class ImageEditor(QMainWindow):
|
|
|
119
86
|
return True
|
|
120
87
|
|
|
121
88
|
def sort_layers(self, order):
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
master_index = -1
|
|
125
|
-
master_label = None
|
|
126
|
-
master_layer = None
|
|
127
|
-
for i, label in enumerate(self.current_labels):
|
|
128
|
-
if label.lower() == "master":
|
|
129
|
-
master_index = i
|
|
130
|
-
master_label = self.current_labels.pop(i)
|
|
131
|
-
master_layer = self.current_stack[i]
|
|
132
|
-
self.current_stack = np.delete(self.current_stack, i, axis=0)
|
|
133
|
-
break
|
|
134
|
-
if order == 'asc':
|
|
135
|
-
self.sorted_indices = sorted(range(len(self.current_labels)),
|
|
136
|
-
key=lambda i: self.current_labels[i].lower())
|
|
137
|
-
elif order == 'desc':
|
|
138
|
-
self.sorted_indices = sorted(range(len(self.current_labels)),
|
|
139
|
-
key=lambda i: self.current_labels[i].lower(),
|
|
140
|
-
reverse=True)
|
|
141
|
-
else:
|
|
142
|
-
raise ValueError(f"Invalid sorting order: {order}")
|
|
143
|
-
self.current_labels = [self.current_labels[i] for i in self.sorted_indices]
|
|
144
|
-
self.current_stack = self.current_stack[self.sorted_indices]
|
|
145
|
-
if master_index != -1:
|
|
146
|
-
self.current_labels.insert(0, master_label)
|
|
147
|
-
self.current_stack = np.insert(self.current_stack, 0, master_layer, axis=0)
|
|
148
|
-
self.master_layer = master_layer.copy()
|
|
149
|
-
self.master_layer.setflags(write=True)
|
|
89
|
+
self.layer_collection.sort_layers(order)
|
|
150
90
|
self.update_thumbnails()
|
|
151
|
-
|
|
152
|
-
self.current_layer = len(self.current_stack) - 1
|
|
153
|
-
self.change_layer(self.current_layer)
|
|
91
|
+
self.change_layer(self.layer_collection.current_layer)
|
|
154
92
|
|
|
155
93
|
def update_title(self):
|
|
156
94
|
title = constants.APP_TITLE
|
|
157
|
-
if self.current_file_path:
|
|
158
|
-
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]}"
|
|
159
97
|
if self.modified:
|
|
160
98
|
title += " *"
|
|
161
99
|
self.window().setWindowTitle(title)
|
|
@@ -173,7 +111,7 @@ class ImageEditor(QMainWindow):
|
|
|
173
111
|
self.import_frames_from_files(file_paths)
|
|
174
112
|
return
|
|
175
113
|
path = file_paths[0] if isinstance(file_paths, list) else file_paths
|
|
176
|
-
self.current_file_path = path
|
|
114
|
+
self.io_manager.current_file_path = path
|
|
177
115
|
QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
|
|
178
116
|
self.loading_dialog = QDialog(self)
|
|
179
117
|
self.loading_dialog.setWindowTitle("Loading")
|
|
@@ -195,14 +133,12 @@ class ImageEditor(QMainWindow):
|
|
|
195
133
|
QApplication.restoreOverrideCursor()
|
|
196
134
|
self.loading_timer.stop()
|
|
197
135
|
self.loading_dialog.hide()
|
|
198
|
-
self.
|
|
136
|
+
self.layer_collection.layer_stack = stack
|
|
199
137
|
if labels is None:
|
|
200
|
-
self.
|
|
138
|
+
self.layer_collection.layer_labels = [f'Layer {i:03d}' for i in range(len(stack))]
|
|
201
139
|
else:
|
|
202
|
-
self.
|
|
203
|
-
self.master_layer = master_layer
|
|
204
|
-
self.shape = np.array(master_layer).shape
|
|
205
|
-
self.dtype = master_layer.dtype
|
|
140
|
+
self.layer_collection.layer_labels = labels
|
|
141
|
+
self.layer_collection.master_layer = master_layer
|
|
206
142
|
self.modified = False
|
|
207
143
|
self.undo_manager.reset()
|
|
208
144
|
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
@@ -210,7 +146,7 @@ class ImageEditor(QMainWindow):
|
|
|
210
146
|
self.image_viewer.setup_brush_cursor()
|
|
211
147
|
self.change_layer(0)
|
|
212
148
|
self.image_viewer.reset_zoom()
|
|
213
|
-
self.statusBar().showMessage(f"Loaded: {self.current_file_path}")
|
|
149
|
+
self.statusBar().showMessage(f"Loaded: {self.io_manager.current_file_path}")
|
|
214
150
|
self.thumbnail_list.setFocus()
|
|
215
151
|
self.update_title()
|
|
216
152
|
|
|
@@ -220,7 +156,7 @@ class ImageEditor(QMainWindow):
|
|
|
220
156
|
self.loading_dialog.accept()
|
|
221
157
|
self.loading_dialog.deleteLater()
|
|
222
158
|
QMessageBox.critical(self, "Error", error_msg)
|
|
223
|
-
self.statusBar().showMessage(f"Error loading: {self.current_file_path}")
|
|
159
|
+
self.statusBar().showMessage(f"Error loading: {self.io_manager.current_file_path}")
|
|
224
160
|
|
|
225
161
|
def mark_as_modified(self):
|
|
226
162
|
self.modified = True
|
|
@@ -231,68 +167,32 @@ class ImageEditor(QMainWindow):
|
|
|
231
167
|
"Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
|
|
232
168
|
if file_paths:
|
|
233
169
|
self.import_frames_from_files(file_paths)
|
|
170
|
+
self.statusBar().showMessage("Imported selected frames")
|
|
234
171
|
|
|
235
172
|
def import_frames_from_files(self, file_paths):
|
|
236
|
-
|
|
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()
|
|
237
181
|
return
|
|
238
|
-
if self.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
self.
|
|
242
|
-
self.
|
|
243
|
-
label = path.split("/")[-1].split(".")[0]
|
|
244
|
-
self.current_labels = [label]
|
|
245
|
-
if self.master_layer is None:
|
|
246
|
-
self.master_layer = img.copy()
|
|
247
|
-
self.blank_layer = np.zeros(self.master_layer.shape[:2])
|
|
248
|
-
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])
|
|
249
187
|
else:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
|
|
255
|
-
try:
|
|
256
|
-
validate_image(img, self.shape, self.dtype)
|
|
257
|
-
except ShapeError as e:
|
|
258
|
-
msg = QMessageBox()
|
|
259
|
-
msg.setIcon(QMessageBox.Critical)
|
|
260
|
-
msg.setWindowTitle("Import error")
|
|
261
|
-
msg.setText(f"All files must have the same shape.\n{str(e)}")
|
|
262
|
-
msg.exec()
|
|
263
|
-
return
|
|
264
|
-
except BitDepthError as e:
|
|
265
|
-
msg = QMessageBox()
|
|
266
|
-
msg.setIcon(QMessageBox.Critical)
|
|
267
|
-
msg.setWindowTitle("Import error")
|
|
268
|
-
msg.setText(f"All flies must have the same bit depth.\n{str(e)}")
|
|
269
|
-
msg.exec()
|
|
270
|
-
return
|
|
271
|
-
except Exception as e:
|
|
272
|
-
traceback.print_tb(e.__traceback__)
|
|
273
|
-
raise e
|
|
274
|
-
return
|
|
275
|
-
label_x = label
|
|
276
|
-
i = 0
|
|
277
|
-
while label_x in self.current_labels:
|
|
278
|
-
i += 1
|
|
279
|
-
label_x = f"{label} ({i})"
|
|
280
|
-
self.current_labels.append(label_x)
|
|
281
|
-
self.current_stack = np.append(self.current_stack, [img], axis=0)
|
|
282
|
-
except Exception as e:
|
|
283
|
-
traceback.print_tb(e.__traceback__)
|
|
284
|
-
msg = QMessageBox()
|
|
285
|
-
msg.setIcon(QMessageBox.Critical)
|
|
286
|
-
msg.setWindowTitle("Import error")
|
|
287
|
-
msg.setText(f"Error loading file: {path}.\n{str(e)}")
|
|
288
|
-
msg.exec()
|
|
289
|
-
self.statusBar().showMessage(f"Error loading file: {path}")
|
|
290
|
-
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)
|
|
291
192
|
self.mark_as_modified()
|
|
292
193
|
self.change_layer(0)
|
|
293
194
|
self.image_viewer.reset_zoom()
|
|
294
195
|
self.thumbnail_list.setFocus()
|
|
295
|
-
|
|
296
196
|
self.update_thumbnails()
|
|
297
197
|
|
|
298
198
|
def save_file(self):
|
|
@@ -308,7 +208,7 @@ class ImageEditor(QMainWindow):
|
|
|
308
208
|
self.save_multilayer_as()
|
|
309
209
|
|
|
310
210
|
def save_multilayer(self):
|
|
311
|
-
if self.
|
|
211
|
+
if self.layer_collection.layer_stack is None:
|
|
312
212
|
return
|
|
313
213
|
if self.current_file_path != '':
|
|
314
214
|
extension = self.current_file_path.split('.')[-1]
|
|
@@ -318,7 +218,7 @@ class ImageEditor(QMainWindow):
|
|
|
318
218
|
self.save_multilayer_file_as()
|
|
319
219
|
|
|
320
220
|
def save_multilayer_as(self):
|
|
321
|
-
if self.
|
|
221
|
+
if self.layer_collection.layer_stack is None:
|
|
322
222
|
return
|
|
323
223
|
path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
|
|
324
224
|
"TIFF Files (*.tif *.tiff);;All Files (*)")
|
|
@@ -329,10 +229,8 @@ class ImageEditor(QMainWindow):
|
|
|
329
229
|
|
|
330
230
|
def save_multilayer_to_path(self, path):
|
|
331
231
|
try:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
write_multilayer_tiff_from_images({**master_layer, **individual_layers}, path, exif_path=self.exif_path)
|
|
335
|
-
self.current_file_path = path
|
|
232
|
+
self.io_manager.save_multilayer(path)
|
|
233
|
+
self.io_manager.current_file_path = path
|
|
336
234
|
self.modified = False
|
|
337
235
|
self.update_title()
|
|
338
236
|
self.statusBar().showMessage(f"Saved multilayer to: {path}")
|
|
@@ -341,7 +239,7 @@ class ImageEditor(QMainWindow):
|
|
|
341
239
|
QMessageBox.critical(self, "Save Error", f"Could not save file: {str(e)}")
|
|
342
240
|
|
|
343
241
|
def save_master(self):
|
|
344
|
-
if self.master_layer is None:
|
|
242
|
+
if self.layer_collection.master_layer is None:
|
|
345
243
|
return
|
|
346
244
|
if self.current_file_path != '':
|
|
347
245
|
self.save_master_to_path(self.current_file_path)
|
|
@@ -349,7 +247,7 @@ class ImageEditor(QMainWindow):
|
|
|
349
247
|
self.save_master_as()
|
|
350
248
|
|
|
351
249
|
def save_master_as(self):
|
|
352
|
-
if self.
|
|
250
|
+
if self.layer_collection.layer_stack is None:
|
|
353
251
|
return
|
|
354
252
|
path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
|
|
355
253
|
"TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
|
|
@@ -358,8 +256,8 @@ class ImageEditor(QMainWindow):
|
|
|
358
256
|
|
|
359
257
|
def save_master_to_path(self, path):
|
|
360
258
|
try:
|
|
361
|
-
|
|
362
|
-
self.current_file_path = path
|
|
259
|
+
self.io_manager.save_master(path)
|
|
260
|
+
self.io_manager.current_file_path = path
|
|
363
261
|
self.modified = False
|
|
364
262
|
self.update_title()
|
|
365
263
|
self.statusBar().showMessage(f"Saved master layer to: {path}")
|
|
@@ -370,20 +268,19 @@ class ImageEditor(QMainWindow):
|
|
|
370
268
|
def select_exif_path(self):
|
|
371
269
|
path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
|
|
372
270
|
if path:
|
|
373
|
-
self.
|
|
374
|
-
self.exif_data = get_exif(path)
|
|
271
|
+
self.io_manager.set_exif_data(path)
|
|
375
272
|
self.statusBar().showMessage(f"EXIF data extracted from {path}.")
|
|
376
|
-
self._exif_dialog = ExifData(self.exif_data, self)
|
|
273
|
+
self._exif_dialog = ExifData(self.io_manager.exif_data, self)
|
|
377
274
|
self._exif_dialog.exec()
|
|
378
275
|
|
|
379
276
|
def close_file(self):
|
|
380
277
|
if self._check_unsaved_changes():
|
|
381
|
-
self.master_layer = None
|
|
278
|
+
self.layer_collection.master_layer = None
|
|
382
279
|
self.blank_layer = None
|
|
383
280
|
self.current_stack = None
|
|
384
|
-
self.master_layer = None
|
|
385
|
-
self.
|
|
386
|
-
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 = ''
|
|
387
284
|
self.modified = False
|
|
388
285
|
self.undo_manager.reset()
|
|
389
286
|
self.image_viewer.clear_image()
|
|
@@ -423,10 +320,10 @@ class ImageEditor(QMainWindow):
|
|
|
423
320
|
self.display_master_layer()
|
|
424
321
|
|
|
425
322
|
def display_master_layer(self):
|
|
426
|
-
if self.master_layer is None:
|
|
323
|
+
if self.layer_collection.master_layer is None:
|
|
427
324
|
self.image_viewer.clear_image()
|
|
428
325
|
else:
|
|
429
|
-
qimage = self.numpy_to_qimage(self.master_layer)
|
|
326
|
+
qimage = self.numpy_to_qimage(self.layer_collection.master_layer)
|
|
430
327
|
self.image_viewer.set_image(qimage)
|
|
431
328
|
|
|
432
329
|
def create_thumbnail(self, layer, size):
|
|
@@ -440,30 +337,30 @@ class ImageEditor(QMainWindow):
|
|
|
440
337
|
return QPixmap.fromImage(qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
|
|
441
338
|
|
|
442
339
|
def update_master_thumbnail(self):
|
|
443
|
-
if self.master_layer is None:
|
|
340
|
+
if self.layer_collection.master_layer is None:
|
|
444
341
|
self.master_thumbnail_label.clear()
|
|
445
342
|
else:
|
|
446
343
|
thumb_size = gui_constants.UI_SIZES['thumbnail']
|
|
447
|
-
master_thumb = self.create_thumbnail(self.master_layer, thumb_size)
|
|
344
|
+
master_thumb = self.create_thumbnail(self.layer_collection.master_layer, thumb_size)
|
|
448
345
|
self.master_thumbnail_label.setPixmap(master_thumb)
|
|
449
346
|
|
|
450
347
|
def update_thumbnails(self):
|
|
451
348
|
self.update_master_thumbnail()
|
|
452
349
|
self.thumbnail_list.clear()
|
|
453
350
|
thumb_size = gui_constants.UI_SIZES['thumbnail']
|
|
454
|
-
if self.
|
|
351
|
+
if self.layer_collection.layer_stack is None:
|
|
455
352
|
return
|
|
456
|
-
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)):
|
|
457
354
|
thumbnail = self.create_thumbnail(layer, thumb_size)
|
|
458
|
-
self._add_thumbnail_item(thumbnail, label, i, i == self.
|
|
355
|
+
self._add_thumbnail_item(thumbnail, label, i, i == self.layer_collection.current_layer_idx)
|
|
459
356
|
|
|
460
357
|
def _add_thumbnail_item(self, thumbnail, label, i, is_current):
|
|
461
358
|
pass
|
|
462
359
|
|
|
463
360
|
def change_layer(self, layer_idx):
|
|
464
|
-
if 0 <= layer_idx <
|
|
361
|
+
if 0 <= layer_idx < self.layer_collection.number_of_layers():
|
|
465
362
|
view_state = self.image_viewer.get_view_state()
|
|
466
|
-
self.
|
|
363
|
+
self.layer_collection.current_layer_idx = layer_idx
|
|
467
364
|
self.display_current_view()
|
|
468
365
|
self.image_viewer.set_view_state(view_state)
|
|
469
366
|
self.thumbnail_list.setCurrentRow(layer_idx)
|
|
@@ -471,14 +368,10 @@ class ImageEditor(QMainWindow):
|
|
|
471
368
|
self.image_viewer.update_brush_cursor()
|
|
472
369
|
self.image_viewer.setFocus()
|
|
473
370
|
|
|
474
|
-
def change_layer_item(self, item):
|
|
475
|
-
layer_idx = self.thumbnail_list.row(item)
|
|
476
|
-
self.change_layer(layer_idx)
|
|
477
|
-
|
|
478
371
|
def display_current_layer(self):
|
|
479
|
-
if self.
|
|
372
|
+
if self.layer_collection.layer_stack is None:
|
|
480
373
|
return
|
|
481
|
-
layer = self.
|
|
374
|
+
layer = self.layer_collection.current_layer()
|
|
482
375
|
qimage = self.numpy_to_qimage(layer)
|
|
483
376
|
self.image_viewer.set_image(qimage)
|
|
484
377
|
|
|
@@ -497,65 +390,65 @@ class ImageEditor(QMainWindow):
|
|
|
497
390
|
return QImage()
|
|
498
391
|
|
|
499
392
|
def prev_layer(self):
|
|
500
|
-
if self.
|
|
501
|
-
new_idx = max(0, self.
|
|
502
|
-
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:
|
|
503
396
|
self.change_layer(new_idx)
|
|
504
397
|
self.highlight_thumbnail(new_idx)
|
|
505
398
|
|
|
506
399
|
def next_layer(self):
|
|
507
|
-
if self.
|
|
508
|
-
new_idx = min(
|
|
509
|
-
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:
|
|
510
403
|
self.change_layer(new_idx)
|
|
511
404
|
self.highlight_thumbnail(new_idx)
|
|
512
405
|
|
|
513
406
|
def highlight_thumbnail(self, index):
|
|
514
407
|
self.thumbnail_list.setCurrentRow(index)
|
|
515
|
-
self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index),
|
|
516
|
-
QAbstractItemView.PositionAtCenter)
|
|
408
|
+
self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
517
409
|
|
|
518
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
|
+
|
|
519
418
|
self.brush.size = slider_to_brush_size(slider_val)
|
|
520
419
|
self.update_brush_thumb()
|
|
521
|
-
self.image_viewer.update_brush_cursor()
|
|
522
|
-
self.clear_brush_cache()
|
|
523
420
|
|
|
524
421
|
def increase_brush_size(self, amount=5):
|
|
525
|
-
val = self.brush_size_slider.value()
|
|
526
|
-
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)
|
|
527
424
|
self.update_brush_size(val)
|
|
528
425
|
|
|
529
426
|
def decrease_brush_size(self, amount=5):
|
|
530
|
-
val = self.brush_size_slider.value()
|
|
531
|
-
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)
|
|
532
429
|
self.update_brush_size(val)
|
|
533
430
|
|
|
534
431
|
def increase_brush_hardness(self, amount=2):
|
|
535
|
-
val = self.hardness_slider.value()
|
|
536
|
-
self.hardness_slider.setValue(
|
|
432
|
+
val = min(self.hardness_slider.value() + amount, self.hardness_slider.maximum())
|
|
433
|
+
self.hardness_slider.setValue(val)
|
|
537
434
|
self.update_brush_hardness(val)
|
|
538
435
|
|
|
539
436
|
def decrease_brush_hardness(self, amount=2):
|
|
540
|
-
val = self.hardness_slider.value()
|
|
541
|
-
self.hardness_slider.setValue(
|
|
437
|
+
val = max(self.hardness_slider.value() - amount, self.hardness_slider.minimum())
|
|
438
|
+
self.hardness_slider.setValue(val)
|
|
542
439
|
self.update_brush_hardness(val)
|
|
543
440
|
|
|
544
441
|
def update_brush_hardness(self, hardness):
|
|
545
442
|
self.brush.hardness = hardness
|
|
546
443
|
self.update_brush_thumb()
|
|
547
|
-
self.image_viewer.update_brush_cursor()
|
|
548
|
-
self.clear_brush_cache()
|
|
549
444
|
|
|
550
445
|
def update_brush_opacity(self, opacity):
|
|
551
446
|
self.brush.opacity = opacity
|
|
552
447
|
self.update_brush_thumb()
|
|
553
|
-
self.image_viewer.update_brush_cursor()
|
|
554
448
|
|
|
555
449
|
def update_brush_flow(self, flow):
|
|
556
450
|
self.brush.flow = flow
|
|
557
451
|
self.update_brush_thumb()
|
|
558
|
-
self.image_viewer.update_brush_cursor()
|
|
559
452
|
|
|
560
453
|
def update_brush_thumb(self):
|
|
561
454
|
width, height = gui_constants.UI_SIZES['brush_preview']
|
|
@@ -591,15 +484,13 @@ class ImageEditor(QMainWindow):
|
|
|
591
484
|
painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
|
|
592
485
|
painter.end()
|
|
593
486
|
self.brush_preview.setPixmap(pixmap)
|
|
594
|
-
|
|
595
|
-
def clear_brush_cache(self):
|
|
596
|
-
self._brush_mask_cache.clear()
|
|
487
|
+
self.image_viewer.update_brush_cursor()
|
|
597
488
|
|
|
598
489
|
def allow_cursor_preview(self):
|
|
599
490
|
return self.view_mode == 'master' and not self.temp_view_individual
|
|
600
491
|
|
|
601
492
|
def copy_layer_to_master(self):
|
|
602
|
-
if self.
|
|
493
|
+
if self.layer_collection.layer_stack is None or self.layer_collection.master_layer is None:
|
|
603
494
|
return
|
|
604
495
|
reply = QMessageBox.question(
|
|
605
496
|
self,
|
|
@@ -609,27 +500,27 @@ class ImageEditor(QMainWindow):
|
|
|
609
500
|
QMessageBox.No
|
|
610
501
|
)
|
|
611
502
|
if reply == QMessageBox.Yes:
|
|
612
|
-
self.master_layer = self.
|
|
613
|
-
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)
|
|
614
505
|
self.display_current_view()
|
|
615
506
|
self.update_thumbnails()
|
|
616
507
|
self.mark_as_modified()
|
|
617
|
-
self.statusBar().showMessage(f"Copied layer {self.
|
|
508
|
+
self.statusBar().showMessage(f"Copied layer {self.layer_collection.current_layer_idx + 1} to master")
|
|
618
509
|
|
|
619
510
|
def copy_brush_area_to_master(self, view_pos):
|
|
620
|
-
if self.
|
|
511
|
+
if self.layer_collection.layer_stack is None or self.layer_collection.number_of_layers() == 0 \
|
|
621
512
|
or self.view_mode != 'master' or self.temp_view_individual:
|
|
622
513
|
return
|
|
623
|
-
area = self.brush_controller.apply_brush_operation(self.master_layer_copy,
|
|
624
|
-
self.
|
|
625
|
-
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,
|
|
626
517
|
view_pos, self.image_viewer)
|
|
627
518
|
self.undo_manager.extend_undo_area(*area)
|
|
628
519
|
|
|
629
520
|
def begin_copy_brush_area(self, pos):
|
|
630
521
|
if self.view_mode == 'master' and not self.temp_view_individual:
|
|
631
522
|
self.mask_layer = self.blank_layer.copy()
|
|
632
|
-
self.
|
|
523
|
+
self.layer_collection.copy_master_layer()
|
|
633
524
|
self.undo_manager.reset_undo_area()
|
|
634
525
|
self.copy_brush_area_to_master(pos)
|
|
635
526
|
self.needs_update = True
|
|
@@ -649,7 +540,7 @@ class ImageEditor(QMainWindow):
|
|
|
649
540
|
if self.update_timer.isActive():
|
|
650
541
|
self.display_master_layer()
|
|
651
542
|
self.update_master_thumbnail()
|
|
652
|
-
self.undo_manager.save_undo_state(self.master_layer_copy, 'Brush Stroke')
|
|
543
|
+
self.undo_manager.save_undo_state(self.layer_collection.master_layer_copy, 'Brush Stroke')
|
|
653
544
|
self.update_timer.stop()
|
|
654
545
|
self.mark_as_modified()
|
|
655
546
|
|
|
@@ -172,7 +172,12 @@ class ImageEditorUI(ImageFilters):
|
|
|
172
172
|
self.thumbnail_list.setFixedWidth(gui_constants.THUMB_WIDTH)
|
|
173
173
|
self.thumbnail_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
174
174
|
self.thumbnail_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
175
|
-
|
|
175
|
+
|
|
176
|
+
def change_layer_item(item):
|
|
177
|
+
layer_idx = self.thumbnail_list.row(item)
|
|
178
|
+
self.change_layer(layer_idx)
|
|
179
|
+
|
|
180
|
+
self.thumbnail_list.itemClicked.connect(change_layer_item)
|
|
176
181
|
self.thumbnail_list.setStyleSheet("""
|
|
177
182
|
QListWidget {
|
|
178
183
|
background-color: #f5f5f5;
|
|
@@ -226,12 +231,12 @@ class ImageEditorUI(ImageFilters):
|
|
|
226
231
|
self.undo_action = QAction("Undo", self)
|
|
227
232
|
self.undo_action.setEnabled(False)
|
|
228
233
|
self.undo_action.setShortcut("Ctrl+Z")
|
|
229
|
-
self.undo_action.triggered.connect(self.
|
|
234
|
+
self.undo_action.triggered.connect(self.undo)
|
|
230
235
|
edit_menu.addAction(self.undo_action)
|
|
231
236
|
self.redo_action = QAction("Redo", self)
|
|
232
237
|
self.redo_action.setEnabled(False)
|
|
233
238
|
self.redo_action.setShortcut("Ctrl+Y")
|
|
234
|
-
self.redo_action.triggered.connect(self.
|
|
239
|
+
self.redo_action.triggered.connect(self.redo)
|
|
235
240
|
edit_menu.addAction(self.redo_action)
|
|
236
241
|
edit_menu.addSeparator()
|
|
237
242
|
|
|
@@ -381,17 +386,19 @@ class ImageEditorUI(ImageFilters):
|
|
|
381
386
|
self._update_label_in_data(old_label, new_label, i)
|
|
382
387
|
|
|
383
388
|
def _update_label_in_data(self, old_label, new_label, i):
|
|
384
|
-
self.
|
|
389
|
+
self.layer_collection.layer_labels[i] = new_label
|
|
385
390
|
|
|
386
|
-
def
|
|
387
|
-
if self.undo_manager.undo(self.master_layer):
|
|
391
|
+
def undo(self):
|
|
392
|
+
if self.undo_manager.undo(self.layer_collection.master_layer):
|
|
388
393
|
self.display_current_view()
|
|
394
|
+
self.update_master_thumbnail()
|
|
389
395
|
self.mark_as_modified()
|
|
390
396
|
self.statusBar().showMessage("Undo applied", 2000)
|
|
391
397
|
|
|
392
|
-
def
|
|
393
|
-
if self.undo_manager.redo(self.master_layer):
|
|
398
|
+
def redo(self):
|
|
399
|
+
if self.undo_manager.redo(self.layer_collection.master_layer):
|
|
394
400
|
self.display_current_view()
|
|
401
|
+
self.update_master_thumbnail()
|
|
395
402
|
self.mark_as_modified()
|
|
396
403
|
self.statusBar().showMessage("Redo applied", 2000)
|
|
397
404
|
|