shinestacker 0.4.0__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/app/about_dialog.py +69 -1
- shinestacker/config/gui_constants.py +7 -5
- shinestacker/gui/actions_window.py +8 -0
- shinestacker/gui/main_window.py +13 -8
- 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 +53 -15
- shinestacker/retouch/io_gui_handler.py +71 -22
- shinestacker/retouch/io_manager.py +23 -7
- {shinestacker-0.4.0.dist-info → shinestacker-0.5.0.dist-info}/METADATA +1 -1
- {shinestacker-0.4.0.dist-info → shinestacker-0.5.0.dist-info}/RECORD +17 -17
- {shinestacker-0.4.0.dist-info → shinestacker-0.5.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-0.5.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-0.5.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.5.0'
|
shinestacker/app/about_dialog.py
CHANGED
|
@@ -1,15 +1,82 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0116, E0611
|
|
1
|
+
# pylint: disable=C0114, C0116, E0611, W0718
|
|
2
|
+
import json
|
|
3
|
+
from urllib.request import urlopen, Request
|
|
4
|
+
from urllib.error import URLError
|
|
2
5
|
from PySide6.QtWidgets import QMessageBox
|
|
3
6
|
from PySide6.QtCore import Qt
|
|
4
7
|
from .. import __version__
|
|
5
8
|
from .. config.constants import constants
|
|
6
9
|
|
|
7
10
|
|
|
11
|
+
def compare_versions(current, latest):
|
|
12
|
+
def parse_version(v):
|
|
13
|
+
v = v.lstrip('v')
|
|
14
|
+
parts = v.split('.')
|
|
15
|
+
result = []
|
|
16
|
+
for part in parts:
|
|
17
|
+
try:
|
|
18
|
+
result.append(int(part))
|
|
19
|
+
except ValueError:
|
|
20
|
+
result.append(part)
|
|
21
|
+
return result
|
|
22
|
+
current_parts = parse_version(current)
|
|
23
|
+
latest_parts = parse_version(latest)
|
|
24
|
+
for i in range(max(len(current_parts), len(latest_parts))):
|
|
25
|
+
c = current_parts[i] if i < len(current_parts) else 0
|
|
26
|
+
l = latest_parts[i] if i < len(latest_parts) else 0 # noqa: E741
|
|
27
|
+
if isinstance(c, int) and isinstance(l, int):
|
|
28
|
+
if c < l:
|
|
29
|
+
return -1
|
|
30
|
+
if c > l:
|
|
31
|
+
return 1
|
|
32
|
+
else:
|
|
33
|
+
if str(c) < str(l):
|
|
34
|
+
return -1
|
|
35
|
+
if str(c) > str(l):
|
|
36
|
+
return 1
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_latest_version():
|
|
41
|
+
try:
|
|
42
|
+
url = "https://api.github.com/repos/lucalista/shinestacker/releases/latest"
|
|
43
|
+
headers = {'User-Agent': 'ShineStacker'}
|
|
44
|
+
req = Request(url, headers=headers)
|
|
45
|
+
with urlopen(req, timeout=5) as response:
|
|
46
|
+
data = json.loads(response.read().decode())
|
|
47
|
+
return data['tag_name']
|
|
48
|
+
except (URLError, ValueError, KeyError, TimeoutError):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
8
52
|
def show_about_dialog():
|
|
9
53
|
version_clean = __version__.split("+", maxsplit=1)[0]
|
|
54
|
+
latest_version = None
|
|
55
|
+
try:
|
|
56
|
+
latest_version = get_latest_version()
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
update_text = ""
|
|
60
|
+
# pyling: disable=XXX
|
|
61
|
+
if latest_version:
|
|
62
|
+
latest_clean = latest_version.lstrip('v')
|
|
63
|
+
if compare_versions(version_clean, latest_clean) < 0:
|
|
64
|
+
update_text = f"""
|
|
65
|
+
<p style="color: red; font-weight: bold;">
|
|
66
|
+
Update available! Latest version: {latest_version}
|
|
67
|
+
<br><a href="https://github.com/lucalista/shinestacker/releases/latest">Download here</a>
|
|
68
|
+
</p>
|
|
69
|
+
""" # noqa E501
|
|
70
|
+
else:
|
|
71
|
+
update_text = f"""
|
|
72
|
+
<p style="color: green; font-weight: bold;">
|
|
73
|
+
You are using the lastet version: {latest_version}.
|
|
74
|
+
</p>
|
|
75
|
+
"""
|
|
10
76
|
about_text = f"""
|
|
11
77
|
<h3>{constants.APP_TITLE}</h3>
|
|
12
78
|
<h4>version: v{version_clean}</h4>
|
|
79
|
+
{update_text}
|
|
13
80
|
<p style='font-weight: normal;'>App and framework to combine multiple images
|
|
14
81
|
into a single focused image.</p>
|
|
15
82
|
<p>Author: Luca Lista<br/>
|
|
@@ -19,6 +86,7 @@ def show_about_dialog():
|
|
|
19
86
|
<li><a href="https://github.com/lucalista/shinestacker">GitHub project repository</a></li>
|
|
20
87
|
</ul>
|
|
21
88
|
"""
|
|
89
|
+
# pyling: enable=XXX
|
|
22
90
|
msg = QMessageBox()
|
|
23
91
|
msg.setWindowTitle(f"About {constants.APP_STRING}")
|
|
24
92
|
msg.setIcon(QMessageBox.Icon.Information)
|
|
@@ -36,9 +36,10 @@ class _GuiConstants:
|
|
|
36
36
|
|
|
37
37
|
THUMB_WIDTH = 120 # px
|
|
38
38
|
THUMB_HEIGHT = 80 # px
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
THUMB_HI_COLOR = '#0000FF'
|
|
40
|
+
THUMB_LO_COLOR = '#0000FF'
|
|
41
|
+
THUMB_MASTER_HI_COLOR = '#0000FF'
|
|
42
|
+
THUMB_MASTER_LO_COLOR = 'transparent'
|
|
42
43
|
|
|
43
44
|
MAX_UNDO_STEPS = 50
|
|
44
45
|
|
|
@@ -46,8 +47,9 @@ class _GuiConstants:
|
|
|
46
47
|
|
|
47
48
|
UI_SIZES = {
|
|
48
49
|
'brush_preview': (100, 80),
|
|
49
|
-
'
|
|
50
|
-
'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT)
|
|
50
|
+
'thumbnail_width': 100,
|
|
51
|
+
'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT),
|
|
52
|
+
'label_height': 20
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
DEFAULT_BRUSH_HARDNESS = 50
|
|
@@ -30,6 +30,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
30
30
|
def mark_as_modified(self):
|
|
31
31
|
self._modified_project = True
|
|
32
32
|
self.project_buffer.append(self.project.clone())
|
|
33
|
+
self.save_actions_set_enabled(True)
|
|
33
34
|
self.update_title()
|
|
34
35
|
|
|
35
36
|
def close_project(self):
|
|
@@ -40,6 +41,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
40
41
|
self.job_list.clear()
|
|
41
42
|
self.action_list.clear()
|
|
42
43
|
self._modified_project = False
|
|
44
|
+
self.save_actions_set_enabled(False)
|
|
43
45
|
|
|
44
46
|
def new_project(self):
|
|
45
47
|
if not self._check_unsaved_changes():
|
|
@@ -51,8 +53,10 @@ class ActionsWindow(ProjectEditor):
|
|
|
51
53
|
self.job_list.clear()
|
|
52
54
|
self.action_list.clear()
|
|
53
55
|
self.set_project(Project())
|
|
56
|
+
self.save_actions_set_enabled(False)
|
|
54
57
|
dialog = NewProjectDialog(self)
|
|
55
58
|
if dialog.exec() == QDialog.Accepted:
|
|
59
|
+
self.save_actions_set_enabled(True)
|
|
56
60
|
input_folder = dialog.get_input_folder().split('/')
|
|
57
61
|
working_path = '/'.join(input_folder[:-1])
|
|
58
62
|
input_path = input_folder[-1]
|
|
@@ -142,6 +146,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
142
146
|
if len(self.project.jobs) > 0:
|
|
143
147
|
self.job_list.setCurrentRow(0)
|
|
144
148
|
self.activateWindow()
|
|
149
|
+
self.save_actions_set_enabled(True)
|
|
145
150
|
for job in self.project.jobs:
|
|
146
151
|
if 'working_path' in job.params.keys():
|
|
147
152
|
working_path = job.params['working_path']
|
|
@@ -256,3 +261,6 @@ class ActionsWindow(ProjectEditor):
|
|
|
256
261
|
if dialog.exec() == QDialog.Accepted:
|
|
257
262
|
self.on_job_selected(self.job_list.currentRow())
|
|
258
263
|
self.mark_as_modified()
|
|
264
|
+
|
|
265
|
+
def save_actions_set_enabled(self, enabled):
|
|
266
|
+
pass
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -189,19 +189,24 @@ class MainWindow(ActionsWindow, LogManager):
|
|
|
189
189
|
open_action.setShortcut("Ctrl+O")
|
|
190
190
|
open_action.triggered.connect(self.open_project)
|
|
191
191
|
menu.addAction(open_action)
|
|
192
|
-
save_action = QAction("&Save", self)
|
|
193
|
-
save_action.setShortcut("Ctrl+S")
|
|
194
|
-
save_action.triggered.connect(self.save_project)
|
|
195
|
-
menu.addAction(save_action)
|
|
196
|
-
save_as_action = QAction("Save &As...", self)
|
|
197
|
-
save_as_action.setShortcut("Ctrl+Shift+S")
|
|
198
|
-
save_as_action.triggered.connect(self.save_project_as)
|
|
199
|
-
menu.addAction(save_as_action)
|
|
192
|
+
self.save_action = QAction("&Save", self)
|
|
193
|
+
self.save_action.setShortcut("Ctrl+S")
|
|
194
|
+
self.save_action.triggered.connect(self.save_project)
|
|
195
|
+
menu.addAction(self.save_action)
|
|
196
|
+
self.save_as_action = QAction("Save &As...", self)
|
|
197
|
+
self.save_as_action.setShortcut("Ctrl+Shift+S")
|
|
198
|
+
self.save_as_action.triggered.connect(self.save_project_as)
|
|
199
|
+
menu.addAction(self.save_as_action)
|
|
200
|
+
self.save_actions_set_enabled(False)
|
|
200
201
|
close_action = QAction("&Close", self)
|
|
201
202
|
close_action.setShortcut("Ctrl+W")
|
|
202
203
|
close_action.triggered.connect(self.close_project)
|
|
203
204
|
menu.addAction(close_action)
|
|
204
205
|
|
|
206
|
+
def save_actions_set_enabled(self, enabled):
|
|
207
|
+
self.save_action.setEnabled(enabled)
|
|
208
|
+
self.save_as_action.setEnabled(enabled)
|
|
209
|
+
|
|
205
210
|
def add_edit_menu(self, menubar):
|
|
206
211
|
menu = menubar.addMenu("&Edit")
|
|
207
212
|
undo_action = QAction("&Undo", self)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0902, R0913, R0917, R0914
|
|
2
2
|
import numpy as np
|
|
3
|
-
from PySide6.
|
|
3
|
+
from PySide6.QtWidgets import QApplication, QLabel
|
|
4
|
+
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QFont
|
|
4
5
|
from PySide6.QtCore import Qt, QPoint
|
|
5
6
|
from .brush_gradient import create_default_brush_gradient
|
|
6
7
|
from .. config.gui_constants import gui_constants
|
|
@@ -18,11 +19,16 @@ class BrushTool:
|
|
|
18
19
|
self.opacity_slider = None
|
|
19
20
|
self.flow_slider = None
|
|
20
21
|
self._brush_mask_cache = {}
|
|
22
|
+
self.brush_text = None
|
|
21
23
|
|
|
22
24
|
def setup_ui(self, brush, brush_preview, image_viewer, size_slider, hardness_slider,
|
|
23
25
|
opacity_slider, flow_slider):
|
|
24
26
|
self.brush = brush
|
|
25
27
|
self.brush_preview = brush_preview
|
|
28
|
+
self.brush_text = QLabel(brush_preview.parent())
|
|
29
|
+
self.brush_text.setStyleSheet("color: navy; background: transparent;")
|
|
30
|
+
self.brush_text.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
31
|
+
self.brush_text.raise_()
|
|
26
32
|
self.image_viewer = image_viewer
|
|
27
33
|
self.size_slider = size_slider
|
|
28
34
|
self.hardness_slider = hardness_slider
|
|
@@ -86,7 +92,7 @@ class BrushTool:
|
|
|
86
92
|
pixmap = QPixmap(width, height)
|
|
87
93
|
pixmap.fill(Qt.transparent)
|
|
88
94
|
painter = QPainter(pixmap)
|
|
89
|
-
painter.setRenderHint(QPainter.
|
|
95
|
+
painter.setRenderHint(QPainter.TextAntialiasing, True)
|
|
90
96
|
preview_size = min(self.brush.size, width + 30, height + 30)
|
|
91
97
|
center_x, center_y = width // 2, height // 2
|
|
92
98
|
radius = preview_size // 2
|
|
@@ -109,10 +115,21 @@ class BrushTool:
|
|
|
109
115
|
painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
|
|
110
116
|
if self.image_viewer.cursor_style == 'preview':
|
|
111
117
|
painter.setPen(QPen(QColor(0, 0, 160)))
|
|
112
|
-
|
|
113
|
-
painter.
|
|
114
|
-
|
|
115
|
-
painter.
|
|
118
|
+
font = QApplication.font()
|
|
119
|
+
painter.setFont(font)
|
|
120
|
+
font.setHintingPreference(QFont.PreferFullHinting)
|
|
121
|
+
painter.setFont(font)
|
|
122
|
+
self.brush_text.setText(
|
|
123
|
+
f"Size: {int(self.brush.size)}px\n"
|
|
124
|
+
f"Hardness: {self.brush.hardness}%\n"
|
|
125
|
+
f"Opacity: {self.brush.opacity}%\n"
|
|
126
|
+
f"Flow: {self.brush.flow}%"
|
|
127
|
+
)
|
|
128
|
+
self.brush_text.adjustSize()
|
|
129
|
+
self.brush_text.move(10, self.brush_preview.height() // 2 + 125)
|
|
130
|
+
self.brush_text.show()
|
|
131
|
+
else:
|
|
132
|
+
self.brush_text.hide()
|
|
116
133
|
painter.end()
|
|
117
134
|
self.brush_preview.setPixmap(pixmap)
|
|
118
135
|
self.image_viewer.update_brush_cursor()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902
|
|
2
2
|
import numpy as np
|
|
3
|
-
from PySide6.QtWidgets import QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog
|
|
3
|
+
from PySide6.QtWidgets import (QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog,
|
|
4
|
+
QAbstractItemView)
|
|
4
5
|
from PySide6.QtGui import QPixmap, QImage
|
|
5
6
|
from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
|
|
6
7
|
from .. config.gui_constants import gui_constants
|
|
@@ -39,6 +40,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
39
40
|
self.update_timer = QTimer()
|
|
40
41
|
self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
|
|
41
42
|
self.update_timer.timeout.connect(self.process_pending_updates)
|
|
43
|
+
self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
|
|
42
44
|
|
|
43
45
|
def process_pending_updates(self):
|
|
44
46
|
if self.needs_update:
|
|
@@ -64,15 +66,15 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
64
66
|
self.display_master_layer()
|
|
65
67
|
|
|
66
68
|
def create_thumbnail(self, layer):
|
|
67
|
-
if layer.dtype == np.uint16
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
|
|
69
|
+
source_layer = (layer // 256).astype(np.uint8) if layer.dtype == np.uint16 else layer
|
|
70
|
+
height, width = source_layer.shape[:2]
|
|
71
|
+
if layer.ndim == 3 and source_layer.shape[-1] == 3:
|
|
72
|
+
qimg = QImage(source_layer.data, width, height, 3 * width, QImage.Format_RGB888)
|
|
72
73
|
else:
|
|
73
|
-
qimg = QImage(
|
|
74
|
+
qimg = QImage(source_layer.data, width, height, width, QImage.Format_Grayscale8)
|
|
74
75
|
return QPixmap.fromImage(
|
|
75
|
-
qimg.
|
|
76
|
+
qimg.scaledToWidth(
|
|
77
|
+
gui_constants.UI_SIZES['thumbnail_width'], Qt.SmoothTransformation))
|
|
76
78
|
|
|
77
79
|
def update_thumbnails(self):
|
|
78
80
|
self.update_master_thumbnail()
|
|
@@ -103,17 +105,22 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
103
105
|
self.master_thumbnail_label.setPixmap(pixmap)
|
|
104
106
|
|
|
105
107
|
def add_thumbnail_item(self, thumbnail, label, i, is_current):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
container = QWidget()
|
|
109
|
+
container.setFixedWidth(gui_constants.UI_SIZES['thumbnail_width'] + 4)
|
|
110
|
+
container.setObjectName("thumbnailContainer")
|
|
111
|
+
container_layout = QVBoxLayout(container)
|
|
112
|
+
container_layout.setContentsMargins(2, 2, 2, 2)
|
|
113
|
+
container_layout.setSpacing(0)
|
|
114
|
+
content_widget = QWidget()
|
|
115
|
+
content_layout = QVBoxLayout(content_widget)
|
|
116
|
+
content_layout.setContentsMargins(0, 0, 0, 0)
|
|
117
|
+
content_layout.setSpacing(0)
|
|
111
118
|
thumbnail_label = QLabel()
|
|
112
119
|
thumbnail_label.setPixmap(thumbnail)
|
|
113
120
|
thumbnail_label.setAlignment(Qt.AlignCenter)
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
content_layout.addWidget(thumbnail_label)
|
|
116
122
|
label_widget = ClickableLabel(label)
|
|
123
|
+
label_widget.setFixedHeight(gui_constants.UI_SIZES['label_height'])
|
|
117
124
|
label_widget.setAlignment(Qt.AlignCenter)
|
|
118
125
|
|
|
119
126
|
def rename_label(label_widget, old_label, i):
|
|
@@ -124,21 +131,45 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
124
131
|
self.set_layer_labels(i, new_label)
|
|
125
132
|
|
|
126
133
|
label_widget.double_clicked.connect(lambda: rename_label(label_widget, label, i))
|
|
127
|
-
|
|
134
|
+
content_layout.addWidget(label_widget)
|
|
135
|
+
container_layout.addWidget(content_widget)
|
|
136
|
+
if is_current:
|
|
137
|
+
container.setStyleSheet(
|
|
138
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
139
|
+
else:
|
|
140
|
+
container.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
|
|
128
141
|
item = QListWidgetItem()
|
|
129
|
-
item.setSizeHint(QSize(gui_constants.
|
|
142
|
+
item.setSizeHint(QSize(gui_constants.UI_SIZES['thumbnail_width'] + 4,
|
|
143
|
+
thumbnail.height() + label_widget.height() + 4))
|
|
130
144
|
self.thumbnail_list.addItem(item)
|
|
131
|
-
self.thumbnail_list.setItemWidget(item,
|
|
132
|
-
|
|
145
|
+
self.thumbnail_list.setItemWidget(item, container)
|
|
133
146
|
if is_current:
|
|
134
147
|
self.thumbnail_list.setCurrentItem(item)
|
|
135
148
|
|
|
149
|
+
def highlight_thumbnail(self, index):
|
|
150
|
+
for i in range(self.thumbnail_list.count()):
|
|
151
|
+
item = self.thumbnail_list.item(i)
|
|
152
|
+
widget = self.thumbnail_list.itemWidget(item)
|
|
153
|
+
if widget:
|
|
154
|
+
widget.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
|
|
155
|
+
current_item = self.thumbnail_list.item(index)
|
|
156
|
+
if current_item:
|
|
157
|
+
widget = self.thumbnail_list.itemWidget(current_item)
|
|
158
|
+
if widget:
|
|
159
|
+
widget.setStyleSheet(
|
|
160
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
161
|
+
self.thumbnail_list.setCurrentRow(index)
|
|
162
|
+
self.thumbnail_list.scrollToItem(
|
|
163
|
+
self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
164
|
+
|
|
136
165
|
def set_view_master(self):
|
|
137
166
|
if self.has_no_master_layer():
|
|
138
167
|
return
|
|
139
168
|
self.view_mode = 'master'
|
|
140
169
|
self.temp_view_individual = False
|
|
141
170
|
self.display_master_layer()
|
|
171
|
+
self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
|
|
172
|
+
self.highlight_thumbnail(self.current_layer_idx())
|
|
142
173
|
self.status_message_requested.emit("View mode: Master")
|
|
143
174
|
self.cursor_preview_state_changed.emit(True) # True = allow preview
|
|
144
175
|
|
|
@@ -148,6 +179,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
148
179
|
self.view_mode = 'individual'
|
|
149
180
|
self.temp_view_individual = False
|
|
150
181
|
self.display_current_layer()
|
|
182
|
+
self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
|
|
183
|
+
self.highlight_thumbnail(self.current_layer_idx())
|
|
151
184
|
self.status_message_requested.emit("View mode: Individual layers")
|
|
152
185
|
self.cursor_preview_state_changed.emit(False) # False = no preview
|
|
153
186
|
|
|
@@ -155,6 +188,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
155
188
|
if not self.temp_view_individual and self.view_mode == 'master':
|
|
156
189
|
self.temp_view_individual = True
|
|
157
190
|
self.image_viewer.update_brush_cursor()
|
|
191
|
+
self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
|
|
192
|
+
self.highlight_thumbnail(self.current_layer_idx())
|
|
158
193
|
self.display_current_layer()
|
|
159
194
|
self.status_message_requested.emit("Temporary view: Individual layer (hold X)")
|
|
160
195
|
|
|
@@ -162,6 +197,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
162
197
|
if self.temp_view_individual:
|
|
163
198
|
self.temp_view_individual = False
|
|
164
199
|
self.image_viewer.update_brush_cursor()
|
|
200
|
+
self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
|
|
201
|
+
self.highlight_thumbnail(self.current_layer_idx())
|
|
165
202
|
self.display_master_layer()
|
|
166
203
|
self.status_message_requested.emit("View mode: Master")
|
|
167
204
|
self.cursor_preview_state_changed.emit(True) # Restore preview
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0902
|
|
2
|
-
from PySide6.QtWidgets import QMainWindow, QMessageBox
|
|
2
|
+
from PySide6.QtWidgets import QMainWindow, QMessageBox
|
|
3
3
|
from .. config.constants import constants
|
|
4
4
|
from .undo_manager import UndoManager
|
|
5
5
|
from .layer_collection import LayerCollection
|
|
@@ -87,7 +87,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
|
|
|
87
87
|
def update_title(self):
|
|
88
88
|
title = constants.APP_TITLE
|
|
89
89
|
if self.io_gui_handler is not None:
|
|
90
|
-
path = self.io_gui_handler.current_file_path
|
|
90
|
+
path = self.io_gui_handler.current_file_path()
|
|
91
91
|
if path != '':
|
|
92
92
|
title += f" - {path.split('/')[-1]}"
|
|
93
93
|
if self.modified:
|
|
@@ -96,6 +96,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
|
|
|
96
96
|
|
|
97
97
|
def mark_as_modified(self):
|
|
98
98
|
self.modified = True
|
|
99
|
+
self.save_actions_set_enabled(True)
|
|
99
100
|
self.update_title()
|
|
100
101
|
|
|
101
102
|
def change_layer(self, layer_idx):
|
|
@@ -114,19 +115,14 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
|
|
|
114
115
|
new_idx = max(0, self.current_layer_idx() - 1)
|
|
115
116
|
if new_idx != self.current_layer_idx():
|
|
116
117
|
self.change_layer(new_idx)
|
|
117
|
-
self.highlight_thumbnail(new_idx)
|
|
118
|
+
self.display_manager.highlight_thumbnail(new_idx)
|
|
118
119
|
|
|
119
120
|
def next_layer(self):
|
|
120
121
|
if self.layer_stack() is not None:
|
|
121
122
|
new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
|
|
122
123
|
if new_idx != self.current_layer_idx():
|
|
123
124
|
self.change_layer(new_idx)
|
|
124
|
-
self.highlight_thumbnail(new_idx)
|
|
125
|
-
|
|
126
|
-
def highlight_thumbnail(self, index):
|
|
127
|
-
self.thumbnail_list.setCurrentRow(index)
|
|
128
|
-
self.thumbnail_list.scrollToItem(
|
|
129
|
-
self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
125
|
+
self.display_manager.highlight_thumbnail(new_idx)
|
|
130
126
|
|
|
131
127
|
def copy_layer_to_master(self):
|
|
132
128
|
if self.layer_stack() is None or self.master_layer() is None:
|
|
@@ -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,16 +126,19 @@ 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.
|
|
140
|
+
self.master_thumbnail_label.setFixedWidth(
|
|
141
|
+
gui_constants.UI_SIZES['thumbnail_width'])
|
|
138
142
|
self.master_thumbnail_label.mousePressEvent = \
|
|
139
143
|
lambda e: self.display_manager.set_view_master()
|
|
140
144
|
master_thumbnail_layout.addWidget(self.master_thumbnail_label)
|
|
@@ -152,7 +156,7 @@ class ImageEditorUI(ImageFilters):
|
|
|
152
156
|
}
|
|
153
157
|
""")
|
|
154
158
|
layers_label.setAlignment(Qt.AlignCenter)
|
|
155
|
-
layers_label.setFixedHeight(gui_constants.
|
|
159
|
+
layers_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
|
|
156
160
|
side_layout.addWidget(layers_label)
|
|
157
161
|
self.thumbnail_list = QListWidget()
|
|
158
162
|
self.thumbnail_list.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -204,18 +208,29 @@ class ImageEditorUI(ImageFilters):
|
|
|
204
208
|
layout.setSpacing(2)
|
|
205
209
|
super().setup_ui()
|
|
206
210
|
|
|
211
|
+
def highlight_master_thumbnail(self):
|
|
212
|
+
self.master_thumbnail_frame.setStyleSheet(
|
|
213
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
214
|
+
|
|
207
215
|
def setup_menu(self):
|
|
208
216
|
menubar = self.menuBar()
|
|
209
217
|
file_menu = menubar.addMenu("&File")
|
|
210
218
|
file_menu.addAction("&Open...", self.io_gui_handler.open_file, "Ctrl+O")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
self.
|
|
214
|
-
self.
|
|
215
|
-
self.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
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")
|
|
219
234
|
file_menu.addSeparator()
|
|
220
235
|
file_menu.addAction("&Import frames", self.io_gui_handler.import_frames)
|
|
221
236
|
file_menu.addAction("Import &EXIF data", self.io_gui_handler.select_exif_path)
|
|
@@ -271,12 +286,12 @@ class ImageEditorUI(ImageFilters):
|
|
|
271
286
|
|
|
272
287
|
view_master_action = QAction("View Master", self)
|
|
273
288
|
view_master_action.setShortcut("M")
|
|
274
|
-
view_master_action.triggered.connect(self.
|
|
289
|
+
view_master_action.triggered.connect(self.set_view_master)
|
|
275
290
|
view_menu.addAction(view_master_action)
|
|
276
291
|
|
|
277
292
|
view_individual_action = QAction("View Individual", self)
|
|
278
293
|
view_individual_action.setShortcut("L")
|
|
279
|
-
view_individual_action.triggered.connect(self.
|
|
294
|
+
view_individual_action.triggered.connect(self.set_view_individual)
|
|
280
295
|
view_menu.addAction(view_individual_action)
|
|
281
296
|
view_menu.addSeparator()
|
|
282
297
|
|
|
@@ -334,6 +349,25 @@ class ImageEditorUI(ImageFilters):
|
|
|
334
349
|
shortcuts_help_action.triggered.connect(self.shortcuts_help)
|
|
335
350
|
help_menu.addAction(shortcuts_help_action)
|
|
336
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
|
+
|
|
337
371
|
def shortcuts_help(self):
|
|
338
372
|
self._dialog = ShortcutsHelp(self)
|
|
339
373
|
self._dialog.exec()
|
|
@@ -365,8 +399,12 @@ class ImageEditorUI(ImageFilters):
|
|
|
365
399
|
def handle_temp_view(self, start):
|
|
366
400
|
if start:
|
|
367
401
|
self.display_manager.start_temp_view()
|
|
402
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
|
|
403
|
+
self.highlight_master_thumbnail()
|
|
368
404
|
else:
|
|
369
405
|
self.display_manager.end_temp_view()
|
|
406
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
|
|
407
|
+
self.highlight_master_thumbnail()
|
|
370
408
|
|
|
371
409
|
def handle_brush_size_change(self, delta):
|
|
372
410
|
if delta > 0:
|
|
@@ -7,7 +7,7 @@ from PySide6.QtGui import QGuiApplication, QCursor
|
|
|
7
7
|
from PySide6.QtCore import Qt, QObject, QTimer, Signal
|
|
8
8
|
from .file_loader import FileLoader
|
|
9
9
|
from .exif_data import ExifData
|
|
10
|
-
from .io_manager import IOManager
|
|
10
|
+
from .io_manager import IOManager, FileMultilayerSaver
|
|
11
11
|
from .layer_collection import LayerCollectionHandler
|
|
12
12
|
|
|
13
13
|
|
|
@@ -28,7 +28,15 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
28
28
|
self.loading_dialog = None
|
|
29
29
|
self.loading_timer = None
|
|
30
30
|
self.exif_dialog = None
|
|
31
|
-
self.
|
|
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
|
|
32
40
|
|
|
33
41
|
def setup_ui(self, display_manager, image_viewer):
|
|
34
42
|
self.display_manager = display_manager
|
|
@@ -44,16 +52,18 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
44
52
|
else:
|
|
45
53
|
self.set_layer_labels(labels)
|
|
46
54
|
self.set_master_layer(master_layer)
|
|
47
|
-
self.parent().modified = False
|
|
48
55
|
self.undo_manager.reset()
|
|
49
56
|
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
50
57
|
self.display_manager.update_thumbnails()
|
|
51
58
|
self.image_viewer.setup_brush_cursor()
|
|
52
|
-
self.parent().change_layer(0)
|
|
53
59
|
self.image_viewer.reset_zoom()
|
|
54
|
-
self.status_message_requested.emit(f"Loaded: {self.current_file_path}")
|
|
55
|
-
self.parent().thumbnail_list.setFocus()
|
|
60
|
+
self.status_message_requested.emit(f"Loaded: {self.current_file_path()}")
|
|
56
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()
|
|
57
67
|
|
|
58
68
|
def on_file_error(self, error_msg):
|
|
59
69
|
QApplication.restoreOverrideCursor()
|
|
@@ -61,7 +71,23 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
61
71
|
self.loading_dialog.accept()
|
|
62
72
|
self.loading_dialog.deleteLater()
|
|
63
73
|
QMessageBox.critical(self.parent(), "Error", error_msg)
|
|
64
|
-
self.status_message_requested.emit(f"Error loading: {self.current_file_path}")
|
|
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}")
|
|
65
91
|
|
|
66
92
|
def open_file(self, file_paths=None):
|
|
67
93
|
if file_paths is None:
|
|
@@ -77,7 +103,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
77
103
|
self.import_frames_from_files(file_paths)
|
|
78
104
|
return
|
|
79
105
|
path = file_paths[0] if isinstance(file_paths, list) else file_paths
|
|
80
|
-
self.
|
|
106
|
+
self.current_file_path_master = os.path.abspath(path)
|
|
107
|
+
self.current_file_path_multi = os.path.abspath(path)
|
|
81
108
|
QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
|
|
82
109
|
self.loading_dialog = QDialog(self.parent())
|
|
83
110
|
self.loading_dialog.setWindowTitle("Loading")
|
|
@@ -129,13 +156,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
129
156
|
self.display_manager.update_thumbnails()
|
|
130
157
|
|
|
131
158
|
def save_file(self):
|
|
132
|
-
if self.
|
|
159
|
+
if self.save_master_only.isChecked():
|
|
133
160
|
self.save_master()
|
|
134
161
|
else:
|
|
135
162
|
self.save_multilayer()
|
|
136
163
|
|
|
137
164
|
def save_file_as(self):
|
|
138
|
-
if self.
|
|
165
|
+
if self.save_master_only.isChecked():
|
|
139
166
|
self.save_master_as()
|
|
140
167
|
else:
|
|
141
168
|
self.save_multilayer_as()
|
|
@@ -143,11 +170,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
143
170
|
def save_multilayer(self):
|
|
144
171
|
if self.layer_stack() is None:
|
|
145
172
|
return
|
|
146
|
-
if self.
|
|
147
|
-
extension = self.
|
|
173
|
+
if self.current_file_path_multi != '':
|
|
174
|
+
extension = self.current_file_path_multi.split('.')[-1]
|
|
148
175
|
if extension in ['tif', 'tiff']:
|
|
149
|
-
self.save_multilayer_to_path(self.
|
|
176
|
+
self.save_multilayer_to_path(self.current_file_path_multi)
|
|
150
177
|
return
|
|
178
|
+
else:
|
|
179
|
+
self.save_multilayer_as()
|
|
151
180
|
|
|
152
181
|
def save_multilayer_as(self):
|
|
153
182
|
if self.layer_stack() is None:
|
|
@@ -161,11 +190,30 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
161
190
|
|
|
162
191
|
def save_multilayer_to_path(self, path):
|
|
163
192
|
try:
|
|
164
|
-
self.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
|
|
169
217
|
except Exception as e:
|
|
170
218
|
traceback.print_tb(e.__traceback__)
|
|
171
219
|
QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
|
|
@@ -173,8 +221,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
173
221
|
def save_master(self):
|
|
174
222
|
if self.master_layer() is None:
|
|
175
223
|
return
|
|
176
|
-
if self.
|
|
177
|
-
self.save_master_to_path(self.
|
|
224
|
+
if self.current_file_path_master != '':
|
|
225
|
+
self.save_master_to_path(self.current_file_path_master)
|
|
178
226
|
return
|
|
179
227
|
self.save_master_as()
|
|
180
228
|
|
|
@@ -190,7 +238,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
190
238
|
def save_master_to_path(self, path):
|
|
191
239
|
try:
|
|
192
240
|
self.io_manager.save_master(path)
|
|
193
|
-
self.
|
|
241
|
+
self.current_file_path_master = os.path.abspath(path)
|
|
194
242
|
self.parent().modified = False
|
|
195
243
|
self.update_title_requested.emit()
|
|
196
244
|
self.status_message_requested.emit(f"Saved master layer to: {path}")
|
|
@@ -211,7 +259,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
211
259
|
self.set_master_layer(None)
|
|
212
260
|
self.blank_layer = None
|
|
213
261
|
self.layer_collection.reset()
|
|
214
|
-
self.
|
|
262
|
+
self.current_file_path_master = ''
|
|
263
|
+
self.current_file_path_multi = ''
|
|
215
264
|
self.parent().modified = False
|
|
216
265
|
self.undo_manager.reset()
|
|
217
266
|
self.image_viewer.clear_image()
|
|
@@ -1,11 +1,33 @@
|
|
|
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)
|
|
@@ -38,12 +60,6 @@ class IOManager(LayerCollectionHandler):
|
|
|
38
60
|
raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
|
|
39
61
|
return stack, labels, master
|
|
40
62
|
|
|
41
|
-
def save_multilayer(self, path):
|
|
42
|
-
master_layer = {'Master': self.master_layer()}
|
|
43
|
-
individual_layers = dict(zip(self.layer_labels(), self.layer_stack()))
|
|
44
|
-
write_multilayer_tiff_from_images({**master_layer, **individual_layers},
|
|
45
|
-
path, exif_path=self.exif_path)
|
|
46
|
-
|
|
47
63
|
def save_master(self, path):
|
|
48
64
|
img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
|
|
49
65
|
write_image_with_exif_data(self.exif_data, img, path)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=LyVsN6QRbZCjxbel-HtG6unyJHf29KfzURL0WnqwB_I,21
|
|
3
3
|
shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
|
|
4
4
|
shinestacker/algorithms/align.py,sha256=FKGcDrp20jubY-LWA1OBqO-V781AoP8jCLH5xZm9nhk,17902
|
|
5
5
|
shinestacker/algorithms/balance.py,sha256=y68L5h8yuIuGZI3g5Zhj-jLXXOPsxHVGEhNTbCc2tlI,16518
|
|
@@ -17,7 +17,7 @@ shinestacker/algorithms/utils.py,sha256=VLm6eZmcAk2QPvomT4d1q56laJSYfbCQmiwI2Rmu
|
|
|
17
17
|
shinestacker/algorithms/vignetting.py,sha256=wFwi20ob1O3Memav1XQrtrOHgOtKRiK1RV4E-ex69r8,7470
|
|
18
18
|
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
19
19
|
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
shinestacker/app/about_dialog.py,sha256=
|
|
20
|
+
shinestacker/app/about_dialog.py,sha256=Y_gt28U3OPkKSkDdlA4kN1GCM6zkB-QJ5-c2qdeyG1Y,3321
|
|
21
21
|
shinestacker/app/app_config.py,sha256=eTIRxp0t7Wic46jMTe_oY3kz7ktZbdM43C3bjshVDKg,494
|
|
22
22
|
shinestacker/app/gui_utils.py,sha256=ptbUKjv5atbx5vW912_j8BVmDZpovAqZDEC48d0R2vA,2331
|
|
23
23
|
shinestacker/app/help_menu.py,sha256=UOlabEY_EKV2Q1BoiU2JAM1udSSBAwXlL7d58bqxKe0,516
|
|
@@ -28,7 +28,7 @@ shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2
|
|
|
28
28
|
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
29
29
|
shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
|
|
30
30
|
shinestacker/config/constants.py,sha256=MeZ15b7xIYJoN6EeiuR_OKi4sP-7_E7OtzrETzIowZI,5976
|
|
31
|
-
shinestacker/config/gui_constants.py,sha256=
|
|
31
|
+
shinestacker/config/gui_constants.py,sha256=XnjA65a1anlFXLm1zMP01Z8nZRBWX0D35FVxKnW2pJg,2568
|
|
32
32
|
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
33
33
|
shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
|
|
34
34
|
shinestacker/core/core_utils.py,sha256=ulJhzen5McAb5n6wWNA_KB4U_PdTEr-H2TCQkVKUaOw,1421
|
|
@@ -37,12 +37,12 @@ shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7K
|
|
|
37
37
|
shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
|
|
38
38
|
shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
39
|
shinestacker/gui/action_config.py,sha256=zWzTVkySEYfODJ620wQk6B2dr1C-YSxtDicJ0XXrU_M,48955
|
|
40
|
-
shinestacker/gui/actions_window.py,sha256
|
|
40
|
+
shinestacker/gui/actions_window.py,sha256=wJ_s_acXlftwOgnttT5TWVr3dq451TLL-98dmmPYAJ4,12326
|
|
41
41
|
shinestacker/gui/colors.py,sha256=zgLRcC3fAzklx7zzyjLEsMX2i64YTxGUmQM2woYBZuw,1344
|
|
42
42
|
shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
|
|
43
43
|
shinestacker/gui/gui_logging.py,sha256=ciuwZU-_5TicPpjC83iZmcwuDWiBO17onYJRGyF0FaY,8227
|
|
44
44
|
shinestacker/gui/gui_run.py,sha256=n0OaPZn9C4SVpopMNKUpksMwV27xT0TGFn1inhmCFIk,16230
|
|
45
|
-
shinestacker/gui/main_window.py,sha256=
|
|
45
|
+
shinestacker/gui/main_window.py,sha256=0CDi_uHHGiz1_htLNZ_iApywEaEYkNXnTcGfvOcgk_s,28817
|
|
46
46
|
shinestacker/gui/new_project.py,sha256=GSqYfonv-jdOEb4veXU6LxiDsAevr1TulzT0vsVazAk,8342
|
|
47
47
|
shinestacker/gui/project_converter.py,sha256=zZfXZg2h-PHh2Prr450B1UFADbZPzMBVkYhcpZkqPuk,7370
|
|
48
48
|
shinestacker/gui/project_editor.py,sha256=zwmj7PFs7X06GY4tkoDBcOL4Tl0IGo4Mf13n2qGwaJY,22245
|
|
@@ -61,27 +61,27 @@ shinestacker/retouch/base_filter.py,sha256=74GmzLjpPtn0Um0YOS8qLC1ZvhbvQX4L_AEDk
|
|
|
61
61
|
shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
|
|
62
62
|
shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
|
|
63
63
|
shinestacker/retouch/brush_preview.py,sha256=KlUOqA1uvLZRsz2peJ9NgsukyzsppJUw3XXr0NFCuhQ,5181
|
|
64
|
-
shinestacker/retouch/brush_tool.py,sha256=
|
|
64
|
+
shinestacker/retouch/brush_tool.py,sha256=m3qxyGipJth-lhB8syGSKmBPtXTQaIyaeJnpuCY3mSA,8694
|
|
65
65
|
shinestacker/retouch/denoise_filter.py,sha256=eO0Cxo9xwsuiE6-JiWCFB5jf6U1kf2N3ftsDAEQ5sek,1982
|
|
66
|
-
shinestacker/retouch/display_manager.py,sha256=
|
|
66
|
+
shinestacker/retouch/display_manager.py,sha256=XPbOBmoYc_jNA791WkWkOSaFHb0ztCZechl2p2KSlwQ,9597
|
|
67
67
|
shinestacker/retouch/exif_data.py,sha256=uA9ck9skp8ztSUdX1SFrApgtqmxrHtfWW3vsry82H94,2026
|
|
68
68
|
shinestacker/retouch/file_loader.py,sha256=723A_2w3cjn4rhvAzCq-__SWFelDRsMhkazgnb2h7Ig,4810
|
|
69
69
|
shinestacker/retouch/filter_manager.py,sha256=SkioWTr6iFFpugUgZLg0a3m5b9EHdZAeyNFy39qk0z8,453
|
|
70
70
|
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
71
|
-
shinestacker/retouch/image_editor.py,sha256=
|
|
72
|
-
shinestacker/retouch/image_editor_ui.py,sha256=
|
|
71
|
+
shinestacker/retouch/image_editor.py,sha256=ob2h-g9yIlFBMyEkVAkRRqiDpwykaUAMjpzAxKAtCRE,8336
|
|
72
|
+
shinestacker/retouch/image_editor_ui.py,sha256=fjHAs7x_4jbL5WVBVEzyW3TQMcrITU3ZmglxAwen51o,17520
|
|
73
73
|
shinestacker/retouch/image_filters.py,sha256=JF2a7VATO3CGQr5_OOIPi2k7b9HvHzrhhWS73x32t-A,2883
|
|
74
74
|
shinestacker/retouch/image_viewer.py,sha256=4kovjI8s5MWWA98oqNiQfe4xZvNRL_-UwnmSVK9r-gE,18639
|
|
75
|
-
shinestacker/retouch/io_gui_handler.py,sha256=
|
|
76
|
-
shinestacker/retouch/io_manager.py,sha256=
|
|
75
|
+
shinestacker/retouch/io_gui_handler.py,sha256=iEnyZ0Q1VJCXM5xf0rBFhBn4hFmGxOlIYjH8u1zDnT4,11348
|
|
76
|
+
shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
|
|
77
77
|
shinestacker/retouch/layer_collection.py,sha256=cvAW6nbG-KdhbN6XI4SrhGwvqTYGPyrZBLWz-uZkIJ0,5672
|
|
78
78
|
shinestacker/retouch/shortcuts_help.py,sha256=dlt7OSAr9thYuoEPlirTU_YRzv5xP9vy2-9mZO7GVAA,3308
|
|
79
79
|
shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
|
|
80
80
|
shinestacker/retouch/unsharp_mask_filter.py,sha256=hNJlqXYjf9Nd8KlVy09fd4TxrHa9Ofef0ZLSMHjLL6I,3481
|
|
81
81
|
shinestacker/retouch/white_balance_filter.py,sha256=2krwdz0X6qLWuCIEQcPtSQA_txfAsl7QUzfdsOLBrBU,4878
|
|
82
|
-
shinestacker-0.
|
|
83
|
-
shinestacker-0.
|
|
84
|
-
shinestacker-0.
|
|
85
|
-
shinestacker-0.
|
|
86
|
-
shinestacker-0.
|
|
87
|
-
shinestacker-0.
|
|
82
|
+
shinestacker-0.5.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
83
|
+
shinestacker-0.5.0.dist-info/METADATA,sha256=m4TX-tMSojm4PPLWN_MuOixF0daNqkVC1_igTn-tjDA,5202
|
|
84
|
+
shinestacker-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
85
|
+
shinestacker-0.5.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
86
|
+
shinestacker-0.5.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
87
|
+
shinestacker-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|