shinestacker 0.3.6__py3-none-any.whl → 0.5.0__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/align.py +37 -20
- shinestacker/algorithms/balance.py +2 -1
- shinestacker/algorithms/base_stack_algo.py +2 -1
- shinestacker/algorithms/multilayer.py +11 -8
- shinestacker/algorithms/noise_detection.py +13 -7
- shinestacker/algorithms/stack.py +5 -4
- shinestacker/algorithms/stack_framework.py +12 -10
- shinestacker/app/about_dialog.py +69 -1
- shinestacker/app/main.py +1 -1
- shinestacker/config/config.py +1 -0
- shinestacker/config/constants.py +8 -1
- shinestacker/config/gui_constants.py +7 -5
- shinestacker/core/framework.py +15 -10
- shinestacker/gui/action_config.py +11 -7
- shinestacker/gui/actions_window.py +8 -0
- shinestacker/gui/gui_logging.py +8 -7
- shinestacker/gui/gui_run.py +8 -8
- shinestacker/gui/main_window.py +17 -12
- shinestacker/gui/new_project.py +31 -17
- shinestacker/gui/project_converter.py +0 -1
- shinestacker/gui/select_path_widget.py +3 -1
- shinestacker/retouch/brush_tool.py +23 -6
- shinestacker/retouch/display_manager.py +57 -20
- shinestacker/retouch/image_editor.py +5 -9
- shinestacker/retouch/image_editor_ui.py +55 -16
- shinestacker/retouch/image_viewer.py +104 -20
- shinestacker/retouch/io_gui_handler.py +74 -24
- shinestacker/retouch/io_manager.py +23 -8
- shinestacker/retouch/layer_collection.py +2 -1
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/METADATA +5 -4
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/RECORD +36 -36
- shinestacker-0.5.0.dist-info/licenses/LICENSE +165 -0
- shinestacker-0.3.6.dist-info/licenses/LICENSE +0 -1
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -23,6 +23,7 @@ def brush_size_to_slider(size):
|
|
|
23
23
|
|
|
24
24
|
class ImageEditorUI(ImageFilters):
|
|
25
25
|
def __init__(self):
|
|
26
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
|
|
26
27
|
super().__init__()
|
|
27
28
|
self.brush = Brush()
|
|
28
29
|
self.setup_ui()
|
|
@@ -125,17 +126,21 @@ class ImageEditorUI(ImageFilters):
|
|
|
125
126
|
}
|
|
126
127
|
""")
|
|
127
128
|
master_label.setAlignment(Qt.AlignCenter)
|
|
128
|
-
master_label.setFixedHeight(gui_constants.
|
|
129
|
+
master_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
|
|
129
130
|
side_layout.addWidget(master_label)
|
|
130
131
|
self.master_thumbnail_frame = QFrame()
|
|
132
|
+
self.master_thumbnail_frame.setObjectName("thumbnailContainer")
|
|
133
|
+
self.master_thumbnail_frame.setStyleSheet(
|
|
134
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
131
135
|
self.master_thumbnail_frame.setFrameShape(QFrame.StyledPanel)
|
|
132
136
|
master_thumbnail_layout = QVBoxLayout(self.master_thumbnail_frame)
|
|
133
|
-
master_thumbnail_layout.setContentsMargins(
|
|
137
|
+
master_thumbnail_layout.setContentsMargins(8, 8, 8, 8)
|
|
134
138
|
self.master_thumbnail_label = QLabel()
|
|
135
139
|
self.master_thumbnail_label.setAlignment(Qt.AlignCenter)
|
|
136
|
-
self.master_thumbnail_label.
|
|
137
|
-
gui_constants.
|
|
138
|
-
self.master_thumbnail_label.mousePressEvent =
|
|
140
|
+
self.master_thumbnail_label.setFixedWidth(
|
|
141
|
+
gui_constants.UI_SIZES['thumbnail_width'])
|
|
142
|
+
self.master_thumbnail_label.mousePressEvent = \
|
|
143
|
+
lambda e: self.display_manager.set_view_master()
|
|
139
144
|
master_thumbnail_layout.addWidget(self.master_thumbnail_label)
|
|
140
145
|
side_layout.addWidget(self.master_thumbnail_frame)
|
|
141
146
|
side_layout.addSpacing(10)
|
|
@@ -151,7 +156,7 @@ class ImageEditorUI(ImageFilters):
|
|
|
151
156
|
}
|
|
152
157
|
""")
|
|
153
158
|
layers_label.setAlignment(Qt.AlignCenter)
|
|
154
|
-
layers_label.setFixedHeight(gui_constants.
|
|
159
|
+
layers_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
|
|
155
160
|
side_layout.addWidget(layers_label)
|
|
156
161
|
self.thumbnail_list = QListWidget()
|
|
157
162
|
self.thumbnail_list.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -203,18 +208,29 @@ class ImageEditorUI(ImageFilters):
|
|
|
203
208
|
layout.setSpacing(2)
|
|
204
209
|
super().setup_ui()
|
|
205
210
|
|
|
211
|
+
def highlight_master_thumbnail(self):
|
|
212
|
+
self.master_thumbnail_frame.setStyleSheet(
|
|
213
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
214
|
+
|
|
206
215
|
def setup_menu(self):
|
|
207
216
|
menubar = self.menuBar()
|
|
208
217
|
file_menu = menubar.addMenu("&File")
|
|
209
218
|
file_menu.addAction("&Open...", self.io_gui_handler.open_file, "Ctrl+O")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
self.
|
|
213
|
-
self.
|
|
214
|
-
self.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
file_menu.addAction(
|
|
219
|
+
self.save_action = QAction("&Save", self)
|
|
220
|
+
self.save_action.setShortcut("Ctrl+S")
|
|
221
|
+
self.save_action.triggered.connect(self.io_gui_handler.save_file)
|
|
222
|
+
file_menu.addAction(self.save_action)
|
|
223
|
+
self.save_as_action = QAction("Save &As...", self)
|
|
224
|
+
self.save_as_action.setShortcut("Ctrl+Shift+S")
|
|
225
|
+
self.save_as_action.triggered.connect(self.io_gui_handler.save_file_as)
|
|
226
|
+
file_menu.addAction(self.save_as_action)
|
|
227
|
+
self.io_gui_handler.save_master_only = QAction("Save Master &Only", self)
|
|
228
|
+
self.io_gui_handler.save_master_only.setCheckable(True)
|
|
229
|
+
self.io_gui_handler.save_master_only.setChecked(True)
|
|
230
|
+
file_menu.addAction(self.io_gui_handler.save_master_only)
|
|
231
|
+
self.save_actions_set_enabled(False)
|
|
232
|
+
|
|
233
|
+
file_menu.addAction("&Close", self.close_file, "Ctrl+W")
|
|
218
234
|
file_menu.addSeparator()
|
|
219
235
|
file_menu.addAction("&Import frames", self.io_gui_handler.import_frames)
|
|
220
236
|
file_menu.addAction("Import &EXIF data", self.io_gui_handler.select_exif_path)
|
|
@@ -270,12 +286,12 @@ class ImageEditorUI(ImageFilters):
|
|
|
270
286
|
|
|
271
287
|
view_master_action = QAction("View Master", self)
|
|
272
288
|
view_master_action.setShortcut("M")
|
|
273
|
-
view_master_action.triggered.connect(self.
|
|
289
|
+
view_master_action.triggered.connect(self.set_view_master)
|
|
274
290
|
view_menu.addAction(view_master_action)
|
|
275
291
|
|
|
276
292
|
view_individual_action = QAction("View Individual", self)
|
|
277
293
|
view_individual_action.setShortcut("L")
|
|
278
|
-
view_individual_action.triggered.connect(self.
|
|
294
|
+
view_individual_action.triggered.connect(self.set_view_individual)
|
|
279
295
|
view_menu.addAction(view_individual_action)
|
|
280
296
|
view_menu.addSeparator()
|
|
281
297
|
|
|
@@ -333,6 +349,25 @@ class ImageEditorUI(ImageFilters):
|
|
|
333
349
|
shortcuts_help_action.triggered.connect(self.shortcuts_help)
|
|
334
350
|
help_menu.addAction(shortcuts_help_action)
|
|
335
351
|
|
|
352
|
+
def save_actions_set_enabled(self, enabled):
|
|
353
|
+
self.save_action.setEnabled(enabled)
|
|
354
|
+
self.save_as_action.setEnabled(enabled)
|
|
355
|
+
self.io_gui_handler.save_master_only.setEnabled(enabled)
|
|
356
|
+
|
|
357
|
+
def close_file(self):
|
|
358
|
+
self.io_gui_handler.close_file()
|
|
359
|
+
self.save_actions_set_enabled(False)
|
|
360
|
+
|
|
361
|
+
def set_view_master(self):
|
|
362
|
+
self.display_manager.set_view_master()
|
|
363
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
|
|
364
|
+
self.highlight_master_thumbnail()
|
|
365
|
+
|
|
366
|
+
def set_view_individual(self):
|
|
367
|
+
self.display_manager.set_view_individual()
|
|
368
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
|
|
369
|
+
self.highlight_master_thumbnail()
|
|
370
|
+
|
|
336
371
|
def shortcuts_help(self):
|
|
337
372
|
self._dialog = ShortcutsHelp(self)
|
|
338
373
|
self._dialog.exec()
|
|
@@ -364,8 +399,12 @@ class ImageEditorUI(ImageFilters):
|
|
|
364
399
|
def handle_temp_view(self, start):
|
|
365
400
|
if start:
|
|
366
401
|
self.display_manager.start_temp_view()
|
|
402
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
|
|
403
|
+
self.highlight_master_thumbnail()
|
|
367
404
|
else:
|
|
368
405
|
self.display_manager.end_temp_view()
|
|
406
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
|
|
407
|
+
self.highlight_master_thumbnail()
|
|
369
408
|
|
|
370
409
|
def handle_brush_size_change(self, delta):
|
|
371
410
|
if delta > 0:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914, R0912
|
|
2
2
|
import math
|
|
3
3
|
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
|
|
4
4
|
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence
|
|
5
|
-
from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal
|
|
5
|
+
from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal, QEvent
|
|
6
6
|
from .. config.gui_constants import gui_constants
|
|
7
7
|
from .brush_preview import BrushPreviewItem
|
|
8
8
|
from .brush_gradient import create_default_brush_gradient
|
|
@@ -52,6 +52,13 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
52
52
|
self.empty = True
|
|
53
53
|
self.allow_cursor_preview = True
|
|
54
54
|
self.last_brush_pos = None
|
|
55
|
+
self.grabGesture(Qt.PanGesture)
|
|
56
|
+
self.grabGesture(Qt.PinchGesture)
|
|
57
|
+
self.pinch_start_scale = 1.0
|
|
58
|
+
self.last_scroll_pos = QPointF()
|
|
59
|
+
self.gesture_active = False
|
|
60
|
+
self.pinch_center_view = None
|
|
61
|
+
self.pinch_center_scene = None
|
|
55
62
|
|
|
56
63
|
def set_image(self, qimage):
|
|
57
64
|
pixmap = QPixmap.fromImage(qimage)
|
|
@@ -193,25 +200,44 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
193
200
|
super().mouseReleaseEvent(event)
|
|
194
201
|
|
|
195
202
|
def wheelEvent(self, event):
|
|
196
|
-
if self.empty:
|
|
203
|
+
if self.empty or self.gesture_active:
|
|
197
204
|
return
|
|
198
|
-
if
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
self.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
self.
|
|
213
|
-
|
|
214
|
-
|
|
205
|
+
if event.source() == Qt.MouseEventNotSynthesized: # Physical mouse
|
|
206
|
+
if self.control_pressed:
|
|
207
|
+
self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
|
|
208
|
+
else:
|
|
209
|
+
zoom_in_factor = 1.10
|
|
210
|
+
zoom_out_factor = 1 / zoom_in_factor
|
|
211
|
+
current_scale = self.get_current_scale()
|
|
212
|
+
if event.angleDelta().y() > 0: # Zoom in
|
|
213
|
+
new_scale = current_scale * zoom_in_factor
|
|
214
|
+
if new_scale <= self.max_scale:
|
|
215
|
+
self.scale(zoom_in_factor, zoom_in_factor)
|
|
216
|
+
self.zoom_factor = new_scale
|
|
217
|
+
else: # Zoom out
|
|
218
|
+
new_scale = current_scale * zoom_out_factor
|
|
219
|
+
if new_scale >= self.min_scale:
|
|
220
|
+
self.scale(zoom_out_factor, zoom_out_factor)
|
|
221
|
+
self.zoom_factor = new_scale
|
|
222
|
+
self.update_brush_cursor()
|
|
223
|
+
else: # Touchpad event - fallback for systems without gesture recognition
|
|
224
|
+
# Handle touchpad panning (two-finger scroll)
|
|
225
|
+
if not self.control_pressed:
|
|
226
|
+
delta = event.pixelDelta() or event.angleDelta() / 8
|
|
227
|
+
if delta:
|
|
228
|
+
self.horizontalScrollBar().setValue(
|
|
229
|
+
self.horizontalScrollBar().value() - delta.x()
|
|
230
|
+
)
|
|
231
|
+
self.verticalScrollBar().setValue(
|
|
232
|
+
self.verticalScrollBar().value() - delta.y()
|
|
233
|
+
)
|
|
234
|
+
else: # Control + touchpad scroll for zoom
|
|
235
|
+
zoom_in = event.angleDelta().y() > 0
|
|
236
|
+
if zoom_in:
|
|
237
|
+
self.zoom_in()
|
|
238
|
+
else:
|
|
239
|
+
self.zoom_out()
|
|
240
|
+
event.accept()
|
|
215
241
|
|
|
216
242
|
def enterEvent(self, event):
|
|
217
243
|
self.activateWindow()
|
|
@@ -230,6 +256,64 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
230
256
|
super().leaveEvent(event)
|
|
231
257
|
# pylint: enable=C0103
|
|
232
258
|
|
|
259
|
+
def event(self, event):
|
|
260
|
+
if event.type() == QEvent.Gesture:
|
|
261
|
+
return self.handle_gesture_event(event)
|
|
262
|
+
return super().event(event)
|
|
263
|
+
|
|
264
|
+
def handle_gesture_event(self, event):
|
|
265
|
+
handled = False
|
|
266
|
+
pan_gesture = event.gesture(Qt.PanGesture)
|
|
267
|
+
if pan_gesture:
|
|
268
|
+
self.handle_pan_gesture(pan_gesture)
|
|
269
|
+
handled = True
|
|
270
|
+
pinch_gesture = event.gesture(Qt.PinchGesture)
|
|
271
|
+
if pinch_gesture:
|
|
272
|
+
self.handle_pinch_gesture(pinch_gesture)
|
|
273
|
+
handled = True
|
|
274
|
+
if handled:
|
|
275
|
+
event.accept()
|
|
276
|
+
return True
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
def handle_pan_gesture(self, pan_gesture):
|
|
280
|
+
if pan_gesture.state() == Qt.GestureStarted:
|
|
281
|
+
self.last_scroll_pos = pan_gesture.delta()
|
|
282
|
+
self.gesture_active = True
|
|
283
|
+
elif pan_gesture.state() == Qt.GestureUpdated:
|
|
284
|
+
delta = pan_gesture.delta() - self.last_scroll_pos
|
|
285
|
+
self.last_scroll_pos = pan_gesture.delta()
|
|
286
|
+
zoom_factor = self.get_current_scale()
|
|
287
|
+
scaled_delta = delta * (1.0 / zoom_factor)
|
|
288
|
+
self.horizontalScrollBar().setValue(
|
|
289
|
+
self.horizontalScrollBar().value() - int(scaled_delta.x())
|
|
290
|
+
)
|
|
291
|
+
self.verticalScrollBar().setValue(
|
|
292
|
+
self.verticalScrollBar().value() - int(scaled_delta.y())
|
|
293
|
+
)
|
|
294
|
+
elif pan_gesture.state() == Qt.GestureFinished:
|
|
295
|
+
self.gesture_active = False
|
|
296
|
+
|
|
297
|
+
def handle_pinch_gesture(self, pinch):
|
|
298
|
+
if pinch.state() == Qt.GestureStarted:
|
|
299
|
+
self.pinch_start_scale = self.get_current_scale()
|
|
300
|
+
self.pinch_center_view = pinch.centerPoint()
|
|
301
|
+
self.pinch_center_scene = self.mapToScene(self.pinch_center_view.toPoint())
|
|
302
|
+
self.gesture_active = True
|
|
303
|
+
elif pinch.state() == Qt.GestureUpdated:
|
|
304
|
+
new_scale = self.pinch_start_scale * pinch.totalScaleFactor()
|
|
305
|
+
new_scale = max(self.min_scale, min(new_scale, self.max_scale))
|
|
306
|
+
if abs(new_scale - self.get_current_scale()) > 0.01:
|
|
307
|
+
self.resetTransform()
|
|
308
|
+
self.scale(new_scale, new_scale)
|
|
309
|
+
self.zoom_factor = new_scale
|
|
310
|
+
new_center = self.mapToScene(self.pinch_center_view.toPoint())
|
|
311
|
+
delta = self.pinch_center_scene - new_center
|
|
312
|
+
self.translate(delta.x(), delta.y())
|
|
313
|
+
self.update_brush_cursor()
|
|
314
|
+
elif pinch.state() in (Qt.GestureFinished, Qt.GestureCanceled):
|
|
315
|
+
self.gesture_active = False
|
|
316
|
+
|
|
233
317
|
def setup_brush_cursor(self):
|
|
234
318
|
self.setCursor(Qt.BlankCursor)
|
|
235
319
|
pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718
|
|
2
|
+
import os
|
|
2
3
|
import traceback
|
|
3
4
|
import numpy as np
|
|
4
5
|
from PySide6.QtWidgets import QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog, QApplication
|
|
@@ -6,7 +7,7 @@ from PySide6.QtGui import QGuiApplication, QCursor
|
|
|
6
7
|
from PySide6.QtCore import Qt, QObject, QTimer, Signal
|
|
7
8
|
from .file_loader import FileLoader
|
|
8
9
|
from .exif_data import ExifData
|
|
9
|
-
from .io_manager import IOManager
|
|
10
|
+
from .io_manager import IOManager, FileMultilayerSaver
|
|
10
11
|
from .layer_collection import LayerCollectionHandler
|
|
11
12
|
|
|
12
13
|
|
|
@@ -23,11 +24,19 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
23
24
|
self.loader_thread = None
|
|
24
25
|
self.display_manager = None
|
|
25
26
|
self.image_viewer = None
|
|
26
|
-
self.modified = None
|
|
27
27
|
self.blank_layer = None
|
|
28
28
|
self.loading_dialog = None
|
|
29
29
|
self.loading_timer = None
|
|
30
30
|
self.exif_dialog = None
|
|
31
|
+
self.saver_thread = None
|
|
32
|
+
self.saving_dialog = None
|
|
33
|
+
self.saving_timer = None
|
|
34
|
+
self.current_file_path_master = ''
|
|
35
|
+
self.current_file_path_multi = ''
|
|
36
|
+
|
|
37
|
+
def current_file_path(self):
|
|
38
|
+
return self.current_file_path_master if self.save_master_only.isChecked() \
|
|
39
|
+
else self.current_file_path_multi
|
|
31
40
|
|
|
32
41
|
def setup_ui(self, display_manager, image_viewer):
|
|
33
42
|
self.display_manager = display_manager
|
|
@@ -43,16 +52,18 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
43
52
|
else:
|
|
44
53
|
self.set_layer_labels(labels)
|
|
45
54
|
self.set_master_layer(master_layer)
|
|
46
|
-
self.modified = False
|
|
47
55
|
self.undo_manager.reset()
|
|
48
56
|
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
49
57
|
self.display_manager.update_thumbnails()
|
|
50
58
|
self.image_viewer.setup_brush_cursor()
|
|
51
|
-
self.parent().change_layer(0)
|
|
52
59
|
self.image_viewer.reset_zoom()
|
|
53
|
-
self.status_message_requested.emit(f"Loaded: {self.
|
|
54
|
-
self.parent().thumbnail_list.setFocus()
|
|
60
|
+
self.status_message_requested.emit(f"Loaded: {self.current_file_path()}")
|
|
55
61
|
self.update_title_requested.emit()
|
|
62
|
+
self.current_file_path_master = ''
|
|
63
|
+
self.current_file_path_multi = ''
|
|
64
|
+
self.parent().mark_as_modified()
|
|
65
|
+
self.parent().change_layer(0)
|
|
66
|
+
self.parent().thumbnail_list.setFocus()
|
|
56
67
|
|
|
57
68
|
def on_file_error(self, error_msg):
|
|
58
69
|
QApplication.restoreOverrideCursor()
|
|
@@ -60,7 +71,23 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
60
71
|
self.loading_dialog.accept()
|
|
61
72
|
self.loading_dialog.deleteLater()
|
|
62
73
|
QMessageBox.critical(self.parent(), "Error", error_msg)
|
|
63
|
-
self.status_message_requested.emit(f"Error loading: {self.
|
|
74
|
+
self.status_message_requested.emit(f"Error loading: {self.current_file_path()}")
|
|
75
|
+
|
|
76
|
+
def on_multilayer_save_success(self):
|
|
77
|
+
QApplication.restoreOverrideCursor()
|
|
78
|
+
self.saving_timer.stop()
|
|
79
|
+
self.saving_dialog.hide()
|
|
80
|
+
self.saving_dialog.deleteLater()
|
|
81
|
+
self.parent().modified = False
|
|
82
|
+
self.update_title_requested.emit()
|
|
83
|
+
self.status_message_requested.emit(f"Saved multilayer to: {self.current_file_path_multi}")
|
|
84
|
+
|
|
85
|
+
def on_multilayer_save_error(self, error_msg):
|
|
86
|
+
QApplication.restoreOverrideCursor()
|
|
87
|
+
self.saving_timer.stop()
|
|
88
|
+
self.saving_dialog.hide()
|
|
89
|
+
self.saving_dialog.deleteLater()
|
|
90
|
+
QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {error_msg}")
|
|
64
91
|
|
|
65
92
|
def open_file(self, file_paths=None):
|
|
66
93
|
if file_paths is None:
|
|
@@ -76,7 +103,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
76
103
|
self.import_frames_from_files(file_paths)
|
|
77
104
|
return
|
|
78
105
|
path = file_paths[0] if isinstance(file_paths, list) else file_paths
|
|
79
|
-
self.
|
|
106
|
+
self.current_file_path_master = os.path.abspath(path)
|
|
107
|
+
self.current_file_path_multi = os.path.abspath(path)
|
|
80
108
|
QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
|
|
81
109
|
self.loading_dialog = QDialog(self.parent())
|
|
82
110
|
self.loading_dialog.setWindowTitle("Loading")
|
|
@@ -128,13 +156,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
128
156
|
self.display_manager.update_thumbnails()
|
|
129
157
|
|
|
130
158
|
def save_file(self):
|
|
131
|
-
if self.
|
|
159
|
+
if self.save_master_only.isChecked():
|
|
132
160
|
self.save_master()
|
|
133
161
|
else:
|
|
134
162
|
self.save_multilayer()
|
|
135
163
|
|
|
136
164
|
def save_file_as(self):
|
|
137
|
-
if self.
|
|
165
|
+
if self.save_master_only.isChecked():
|
|
138
166
|
self.save_master_as()
|
|
139
167
|
else:
|
|
140
168
|
self.save_multilayer_as()
|
|
@@ -142,11 +170,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
142
170
|
def save_multilayer(self):
|
|
143
171
|
if self.layer_stack() is None:
|
|
144
172
|
return
|
|
145
|
-
if self.
|
|
146
|
-
extension = self.
|
|
173
|
+
if self.current_file_path_multi != '':
|
|
174
|
+
extension = self.current_file_path_multi.split('.')[-1]
|
|
147
175
|
if extension in ['tif', 'tiff']:
|
|
148
|
-
self.save_multilayer_to_path(self.
|
|
176
|
+
self.save_multilayer_to_path(self.current_file_path_multi)
|
|
149
177
|
return
|
|
178
|
+
else:
|
|
179
|
+
self.save_multilayer_as()
|
|
150
180
|
|
|
151
181
|
def save_multilayer_as(self):
|
|
152
182
|
if self.layer_stack() is None:
|
|
@@ -160,11 +190,30 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
160
190
|
|
|
161
191
|
def save_multilayer_to_path(self, path):
|
|
162
192
|
try:
|
|
163
|
-
self.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
master_layer = {'Master': self.master_layer().copy()}
|
|
194
|
+
individual_layers = dict(zip(
|
|
195
|
+
self.layer_labels(),
|
|
196
|
+
[layer.copy() for layer in self.layer_stack()]
|
|
197
|
+
))
|
|
198
|
+
images_dict = {**master_layer, **individual_layers}
|
|
199
|
+
self.saver_thread = FileMultilayerSaver(
|
|
200
|
+
images_dict, path, exif_path=self.io_manager.exif_path)
|
|
201
|
+
self.saver_thread.finished.connect(self.on_multilayer_save_success)
|
|
202
|
+
self.saver_thread.error.connect(self.on_multilayer_save_error)
|
|
203
|
+
QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
|
|
204
|
+
self.saving_dialog = QDialog(self.parent())
|
|
205
|
+
self.saving_dialog.setWindowTitle("Saving")
|
|
206
|
+
self.saving_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
|
|
207
|
+
self.saving_dialog.setModal(True)
|
|
208
|
+
layout = QVBoxLayout()
|
|
209
|
+
layout.addWidget(QLabel("Saving file..."))
|
|
210
|
+
self.saving_dialog.setLayout(layout)
|
|
211
|
+
self.saving_timer = QTimer()
|
|
212
|
+
self.saving_timer.setSingleShot(True)
|
|
213
|
+
self.saving_timer.timeout.connect(self.saving_dialog.show)
|
|
214
|
+
self.saving_timer.start(100)
|
|
215
|
+
self.saver_thread.start()
|
|
216
|
+
|
|
168
217
|
except Exception as e:
|
|
169
218
|
traceback.print_tb(e.__traceback__)
|
|
170
219
|
QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
|
|
@@ -172,8 +221,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
172
221
|
def save_master(self):
|
|
173
222
|
if self.master_layer() is None:
|
|
174
223
|
return
|
|
175
|
-
if self.
|
|
176
|
-
self.save_master_to_path(self.
|
|
224
|
+
if self.current_file_path_master != '':
|
|
225
|
+
self.save_master_to_path(self.current_file_path_master)
|
|
177
226
|
return
|
|
178
227
|
self.save_master_as()
|
|
179
228
|
|
|
@@ -189,8 +238,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
189
238
|
def save_master_to_path(self, path):
|
|
190
239
|
try:
|
|
191
240
|
self.io_manager.save_master(path)
|
|
192
|
-
self.
|
|
193
|
-
self.modified = False
|
|
241
|
+
self.current_file_path_master = os.path.abspath(path)
|
|
242
|
+
self.parent().modified = False
|
|
194
243
|
self.update_title_requested.emit()
|
|
195
244
|
self.status_message_requested.emit(f"Saved master layer to: {path}")
|
|
196
245
|
except Exception as e:
|
|
@@ -210,8 +259,9 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
210
259
|
self.set_master_layer(None)
|
|
211
260
|
self.blank_layer = None
|
|
212
261
|
self.layer_collection.reset()
|
|
213
|
-
self.
|
|
214
|
-
self.
|
|
262
|
+
self.current_file_path_master = ''
|
|
263
|
+
self.current_file_path_multi = ''
|
|
264
|
+
self.parent().modified = False
|
|
215
265
|
self.undo_manager.reset()
|
|
216
266
|
self.image_viewer.clear_image()
|
|
217
267
|
self.display_manager.thumbnail_list.clear()
|
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
# pylint: disable=E1101, C0114, C0115, C0116
|
|
1
|
+
# pylint: disable=E1101, C0114, C0115, C0116, E0611, W0718, R0903
|
|
2
|
+
import traceback
|
|
2
3
|
import cv2
|
|
4
|
+
from PySide6.QtCore import QThread, Signal
|
|
3
5
|
from .. algorithms.utils import read_img, validate_image, get_img_metadata
|
|
4
6
|
from .. algorithms.exif import get_exif, write_image_with_exif_data
|
|
5
7
|
from .. algorithms.multilayer import write_multilayer_tiff_from_images
|
|
6
8
|
from .layer_collection import LayerCollectionHandler
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
class FileMultilayerSaver(QThread):
|
|
12
|
+
finished = Signal()
|
|
13
|
+
error = Signal(str)
|
|
14
|
+
|
|
15
|
+
def __init__(self, images_dict, path, exif_path=None):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.images_dict = images_dict
|
|
18
|
+
self.path = path
|
|
19
|
+
self.exif_path = exif_path
|
|
20
|
+
|
|
21
|
+
def run(self):
|
|
22
|
+
try:
|
|
23
|
+
write_multilayer_tiff_from_images(
|
|
24
|
+
self.images_dict, self.path, exif_path=self.exif_path)
|
|
25
|
+
self.finished.emit()
|
|
26
|
+
except Exception as e:
|
|
27
|
+
traceback.print_tb(e.__traceback__)
|
|
28
|
+
self.error.emit(str(e))
|
|
29
|
+
|
|
30
|
+
|
|
9
31
|
class IOManager(LayerCollectionHandler):
|
|
10
32
|
def __init__(self, layer_collection):
|
|
11
33
|
super().__init__(layer_collection)
|
|
12
|
-
self.current_file_path = ''
|
|
13
34
|
self.exif_path = ''
|
|
14
35
|
self.exif_data = None
|
|
15
36
|
|
|
@@ -39,12 +60,6 @@ class IOManager(LayerCollectionHandler):
|
|
|
39
60
|
raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
|
|
40
61
|
return stack, labels, master
|
|
41
62
|
|
|
42
|
-
def save_multilayer(self, path):
|
|
43
|
-
master_layer = {'Master': self.master_layer()}
|
|
44
|
-
individual_layers = dict(zip(self.layer_labels(), self.layer_stack()))
|
|
45
|
-
write_multilayer_tiff_from_images({**master_layer, **individual_layers},
|
|
46
|
-
path, exif_path=self.exif_path)
|
|
47
|
-
|
|
48
63
|
def save_master(self, path):
|
|
49
64
|
img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
|
|
50
65
|
write_image_with_exif_data(self.exif_data, img, path)
|
|
@@ -80,7 +80,8 @@ class LayerCollection:
|
|
|
80
80
|
master_label = None
|
|
81
81
|
master_layer = None
|
|
82
82
|
for i, label in enumerate(self.layer_labels):
|
|
83
|
-
|
|
83
|
+
label_lower = label.lower()
|
|
84
|
+
if "master" in label_lower or "stack" in label_lower:
|
|
84
85
|
master_index = i
|
|
85
86
|
master_label = self.layer_labels.pop(i)
|
|
86
87
|
master_layer = self.layer_stack[i]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -37,10 +37,10 @@ Dynamic: license-file
|
|
|
37
37
|
[](https://pypi.org/project/shinestacker/)
|
|
38
38
|
[](https://pypi.org/project/shinestacker/)
|
|
39
39
|
[](https://www.qt.io/qt-for-python)
|
|
40
|
-
[](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
|
|
41
41
|
[](https://codecov.io/github/lucalista/shinestacker)
|
|
42
42
|
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
43
|
-
|
|
43
|
+
[](https://www.gnu.org/licenses/lgpl-3.0)
|
|
44
44
|
|
|
45
45
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
|
|
46
46
|
|
|
@@ -83,7 +83,8 @@ Pyramid methods in image processing
|
|
|
83
83
|
|
|
84
84
|
# License
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
87
|
+
The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
|
|
87
88
|
|
|
88
89
|
# Attribution request
|
|
89
90
|
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|