shinestacker 1.6.0__py3-none-any.whl → 1.6.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 CHANGED
@@ -1 +1 @@
1
- __version__ = '1.6.0'
1
+ __version__ = '1.6.1'
shinestacker/app/main.py CHANGED
@@ -13,6 +13,7 @@ from PySide6.QtCore import Qt, QEvent, QTimer, Signal
13
13
  from shinestacker.config.config import config
14
14
  config.init(DISABLE_TQDM=True, COMBINED_APP=True, DONT_USE_NATIVE_MENU=True)
15
15
  from shinestacker.config.constants import constants
16
+ from shinestacker.config.settings import StdPathFile
16
17
  from shinestacker.core.logging import setup_logging
17
18
  from shinestacker.gui.main_window import MainWindow
18
19
  from shinestacker.retouch.image_editor_ui import ImageEditorUI
@@ -229,7 +230,8 @@ open retouch window at startup instead of project windows.
229
230
  if filename and path:
230
231
  print("can't specify both arguments --filename and --path", file=sys.stderr)
231
232
  sys.exit(1)
232
- setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
233
+ setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
234
+ log_file=StdPathFile('shinestacker.log').get_file_path())
233
235
  app = Application(sys.argv)
234
236
  if config.DONT_USE_NATIVE_MENU:
235
237
  app.setAttribute(Qt.AA_DontUseNativeMenuBar)
@@ -12,6 +12,7 @@ from PySide6.QtCore import Qt, QTimer, QEvent
12
12
  from shinestacker.config.config import config
13
13
  config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
14
14
  from shinestacker.config.constants import constants
15
+ from shinestacker.config.settings import StdPathFile
15
16
  from shinestacker.core.logging import setup_logging
16
17
  from shinestacker.gui.main_window import MainWindow
17
18
  from shinestacker.app.gui_utils import (
@@ -57,7 +58,8 @@ project filename.
57
58
  ''')
58
59
  add_project_arguments(parser)
59
60
  args = vars(parser.parse_args(sys.argv[1:]))
60
- setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
61
+ setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
62
+ log_file=StdPathFile('shinestacker.log').get_file_path())
61
63
  app = Application(sys.argv)
62
64
  if config.DONT_USE_NATIVE_MENU:
63
65
  app.setAttribute(Qt.AA_DontUseNativeMenuBar)
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
2
2
  import os
3
3
  import sys
4
+ import logging
4
5
  import argparse
5
6
  from PySide6.QtWidgets import QApplication, QMenu
6
7
  from PySide6.QtGui import QIcon
@@ -8,6 +9,8 @@ from PySide6.QtCore import Qt, QEvent
8
9
  from shinestacker.config.config import config
9
10
  config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
10
11
  from shinestacker.config.constants import constants
12
+ from shinestacker.config.settings import StdPathFile
13
+ from shinestacker.core.logging import setup_logging
11
14
  from shinestacker.retouch.image_editor_ui import ImageEditorUI
12
15
  from shinestacker.app.gui_utils import (
13
16
  disable_macos_special_menu_items, fill_app_menu, set_css_style)
@@ -54,6 +57,8 @@ Multiple files can be specified separated by ';'.
54
57
  if filename and path:
55
58
  print("can't specify both arguments --filename and --path", file=sys.stderr)
56
59
  sys.exit(1)
60
+ setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
61
+ log_file=StdPathFile('shinestacker.log').get_file_path())
57
62
  app = Application(sys.argv)
58
63
  if config.DONT_USE_NATIVE_MENU:
59
64
  app.setAttribute(Qt.AA_DontUseNativeMenuBar)
@@ -26,19 +26,10 @@ def make_tqdm_bar(name, size, ncols=80):
26
26
 
27
27
 
28
28
  def get_app_base_path():
29
- sep = '\\' if (platform.system() == 'Windows') else '/'
30
29
  if getattr(sys, 'frozen', False):
31
- path = os.path.dirname(os.path.realpath(sys.executable))
32
- dirs = path.split(sep)
33
- last = -1
34
- for i in range(len(dirs) - 1, -1, -1):
35
- if dirs[i] == 'shinestacker':
36
- last = i
37
- break
38
- path = sep.join(dirs if last == 1 else dirs[:last + 1])
39
- elif __file__:
40
- path = sep.join(os.path.dirname(os.path.abspath(__file__)).split(sep)[:-3])
41
- return path
30
+ return os.path.dirname(os.path.abspath(sys.executable))
31
+ else:
32
+ return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
42
33
 
43
34
 
44
35
  def running_under_windows() -> bool:
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116
2
+ import os
2
3
  import logging
3
4
  import sys
4
5
  from pathlib import Path
@@ -64,8 +65,8 @@ def setup_logging(console_level=logging.INFO, file_level=logging.DEBUG, log_file
64
65
  if log_file == '':
65
66
  today = datetime.date.today().strftime("%Y-%m-%d")
66
67
  log_file = f"logs/{constants.APP_STRING.lower()}-{today}.log"
67
- if log_file[0] != '/':
68
- log_file = f'{get_app_base_path()}/{log_file}'
68
+ if not os.path.isabs(log_file):
69
+ log_file = os.path.join(get_app_base_path(), {log_file})
69
70
  Path(log_file).parent.mkdir(parents=True, exist_ok=True)
70
71
  file_handler = logging.FileHandler(log_file)
71
72
  file_handler.setLevel(file_level)
@@ -157,7 +157,7 @@ class BaseFilter(QObject, LayerCollectionHandler):
157
157
  except Exception:
158
158
  h, w = self.master_layer_copy().shape[:2]
159
159
  try:
160
- self.undo_manager.extend_undo_area(0, 0, w, h)
160
+ self.undo_manager.set_paint_area(0, 0, w, h)
161
161
  self.undo_manager.save_undo_state(
162
162
  self.master_layer_copy(),
163
163
  self.name
@@ -215,7 +215,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
215
215
  def refresh_master_view(self):
216
216
  if self.has_no_master_layer():
217
217
  return
218
- self.image_viewer.update_master_display()
218
+ self.image_viewer.update_master_display_area()
219
219
  self.update_master_thumbnail()
220
220
 
221
221
  def refresh_current_view(self):
@@ -229,7 +229,6 @@ class DisplayManager(QObject, LayerCollectionHandler):
229
229
  self.status_message_requested.emit("Temporary view: Individual layer.")
230
230
  else:
231
231
  self._master_refresh_and_thumb()
232
- self.image_viewer.strategy.brush_preview.hide()
233
232
  self.status_message_requested.emit("Temporary view: Master.")
234
233
 
235
234
  def end_temp_view(self):
@@ -14,6 +14,7 @@ from .shortcuts_help import ShortcutsHelp
14
14
  from .brush import Brush
15
15
  from .brush_tool import BrushTool
16
16
  from .layer_collection import LayerCollectionHandler
17
+ from .paint_area_manager import PaintAreaManager
17
18
  from .undo_manager import UndoManager
18
19
  from .layer_collection import LayerCollection
19
20
  from .io_gui_handler import IOGuiHandler
@@ -35,9 +36,9 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
35
36
  self.brush = Brush()
36
37
  self.brush_tool = BrushTool()
37
38
  self.modified = False
38
- self.mask_layer = None
39
39
  self.transformation_manager = TransfromationManager(self)
40
- self.undo_manager = UndoManager(self.transformation_manager)
40
+ self.paint_area_manager = PaintAreaManager()
41
+ self.undo_manager = UndoManager(self.transformation_manager, self.paint_area_manager)
41
42
  self.undo_action = None
42
43
  self.redo_action = None
43
44
  self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
@@ -49,13 +50,13 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
49
50
  central_widget = QWidget()
50
51
  self.setCentralWidget(central_widget)
51
52
  layout = QHBoxLayout(central_widget)
52
- self.image_viewer = ImageViewer(self.layer_collection)
53
+ self.image_viewer = ImageViewer(
54
+ self.layer_collection, self.brush_tool, self.paint_area_manager)
53
55
  self.image_viewer.connect_signals(
54
56
  self.handle_temp_view,
55
- self.begin_copy_brush_area,
56
- self.continue_copy_brush_area,
57
57
  self.end_copy_brush_area,
58
- self.handle_brush_size_change)
58
+ self.handle_brush_size_change,
59
+ self.handle_needs_update)
59
60
  side_panel = QWidget()
60
61
  side_layout = QVBoxLayout(side_panel)
61
62
  side_layout.setContentsMargins(0, 0, 0, 0)
@@ -629,35 +630,11 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
629
630
  self.mark_as_modified()
630
631
  self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
631
632
 
632
- def copy_brush_area_to_master(self, view_pos):
633
- if self.layer_stack() is None or self.number_of_layers() == 0 \
634
- or self.display_manager.view_mode != 'master':
635
- return
636
- area = self.brush_tool.apply_brush_operation(
637
- self.master_layer_copy(),
638
- self.current_layer(),
639
- self.master_layer(), self.mask_layer,
640
- view_pos)
641
- self.undo_manager.extend_undo_area(*area)
642
-
643
- def begin_copy_brush_area(self, pos):
644
- if self.display_manager.view_mode == 'master':
645
- self.mask_layer = self.io_gui_handler.blank_layer.copy()
646
- self.copy_master_layer()
647
- self.undo_manager.reset_undo_area()
648
- self.copy_brush_area_to_master(pos)
649
- self.display_manager.needs_update = True
650
- if not self.display_manager.update_timer.isActive():
651
- self.display_manager.update_timer.start()
652
- self.mark_as_modified()
653
-
654
- def continue_copy_brush_area(self, pos):
655
- if self.display_manager.view_mode == 'master':
656
- self.copy_brush_area_to_master(pos)
657
- self.display_manager.needs_update = True
658
- if not self.display_manager.update_timer.isActive():
659
- self.display_manager.update_timer.start()
660
- self.mark_as_modified()
633
+ def handle_needs_update(self):
634
+ self.display_manager.needs_update = True
635
+ if not self.display_manager.update_timer.isActive():
636
+ self.display_manager.update_timer.start()
637
+ self.mark_as_modified()
661
638
 
662
639
  def end_copy_brush_area(self):
663
640
  if self.display_manager.update_timer.isActive():
@@ -667,6 +644,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
667
644
  self.mark_as_modified()
668
645
 
669
646
  def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
647
+ self.image_viewer.update_brush_cursor()
670
648
  if self.undo_action:
671
649
  if has_undo:
672
650
  self.undo_action.setText(f"Undo {undo_desc}")
@@ -7,13 +7,16 @@ from .sidebyside_view import SideBySideView, TopBottomView
7
7
 
8
8
 
9
9
  class ImageViewer(QWidget):
10
- def __init__(self, layer_collection, parent=None):
10
+ def __init__(self, layer_collection, brush_tool, paint_area_manager, parent=None):
11
11
  super().__init__(parent)
12
12
  self.status = ImageViewStatus()
13
13
  self._strategies = {
14
- 'overlaid': OverlaidView(layer_collection, self.status, self),
15
- 'sidebyside': SideBySideView(layer_collection, self.status, self),
16
- 'topbottom': TopBottomView(layer_collection, self.status, self)
14
+ 'overlaid':
15
+ OverlaidView(layer_collection, self.status, brush_tool, paint_area_manager, self),
16
+ 'sidebyside':
17
+ SideBySideView(layer_collection, self.status, brush_tool, paint_area_manager, self),
18
+ 'topbottom':
19
+ TopBottomView(layer_collection, self.status, brush_tool, paint_area_manager, self)
17
20
  }
18
21
  for strategy in self._strategies.values():
19
22
  strategy.hide()
@@ -48,12 +51,18 @@ class ImageViewer(QWidget):
48
51
  def set_master_image_np(self, img):
49
52
  self.strategy.set_master_image_np(img)
50
53
 
54
+ def arrange_images(self):
55
+ self.strategy.arrange_images()
56
+
51
57
  def show_master(self):
52
58
  self.strategy.show_master()
53
59
 
54
60
  def show_current(self):
55
61
  self.strategy.show_current()
56
62
 
63
+ def update_master_display_area(self):
64
+ self.strategy.update_master_display_area()
65
+
57
66
  def update_master_display(self):
58
67
  self.strategy.update_master_display()
59
68
 
@@ -122,12 +131,11 @@ class ImageViewer(QWidget):
122
131
  st.set_cursor_style(style)
123
132
 
124
133
  def connect_signals(
125
- self, handle_temp_view, begin_copy_brush_area, continue_copy_brush_area,
126
- end_copy_brush_area, handle_brush_size_change):
134
+ self, handle_temp_view, end_copy_brush_area,
135
+ handle_brush_size_change, handle_needs_update):
127
136
  for st in self._strategies.values():
128
137
  st.temp_view_requested.connect(handle_temp_view)
129
- st.brush_operation_started.connect(begin_copy_brush_area)
130
- st.brush_operation_continued.connect(continue_copy_brush_area)
131
- st.brush_operation_ended.connect(end_copy_brush_area)
138
+ st.end_copy_brush_area_requested.connect(end_copy_brush_area)
132
139
  st.brush_size_change_requested.connect(handle_brush_size_change)
140
+ st.needs_update_requested.connect(handle_needs_update)
133
141
  st.setFocusPolicy(Qt.StrongFocus)
@@ -1,13 +1,16 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718, R0904, E1101
2
2
  import os
3
3
  import traceback
4
4
  import numpy as np
5
- from PySide6.QtWidgets import QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog, QApplication
5
+ import cv2
6
+ from PySide6.QtWidgets import (QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog,
7
+ QApplication, QProgressBar)
6
8
  from PySide6.QtGui import QGuiApplication, QCursor
7
9
  from PySide6.QtCore import Qt, QObject, QTimer, Signal
10
+ from .. algorithms.exif import get_exif, write_image_with_exif_data
8
11
  from .file_loader import FileLoader
9
12
  from .exif_data import ExifData
10
- from .io_manager import IOManager, FileMultilayerSaver
13
+ from .io_threads import FileMultilayerSaver, FrameImporter
11
14
  from .layer_collection import LayerCollectionHandler
12
15
 
13
16
 
@@ -22,13 +25,11 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
22
25
  def __init__(self, layer_collection, undo_manager, parent):
23
26
  QObject.__init__(self, parent)
24
27
  LayerCollectionHandler.__init__(self)
25
- self.io_manager = IOManager(layer_collection)
26
28
  self.undo_manager = undo_manager
27
29
  self.set_layer_collection(layer_collection)
28
30
  self.loader_thread = None
29
31
  self.display_manager = None
30
32
  self.image_viewer = None
31
- self.blank_layer = None
32
33
  self.loading_dialog = None
33
34
  self.loading_timer = None
34
35
  self.exif_dialog = None
@@ -37,6 +38,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
37
38
  self.saving_timer = None
38
39
  self.current_file_path_master = ''
39
40
  self.current_file_path_multi = ''
41
+ self.frame_importer_thread = None
42
+ self.frame_loading_dialog = None
43
+ self.frame_loading_timer = None
44
+ self.progress_label = None
45
+ self.progress_bar = None
46
+ self.exif_data = None
47
+ self.exif_path = ''
40
48
 
41
49
  def current_file_path(self):
42
50
  return self.current_file_path_master if self.save_master_only.isChecked() \
@@ -57,8 +65,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
57
65
  self.set_layer_labels(labels)
58
66
  self.set_master_layer(master_layer)
59
67
  self.image_viewer.set_master_image_np(master_layer)
68
+ self.set_blank_layer()
60
69
  self.undo_manager.reset()
61
- self.blank_layer = np.zeros(master_layer.shape[:2])
62
70
  self.finish_loading_setup(f"Loaded: {self.current_file_path()}")
63
71
  self.image_viewer.reset_zoom()
64
72
 
@@ -72,7 +80,40 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
72
80
  self.current_file_path_multi = ''
73
81
  self.status_message_requested.emit(f"Error loading: {self.current_file_path()}")
74
82
 
75
- def on_multilayer_save_success(self):
83
+ def on_frames_imported(self, stack, labels, master):
84
+ QApplication.restoreOverrideCursor()
85
+ self.frame_loading_timer.stop()
86
+ self.frame_loading_dialog.hide()
87
+ self.frame_loading_dialog.deleteLater()
88
+ empty_viewer = self.image_viewer.empty()
89
+ self.image_viewer.set_master_image_np(master)
90
+ if self.layer_stack() is None and len(stack) > 0:
91
+ self.set_layer_stack(np.array(stack))
92
+ if labels is None:
93
+ labels = self.layer_labels()
94
+ else:
95
+ self.set_layer_labels(labels)
96
+ self.set_master_layer(master)
97
+ self.set_blank_layer()
98
+ else:
99
+ if labels is None:
100
+ labels = self.layer_labels()
101
+ for img, label in zip(stack, labels):
102
+ self.add_layer_label(label)
103
+ self.add_layer(img)
104
+ self.finish_loading_setup("Selected frames imported")
105
+ if empty_viewer:
106
+ self.image_viewer.update_master_display()
107
+
108
+ def on_frames_import_error(self, error_msg):
109
+ QApplication.restoreOverrideCursor()
110
+ self.frame_loading_timer.stop()
111
+ self.frame_loading_dialog.hide()
112
+ self.frame_loading_dialog.deleteLater()
113
+ QMessageBox.critical(self.parent(), "Import Error", error_msg)
114
+ self.status_message_requested.emit("Error importing frames")
115
+
116
+ def on_multilayer_saved(self):
76
117
  QApplication.restoreOverrideCursor()
77
118
  self.saving_timer.stop()
78
119
  self.saving_dialog.hide()
@@ -90,6 +131,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
90
131
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {error_msg}")
91
132
 
92
133
  def open_file(self, file_paths=None):
134
+ self.cleanup_old_threads()
93
135
  if file_paths is None:
94
136
  file_paths, _ = QFileDialog.getOpenFileNames(
95
137
  self.parent(), "Open Image", "",
@@ -128,37 +170,37 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
128
170
  "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
129
171
  if file_paths:
130
172
  self.import_frames_from_files(file_paths)
131
- self.status_message_requested.emit("Imported selected frames")
132
173
 
133
174
  def import_frames_from_files(self, file_paths):
134
- empty_viewer = self.image_viewer.empty()
135
- try:
136
- stack, labels, master = self.io_manager.import_frames(file_paths)
137
- except Exception as e:
138
- msg = QMessageBox()
139
- msg.setIcon(QMessageBox.Critical)
140
- msg.setWindowTitle("Import error")
141
- msg.setText(str(e))
142
- msg.exec()
143
- return
144
- self.image_viewer.set_master_image_np(master)
145
- if self.layer_stack() is None and len(stack) > 0:
146
- self.set_layer_stack(np.array(stack))
147
- if labels is None:
148
- labels = self.layer_labels()
149
- else:
150
- self.set_layer_labels(labels)
151
- self.set_master_layer(master)
152
- self.blank_layer = np.zeros(master.shape[:2])
153
- else:
154
- if labels is None:
155
- labels = self.layer_labels()
156
- for img, label in zip(stack, labels):
157
- self.add_layer_label(label)
158
- self.add_layer(img)
159
- self.finish_loading_setup("Selected frames imported")
160
- if empty_viewer:
161
- self.image_viewer.reset_zoom()
175
+ self.cleanup_old_threads()
176
+ QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
177
+ self.frame_loading_dialog = QDialog(self.parent())
178
+ self.frame_loading_dialog.setWindowTitle("Loading Frames")
179
+ self.frame_loading_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
180
+ self.frame_loading_dialog.setModal(True)
181
+ layout = QVBoxLayout()
182
+ self.progress_label = QLabel("Frames loading...")
183
+ layout.addWidget(self.progress_label)
184
+ self.progress_bar = QProgressBar()
185
+ self.progress_bar.setRange(0, 100)
186
+ self.progress_bar.setValue(0)
187
+ layout.addWidget(self.progress_bar)
188
+ self.frame_loading_dialog.setLayout(layout)
189
+ self.frame_loading_timer = QTimer()
190
+ self.frame_loading_timer.setSingleShot(True)
191
+ self.frame_loading_timer.timeout.connect(self.frame_loading_dialog.show)
192
+ self.frame_loading_timer.start(100)
193
+ self.frame_importer_thread = FrameImporter(file_paths, self.master_layer())
194
+ self.frame_importer_thread.finished.connect(self.on_frames_imported)
195
+ self.frame_importer_thread.error.connect(self.on_frames_import_error)
196
+ self.frame_importer_thread.progress.connect(self.update_import_progress)
197
+ self.frame_importer_thread.start()
198
+
199
+ def update_import_progress(self, percent, filename):
200
+ if hasattr(self, 'progress_bar'):
201
+ self.progress_bar.setValue(percent)
202
+ if hasattr(self, 'progress_label'):
203
+ self.progress_label.setText(f"Loading: {filename} ({percent}%)")
162
204
 
163
205
  def finish_loading_setup(self, message):
164
206
  self.display_manager.update_thumbnails()
@@ -203,6 +245,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
203
245
  self.save_multilayer_to_path(path)
204
246
 
205
247
  def save_multilayer_to_path(self, path):
248
+ self.cleanup_old_threads()
206
249
  try:
207
250
  master_layer = {'Master': self.master_layer().copy()}
208
251
  individual_layers = dict(zip(
@@ -211,8 +254,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
211
254
  ))
212
255
  images_dict = {**master_layer, **individual_layers}
213
256
  self.saver_thread = FileMultilayerSaver(
214
- images_dict, path, exif_path=self.io_manager.exif_path)
215
- self.saver_thread.finished.connect(self.on_multilayer_save_success)
257
+ images_dict, path, exif_path=self.exif_path)
258
+ self.saver_thread.finished.connect(self.on_multilayer_saved)
216
259
  self.saver_thread.error.connect(self.on_multilayer_save_error)
217
260
  QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
218
261
  self.saving_dialog = QDialog(self.parent())
@@ -250,12 +293,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
250
293
 
251
294
  def save_master_to_path(self, path):
252
295
  try:
253
- self.io_manager.save_master(path)
296
+ img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
297
+ write_image_with_exif_data(self.exif_data, img, path)
254
298
  self.current_file_path_master = os.path.abspath(path)
255
- self.mark_as_modified_requested.emit(False)
299
+ # self.mark_as_modified_requested.emit(False)
256
300
  self.update_title_requested.emit()
257
- self.status_message_requested.emit(f"Saved master layer to: {path}")
258
301
  self.add_recent_file_requested.emit(self.current_file_path_master)
302
+ self.status_message_requested.emit(f"Saved master layer to: {path}")
259
303
  except Exception as e:
260
304
  traceback.print_tb(e.__traceback__)
261
305
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
@@ -263,14 +307,14 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
263
307
  def select_exif_path(self):
264
308
  path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
265
309
  if path:
266
- self.io_manager.set_exif_data(path)
310
+ self.exif_path = path
311
+ self.exif_data = get_exif(path)
267
312
  self.status_message_requested.emit(f"EXIF data extracted from {path}.")
268
- self.exif_dialog = ExifData(self.io_manager.exif_data, self.parent())
313
+ self.exif_dialog = ExifData(self.exif_data, self.parent())
269
314
  self.exif_dialog.exec()
270
315
 
271
316
  def close_file(self):
272
317
  self.mark_as_modified_requested.emit(False)
273
- self.blank_layer = None
274
318
  self.layer_collection.reset()
275
319
  self.current_file_path_master = ''
276
320
  self.current_file_path_multi = ''
@@ -281,3 +325,11 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
281
325
  self.update_title_requested.emit()
282
326
  self.set_enabled_file_open_close_actions_requested.emit(False)
283
327
  self.status_message_requested.emit("File closed")
328
+
329
+ def cleanup_old_threads(self):
330
+ if self.loader_thread and self.loader_thread.isFinished():
331
+ self.loader_thread = None
332
+ if self.frame_importer_thread and self.frame_importer_thread.isFinished():
333
+ self.frame_importer_thread = None
334
+ if self.saver_thread and self.saver_thread.isFinished():
335
+ self.saver_thread = None
@@ -0,0 +1,78 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, E1101, W0718, R0903, R0914
2
+
3
+ # import time
4
+ import os
5
+ import traceback
6
+ import cv2
7
+ from PySide6.QtCore import QThread, Signal
8
+ from .. algorithms.utils import read_img, validate_image, get_img_metadata
9
+ from .. algorithms.multilayer import write_multilayer_tiff_from_images
10
+
11
+
12
+ class FileMultilayerSaver(QThread):
13
+ finished = Signal()
14
+ error = Signal(str)
15
+
16
+ def __init__(self, images_dict, path, exif_path=None):
17
+ super().__init__()
18
+ self.images_dict = images_dict
19
+ self.path = path
20
+ self.exif_path = exif_path
21
+
22
+ def run(self):
23
+ try:
24
+ write_multilayer_tiff_from_images(
25
+ self.images_dict, self.path, exif_path=self.exif_path)
26
+ self.finished.emit()
27
+ except Exception as e:
28
+ traceback.print_tb(e.__traceback__)
29
+ self.error.emit(str(e))
30
+
31
+
32
+ class FrameImporter(QThread):
33
+ finished = Signal(object, object, object)
34
+ error = Signal(str)
35
+ progress = Signal(int, str)
36
+
37
+ def __init__(self, file_paths, master_layer):
38
+ super().__init__()
39
+ self.file_paths = file_paths
40
+ self.master_layer = master_layer
41
+
42
+ def run(self):
43
+ try:
44
+ stack = []
45
+ labels = []
46
+ master = None
47
+ current_master = self.master_layer
48
+ shape, dtype = None, None
49
+ if current_master is not None:
50
+ shape, dtype = get_img_metadata(current_master)
51
+ total_files = len(self.file_paths)
52
+ for i, path in enumerate(self.file_paths):
53
+ progress_percent = int((i / total_files) * 100)
54
+ self.progress.emit(progress_percent, os.path.basename(path))
55
+ try:
56
+ label = path.split("/")[-1].split(".")[0]
57
+ img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
58
+ if shape is not None and dtype is not None:
59
+ validate_image(img, shape, dtype)
60
+ else:
61
+ shape, dtype = get_img_metadata(img)
62
+ label_x = label
63
+ counter = 0
64
+ while label_x in labels:
65
+ counter += 1
66
+ label_x = f"{label} ({counter})"
67
+ labels.append(label_x)
68
+ stack.append(img)
69
+ if master is None:
70
+ master = img.copy()
71
+ # Add delay for testing
72
+ # time.sleep(0.2)
73
+ except Exception as e:
74
+ raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
75
+ self.progress.emit(100, "Complete")
76
+ self.finished.emit(stack, labels, master)
77
+ except Exception as e:
78
+ self.error.emit(str(e))
@@ -7,6 +7,7 @@ class LayerCollection:
7
7
  self.master_layer = None
8
8
  self.master_layer_copy = None
9
9
  self.layer_stack = None
10
+ self.blank_layer = None
10
11
  self.layer_labels = []
11
12
  self.current_layer_idx = 0
12
13
  self.sorted_indices = None
@@ -15,6 +16,7 @@ class LayerCollection:
15
16
  self.master_layer = None
16
17
  self.master_layer_copy = None
17
18
  self.layer_stack = None
19
+ self.blank_layer = None
18
20
  self.layer_labels = []
19
21
  self.current_layer_idx = 0
20
22
  self.sorted_indices = None
@@ -62,6 +64,10 @@ class LayerCollection:
62
64
  def set_master_layer(self, img):
63
65
  self.master_layer = img
64
66
 
67
+ def set_blank_layer(self):
68
+ if self.master_layer is not None:
69
+ self.blank_layer = np.zeros(self.master_layer.shape[:2])
70
+
65
71
  def restore_master_layer(self):
66
72
  self.master_layer = self.master_layer_copy.copy()
67
73
 
@@ -122,6 +128,9 @@ class LayerCollectionHandler:
122
128
  def current_layer(self):
123
129
  return self.layer_collection.current_layer()
124
130
 
131
+ def blank_layer(self):
132
+ return self.layer_collection.blank_layer
133
+
125
134
  def layer_stack(self):
126
135
  return self.layer_collection.layer_stack
127
136
 
@@ -152,6 +161,9 @@ class LayerCollectionHandler:
152
161
  def set_master_layer(self, img):
153
162
  self.layer_collection.set_master_layer(img)
154
163
 
164
+ def set_blank_layer(self):
165
+ self.layer_collection.set_blank_layer()
166
+
155
167
  def add_layer_label(self, label):
156
168
  self.layer_collection.add_layer_label(label)
157
169
 
@@ -1,11 +1,11 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, E1101, R0904, R0912, R0914, R0902, E0202
1
+ # pylint: disable=C0114, C0115, C0116, E0611, E1101, R0904, R0912, R0914, R0902, E0202, R0913, R0917
2
2
  from PySide6.QtCore import Qt, QPointF, QEvent, QRectF
3
3
  from .view_strategy import ViewStrategy, ImageGraphicsViewBase, ViewSignals
4
4
 
5
5
 
6
6
  class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
7
- def __init__(self, layer_collection, status, parent):
8
- ViewStrategy.__init__(self, layer_collection, status)
7
+ def __init__(self, layer_collection, status, brush_tool, paint_area_manager, parent):
8
+ ViewStrategy.__init__(self, layer_collection, status, brush_tool, paint_area_manager)
9
9
  ImageGraphicsViewBase.__init__(self, parent)
10
10
  self.scene = self.create_scene(self)
11
11
  self.create_pixmaps()
@@ -121,6 +121,7 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
121
121
  self.pixmap_item_master.setVisible(True)
122
122
  self.pixmap_item_current.setVisible(False)
123
123
  self.show_brush_preview()
124
+ self.enable_paint = True
124
125
  if self.brush_cursor:
125
126
  self.scene.removeItem(self.brush_cursor)
126
127
  self.brush_cursor = self.create_circle(self.scene)
@@ -130,21 +131,28 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
130
131
  self.pixmap_item_master.setVisible(False)
131
132
  self.pixmap_item_current.setVisible(True)
132
133
  self.hide_brush_preview()
134
+ self.enable_paint = False
133
135
  if self.brush_cursor:
134
136
  self.scene.removeItem(self.brush_cursor)
135
137
  self.brush_cursor = self.create_alt_circle(self.scene)
136
138
  self.update_brush_cursor()
137
139
 
140
+ def master_is_visible(self):
141
+ return self.pixmap_item_master.isVisible()
142
+
143
+ def current_is_visible(self):
144
+ return self.pixmap_item_current.isVisible()
145
+
138
146
  def arrange_images(self):
139
147
  if self.empty():
140
148
  return
141
- if self.pixmap_item_master.isVisible():
149
+ if self.master_is_visible():
142
150
  pixmap = self.pixmap_item_master.pixmap()
143
151
  if not pixmap.isNull():
144
152
  self.setSceneRect(QRectF(pixmap.rect()))
145
153
  self.centerOn(self.pixmap_item_master)
146
154
  self.center_image(self)
147
- elif self.pixmap_item_current.isVisible():
155
+ elif self.current_is_visible():
148
156
  pixmap = self.pixmap_item_current.pixmap()
149
157
  if not pixmap.isNull():
150
158
  self.setSceneRect(QRectF(pixmap.rect()))
@@ -0,0 +1,30 @@
1
+ # pylint: disable=C0114, C0115, C0116
2
+ from .. config.gui_constants import gui_constants
3
+
4
+
5
+ class PaintAreaManager:
6
+ def __init__(self):
7
+ self.x_start = None
8
+ self.y_start = None
9
+ self.x_end = None
10
+ self.y_end = None
11
+ self.reset()
12
+
13
+ def reset(self):
14
+ self.x_end = self.y_end = 0
15
+ self.x_start = self.y_start = gui_constants.MAX_UNDO_SIZE
16
+
17
+ def extend(self, x_start, y_start, x_end, y_end):
18
+ self.x_start = min(self.x_start, x_start)
19
+ self.y_start = min(self.y_start, y_start)
20
+ self.x_end = max(self.x_end, x_end)
21
+ self.y_end = max(self.y_end, y_end)
22
+
23
+ def area(self):
24
+ return self.x_start, self.y_start, self.x_end, self.y_end
25
+
26
+ def set_area(self, x_start, y_start, x_end, y_end):
27
+ self.x_start = x_start
28
+ self.y_start = y_start
29
+ self.x_end = x_end
30
+ self.y_end = y_end
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003
1
+ # pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003, R0913, R0917
2
2
  import time
3
3
  from PySide6.QtCore import Qt, Signal, QEvent, QRectF
4
4
  from PySide6.QtGui import QCursor
@@ -39,8 +39,8 @@ class ImageGraphicsView(ImageGraphicsViewBase):
39
39
 
40
40
 
41
41
  class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
42
- def __init__(self, layer_collection, status, parent):
43
- ViewStrategy.__init__(self, layer_collection, status)
42
+ def __init__(self, layer_collection, status, brush_tool, paint_area_manager, parent):
43
+ ViewStrategy.__init__(self, layer_collection, status, brush_tool, paint_area_manager)
44
44
  QWidget.__init__(self, parent)
45
45
  self.current_view = ImageGraphicsView(parent)
46
46
  self.master_view = ImageGraphicsView(parent)
@@ -16,8 +16,7 @@ class TransfromationManager(LayerCollectionHandler):
16
16
  if undoable:
17
17
  try:
18
18
  undo = self.editor.undo_manager
19
- undo.x_start, undo.x_stop = 0, 1
20
- undo.y_start, undo.y_stop = 0, 1
19
+ undo.set_paint_area(0, 1, 0, 1)
21
20
  undo.save_undo_state(self.editor.master_layer(), label)
22
21
  except Exception as e:
23
22
  traceback.print_tb(e.__traceback__)
@@ -6,13 +6,10 @@ from .. config.gui_constants import gui_constants
6
6
  class UndoManager(QObject):
7
7
  stack_changed = Signal(bool, str, bool, str)
8
8
 
9
- def __init__(self, transformation_manager):
9
+ def __init__(self, transformation_manager, paint_area_manager):
10
10
  super().__init__()
11
11
  self.transformation_manager = transformation_manager
12
- self.x_start = None
13
- self.y_start = None
14
- self.x_end = None
15
- self.y_end = None
12
+ self.paint_area_manager = paint_area_manager
16
13
  self.undo_stack = None
17
14
  self.redo_stack = None
18
15
  self.reset()
@@ -24,22 +21,25 @@ class UndoManager(QObject):
24
21
  self.stack_changed.emit(False, "", False, "")
25
22
 
26
23
  def reset_undo_area(self):
27
- self.x_end = self.y_end = 0
28
- self.x_start = self.y_start = gui_constants.MAX_UNDO_SIZE
24
+ self.paint_area_manager.reset()
29
25
 
30
26
  def extend_undo_area(self, x_start, y_start, x_end, y_end):
31
- self.x_start = min(self.x_start, x_start)
32
- self.y_start = min(self.y_start, y_start)
33
- self.x_end = max(self.x_end, x_end)
34
- self.y_end = max(self.y_end, y_end)
27
+ self.paint_area_manager.extend(x_start, y_start, x_end, y_end)
28
+
29
+ def paint_area(self):
30
+ return self.paint_area_manager.area()
31
+
32
+ def set_paint_area(self, x_start, y_start, x_end, y_end):
33
+ self.paint_area_manager.set_area(x_start, y_start, x_end, y_end)
35
34
 
36
35
  def save_undo_state(self, layer, description):
37
36
  if layer is None:
38
37
  return
39
38
  self.redo_stack = []
39
+ x_start, y_start, x_end, y_end = self.paint_area()
40
40
  undo_state = {
41
- 'master': layer[self.y_start:self.y_end, self.x_start:self.x_end].copy(),
42
- 'area': (self.x_start, self.y_start, self.x_end, self.y_end),
41
+ 'master': layer[y_start:y_end, x_start:x_end].copy(),
42
+ 'area': (x_start, y_start, x_end, y_end),
43
43
  'description': description
44
44
  }
45
45
  if len(self.undo_stack) >= gui_constants.MAX_UNDO_SIZE:
@@ -54,6 +54,7 @@ class UndoManager(QObject):
54
54
  return False
55
55
  undo_state = self.undo_stack.pop()
56
56
  x_start, y_start, x_end, y_end = undo_state['area']
57
+ self.set_paint_area(x_start, y_start, x_end, y_end)
57
58
  redo_state = {
58
59
  'master': layer[y_start:y_end, x_start:x_end].copy(),
59
60
  'area': (x_start, y_start, x_end, y_end),
@@ -80,6 +81,7 @@ class UndoManager(QObject):
80
81
  return False
81
82
  redo_state = self.redo_stack.pop()
82
83
  x_start, y_start, x_end, y_end = redo_state['area']
84
+ self.set_paint_area(x_start, y_start, x_end, y_end)
83
85
  undo_state = {
84
86
  'master': layer[y_start:y_end, x_start:x_end].copy(),
85
87
  'area': (x_start, y_start, x_end, y_end),
@@ -79,10 +79,9 @@ class BrushCursor(QGraphicsItemGroup):
79
79
 
80
80
  class ViewSignals:
81
81
  temp_view_requested = Signal(bool)
82
- brush_operation_started = Signal(QPoint)
83
- brush_operation_continued = Signal(QPoint)
84
- brush_operation_ended = Signal()
82
+ end_copy_brush_area_requested = Signal()
85
83
  brush_size_change_requested = Signal(int) # +1 or -1
84
+ needs_update_requested = Signal()
86
85
 
87
86
 
88
87
  class ImageGraphicsViewBase(QGraphicsView):
@@ -103,9 +102,12 @@ class ImageGraphicsViewBase(QGraphicsView):
103
102
 
104
103
 
105
104
  class ViewStrategy(LayerCollectionHandler):
106
- def __init__(self, layer_collection, status):
105
+ def __init__(self, layer_collection, status, brush_tool, paint_area_manager):
107
106
  LayerCollectionHandler.__init__(self, layer_collection)
108
107
  self.status = status
108
+ self.brush_tool = brush_tool
109
+ self.paint_area_manager = paint_area_manager
110
+ self.mask_layer = None
109
111
  self.brush = None
110
112
  self.brush_cursor = None
111
113
  self.brush_preview = BrushPreviewItem(layer_collection)
@@ -123,6 +125,7 @@ class ViewStrategy(LayerCollectionHandler):
123
125
  self.last_update_time = QTime.currentTime()
124
126
  self.last_color_update_time = 0
125
127
  self.last_cursor_update_time = 0
128
+ self.enable_paint = True
126
129
 
127
130
  @abstractmethod
128
131
  def create_pixmaps(self):
@@ -274,6 +277,25 @@ class ViewStrategy(LayerCollectionHandler):
274
277
  view.verticalScrollBar().setValue(self.status.v_scroll)
275
278
  self.arrange_images()
276
279
 
280
+ def update_master_display_area(self):
281
+ if self.empty():
282
+ return
283
+ x_start, y_start, x_end, y_end = self.paint_area_manager.area()
284
+ dirty_region = self.master_layer()[y_start:y_end, x_start:x_end]
285
+ qimage = self.numpy_to_qimage(dirty_region)
286
+ if not qimage:
287
+ return
288
+ pixmap = QPixmap.fromImage(qimage)
289
+ master_pixmap_item = self.get_master_pixmap()
290
+ current_pixmap = master_pixmap_item.pixmap()
291
+ if current_pixmap.isNull():
292
+ self.update_master_display()
293
+ return
294
+ painter = QPainter(current_pixmap)
295
+ painter.drawPixmap(x_start, y_start, pixmap)
296
+ painter.end()
297
+ master_pixmap_item.setPixmap(current_pixmap)
298
+
277
299
  def update_master_display(self):
278
300
  self.update_view_display(
279
301
  self.master_layer(),
@@ -698,6 +720,42 @@ class ViewStrategy(LayerCollectionHandler):
698
720
  self.status.set_scroll(view.horizontalScrollBar().value(),
699
721
  view.verticalScrollBar().value())
700
722
 
723
+ def copy_brush_area_to_master(self, view_pos):
724
+ if self.layer_stack() is None or self.number_of_layers() == 0:
725
+ return
726
+ area = self.brush_tool.apply_brush_operation(
727
+ self.master_layer_copy(),
728
+ self.current_layer(),
729
+ self.master_layer(), self.mask_layer,
730
+ view_pos)
731
+ self.paint_area_manager.extend(*area)
732
+
733
+ def begin_copy_brush_area(self, pos):
734
+ self.mask_layer = self.blank_layer().copy()
735
+ self.copy_master_layer()
736
+ self.paint_area_manager.reset()
737
+ self.copy_brush_area_to_master(pos)
738
+ self.needs_update_requested.emit()
739
+
740
+ def continue_copy_brush_area(self, pos):
741
+ self.copy_brush_area_to_master(pos)
742
+ self.needs_update_requested.emit()
743
+
744
+ def mouse_press_event(self, event):
745
+ if self.empty():
746
+ return
747
+ if self.enable_paint and event.button() & Qt.LeftButton and self.has_master_layer():
748
+ if self.space_pressed:
749
+ self.scrolling = True
750
+ self.last_mouse_pos = event.position()
751
+ self.setCursor(Qt.ClosedHandCursor)
752
+ else:
753
+ self.last_brush_pos = event.position()
754
+ self.begin_copy_brush_area(event.position().toPoint())
755
+ self.dragging = True
756
+ if not self.scrolling:
757
+ self.show_brush_cursor()
758
+
701
759
  def mouse_move_event(self, event):
702
760
  if self.empty():
703
761
  return
@@ -710,7 +768,7 @@ class ViewStrategy(LayerCollectionHandler):
710
768
  brush_size = self.brush.size
711
769
  if not self.space_pressed:
712
770
  self.update_brush_cursor()
713
- if self.dragging and event.buttons() & Qt.LeftButton:
771
+ if self.enable_paint and self.dragging and event.buttons() & Qt.LeftButton:
714
772
  current_time = QTime.currentTime()
715
773
  paint_refresh_time = AppConfig.get('paint_refresh_time')
716
774
  if self.last_update_time.msecsTo(current_time) >= paint_refresh_time:
@@ -726,7 +784,7 @@ class ViewStrategy(LayerCollectionHandler):
726
784
  for i in range(0, n_steps + 1):
727
785
  pos = QPoint(self.last_brush_pos.x() + i * delta_x,
728
786
  self.last_brush_pos.y() + i * delta_y)
729
- self.brush_operation_continued.emit(pos)
787
+ self.continue_copy_brush_area(pos)
730
788
  self.last_brush_pos = position
731
789
  self.last_update_time = current_time
732
790
  if self.scrolling and event.buttons() & Qt.LeftButton:
@@ -738,21 +796,6 @@ class ViewStrategy(LayerCollectionHandler):
738
796
  self.last_mouse_pos = position
739
797
  self.scroll_view(master_view, delta.x(), delta.y())
740
798
 
741
- def mouse_press_event(self, event):
742
- if self.empty():
743
- return
744
- if event.button() == Qt.LeftButton and self.has_master_layer():
745
- if self.space_pressed:
746
- self.scrolling = True
747
- self.last_mouse_pos = event.position()
748
- self.setCursor(Qt.ClosedHandCursor)
749
- else:
750
- self.last_brush_pos = event.position()
751
- self.brush_operation_started.emit(event.position().toPoint())
752
- self.dragging = True
753
- if not self.scrolling:
754
- self.show_brush_cursor()
755
-
756
799
  def mouse_release_event(self, event):
757
800
  if self.empty():
758
801
  return
@@ -769,4 +812,4 @@ class ViewStrategy(LayerCollectionHandler):
769
812
  self.last_mouse_pos = None
770
813
  elif self.dragging:
771
814
  self.dragging = False
772
- self.brush_operation_ended.emit()
815
+ self.end_copy_brush_area_requested.emit()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.6.0
3
+ Version: 1.6.1
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -1,5 +1,5 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=aWnRnF5TUBHC_2RdGczq36VyxBiPAviva2RjtMVjCFg,21
2
+ shinestacker/_version.py,sha256=DH87PU9LcZeI_uvttoH-lyhGyb55UShgeFtsG2qIZgE,21
3
3
  shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
4
4
  shinestacker/algorithms/align.py,sha256=mb44u-YxZI1TTSHz81nRpX_2c8awlOhnGrK0LyfTQeQ,33543
5
5
  shinestacker/algorithms/align_auto.py,sha256=pJetw6zZEWQLouzcelkI8gD4cPiOp887ePXzVbm0E6Q,3800
@@ -25,10 +25,10 @@ shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9y
25
25
  shinestacker/app/args_parser_opts.py,sha256=c6IUXOI0SJIFckPWPXYnwmBmdNnOcrtvU5S8hUDU_AQ,979
26
26
  shinestacker/app/gui_utils.py,sha256=EGZejp0XZXRLVa_Wd_2VYAwK3oe9hMSoZzNgT7NUNRw,2986
27
27
  shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
28
- shinestacker/app/main.py,sha256=l9O9J7nICTi3wnW2uJpKfLV7HWDEi-r_xoOrbywJv_s,11129
28
+ shinestacker/app/main.py,sha256=OQwPJvhrsAThv9kmiqtX7CX-smfwXdFXagQmGsQdmuU,11258
29
29
  shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
30
- shinestacker/app/project.py,sha256=8hjKkZqBsumS7McHXRZdGvyHgopPHeHznrnIdXajp_w,2916
31
- shinestacker/app/retouch.py,sha256=pks7aoXmYyRoeTzOA-D2Uy5f4RKoEDh25OQtkcOE_9c,2778
30
+ shinestacker/app/project.py,sha256=jn4LlMTMxrJeAyQQrR5nVS4Di1rOrjiOJ8LOg0CZbwE,3045
31
+ shinestacker/app/retouch.py,sha256=fyk74fgpeoBqY1mRJiXWf1hRzSeG7xXFx4V-Sphr3Kg,3069
32
32
  shinestacker/app/settings_dialog.py,sha256=0P3nqqZEiTIFgidW1_e3Q_zE7NbAouNsuj-yNsU41vk,8192
33
33
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
34
34
  shinestacker/config/app_config.py,sha256=rM1Rndk1GDa5c0AhcVNEN9zSAzxPZixzQYfjODbJUwE,771
@@ -38,10 +38,10 @@ shinestacker/config/gui_constants.py,sha256=PNxzwmVEppJ2mV_vwp68NhWzJOEitVy1Pk9S
38
38
  shinestacker/config/settings.py,sha256=4p4r6wKOCbttzfH9tyHQSTd-iv-GfgCd1LxI3C7WIjU,3861
39
39
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
40
40
  shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
41
- shinestacker/core/core_utils.py,sha256=1LYj19Dfc9jZN9-4dlf1paximDH5WZYa7DXvKr7R7QY,1719
41
+ shinestacker/core/core_utils.py,sha256=wmpu9_idDRe866JFEylMvGPJRT8lSVhikK9PljcbPSI,1392
42
42
  shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk2c,1618
43
43
  shinestacker/core/framework.py,sha256=QaTfnzEUHwzlbyFG7KzeyteckTSWHWEEJE4d5Tc8H18,11015
44
- shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
44
+ shinestacker/core/logging.py,sha256=T-ZTGYjN2n2_5Vu2mBJ2id9qj1c9NA79OD2H2w-W0nM,3096
45
45
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  shinestacker/gui/action_config.py,sha256=Xv7SGbhPl1F_dUnU04VBt_E-wIItnN_q6QuhU_d9GfI,25929
47
47
  shinestacker/gui/action_config_dialog.py,sha256=QN95FiVPYL6uin2sYO5F7tq6G5rBWh9yRkeTVvwKrwU,38341
@@ -75,35 +75,36 @@ shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzd
75
75
  shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
76
76
  shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
77
77
  shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- shinestacker/retouch/base_filter.py,sha256=zpedVvTpof8BsENWdDeu9PDul7Uw0UAXUSsyLZjbcpg,11265
78
+ shinestacker/retouch/base_filter.py,sha256=ObrAcwZv9YJbIhWMHcryHEEj41oAk9hfHmE4phEd1gE,11263
79
79
  shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
80
80
  shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
81
81
  shinestacker/retouch/brush_preview.py,sha256=cOFVMCbEsgR_alzmr_-LLghtGU_unrE-hAjLHcvrZAY,5484
82
82
  shinestacker/retouch/brush_tool.py,sha256=8uVncTA375uC3Nhp2YM0eZjpOR-nN47i2eGjN8tJzOU,8714
83
83
  shinestacker/retouch/denoise_filter.py,sha256=UpNKbFs7uArdglEej8AUHan7oCVYV5E7HNzkovj7XMQ,571
84
- shinestacker/retouch/display_manager.py,sha256=iRPcXWb3h3OVJy-S4xny--SB-rUfU4Qc4YSVu3XstWE,10236
84
+ shinestacker/retouch/display_manager.py,sha256=fTZTGbvmX5DXagexuvbNgOF5GiH2Vv-stLUQQwoglp8,10181
85
85
  shinestacker/retouch/exif_data.py,sha256=LF-fRXW-reMq-xJ_QRE5j8DC2LVGKIlC6MR3QbC1cdg,1896
86
86
  shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
87
87
  shinestacker/retouch/filter_manager.py,sha256=tOGIWj5HjViL1-iXHkd91X-sZ1c1G531pDmLO0x6zx0,866
88
88
  shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
89
- shinestacker/retouch/image_editor_ui.py,sha256=vPfLuHdRZTGNbmX3ZXYoJ9Q2_dCDbFJHUxB41hrOD6k,34117
89
+ shinestacker/retouch/image_editor_ui.py,sha256=r2S4Fgyi3xQi2XeqlVYTUxZ9YlPZe2rMaIpvIak8Aog,33181
90
90
  shinestacker/retouch/image_view_status.py,sha256=2rWi2ugdyjMhWCtRJkwOnb7-tCtVfnGfCY_54qpZhwM,1970
91
- shinestacker/retouch/image_viewer.py,sha256=H8w-ORug1aKf7X3FeSX4lQV-a0IewZ9OVG1-50BK4cE,4452
92
- shinestacker/retouch/io_gui_handler.py,sha256=UOnrFT00s0075Ng_yJACJX9TP8UT9mQOWXwQwNAfpMw,12079
93
- shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
94
- shinestacker/retouch/layer_collection.py,sha256=fZlGrkm9-Ycc7AOzFSpImhafiTieBeCZRk-UlvlFHbo,5819
95
- shinestacker/retouch/overlaid_view.py,sha256=KBwuzC2OQsK7rdtFAKrSvXrHrEV1SiOoZhxPCQLGkvg,6734
91
+ shinestacker/retouch/image_viewer.py,sha256=xf1vYZRPb9ClCQbqrqAFhPubdqIIpku7DgcY8O5bvYU,4694
92
+ shinestacker/retouch/io_gui_handler.py,sha256=tyHMmR6_uE_IEI-CIcJibVhupxarKYHzX2fuhnesHaI,14616
93
+ shinestacker/retouch/io_threads.py,sha256=r0X4it2PfwnmiAU7eStniIfcHhPvuaqdqf5VlnvjZ-4,2832
94
+ shinestacker/retouch/layer_collection.py,sha256=xx8INSLCXIeTQn_nxfCo4QljAmQK1qukSYO1Zk4rqqo,6183
95
+ shinestacker/retouch/overlaid_view.py,sha256=QTTdegUWs99YBZZPlIRdPI5O80U3t_c3HnyegbRqNbA,7029
96
+ shinestacker/retouch/paint_area_manager.py,sha256=ilK6uQT7lzNyvdc8uNv4xTHHHAbk5hGEClJRNmiA4P8,894
96
97
  shinestacker/retouch/shortcuts_help.py,sha256=BFWTT5QvodqMhqa_9LI25hZqjICfckgyWG4fGrGzvnM,4283
97
- shinestacker/retouch/sidebyside_view.py,sha256=iwnIENV_YrR2fogXqKNdCcAQBROWkQxOnh8G4tU5bqA,18829
98
- shinestacker/retouch/transformation_manager.py,sha256=T2ewDsEKn1FBxapQ1kR7KqFwrF7spUu-KZ43bKpfBo8,1761
99
- shinestacker/retouch/undo_manager.py,sha256=_qTpwjGsznc-Sz-sfWEDv3IxJNmrYU0qi7yJelg58DU,4319
98
+ shinestacker/retouch/sidebyside_view.py,sha256=4sNa_IUMbNH18iECO7eDO9S_ls3v2ENwP1AWrBFhofI,18907
99
+ shinestacker/retouch/transformation_manager.py,sha256=QFYCL-l9V6qlhw3y7tcs0saWWClNPsh7F9pTBkfPbRU,1711
100
+ shinestacker/retouch/undo_manager.py,sha256=J4hEAnv9bKLQ0N1wllWswjJBhgRgasCnBoMT5LEw-dM,4453
100
101
  shinestacker/retouch/unsharp_mask_filter.py,sha256=Iapc8UmSVpj3V0LcJq_38P5qerRqTevMynbbk5Rk6iE,3634
101
- shinestacker/retouch/view_strategy.py,sha256=l_Fuh6vmTLv7S400XAP1fDO61Dv7U8GaOrNTvjKKjvc,28521
102
+ shinestacker/retouch/view_strategy.py,sha256=jZxB_vX3_0notH0ClxKkLzbdtx4is3vQiYoIP-sDv3M,30216
102
103
  shinestacker/retouch/vignetting_filter.py,sha256=JhFr6OVIripQzSJrZEG4lxq7wBsmpofLqJQ-aP2bKw8,3789
103
104
  shinestacker/retouch/white_balance_filter.py,sha256=UaH4yxG3fU4vPutBAkV5oTXIQyUTN09x0uTywAzv3sY,8286
104
- shinestacker-1.6.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
105
- shinestacker-1.6.0.dist-info/METADATA,sha256=0sCC4ubrLH2wabRFJaCx9_Kaj3K4HDIdM6vULEj2iJA,6978
106
- shinestacker-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
107
- shinestacker-1.6.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
108
- shinestacker-1.6.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
109
- shinestacker-1.6.0.dist-info/RECORD,,
105
+ shinestacker-1.6.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
106
+ shinestacker-1.6.1.dist-info/METADATA,sha256=9sevPHbXS-yaj-wYc-42q28V34PNOdnPlntqNzlsgOo,6978
107
+ shinestacker-1.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
+ shinestacker-1.6.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
109
+ shinestacker-1.6.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
110
+ shinestacker-1.6.1.dist-info/RECORD,,
@@ -1,69 +0,0 @@
1
- # pylint: disable=E1101, C0114, C0115, C0116, E0611, W0718, R0903
2
- import traceback
3
- import cv2
4
- from PySide6.QtCore import QThread, Signal
5
- from .. algorithms.utils import read_img, validate_image, get_img_metadata
6
- from .. algorithms.exif import get_exif, write_image_with_exif_data
7
- from .. algorithms.multilayer import write_multilayer_tiff_from_images
8
- from .layer_collection import LayerCollectionHandler
9
-
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
-
31
- class IOManager(LayerCollectionHandler):
32
- def __init__(self, layer_collection):
33
- super().__init__(layer_collection)
34
- self.exif_path = ''
35
- self.exif_data = None
36
-
37
- def import_frames(self, file_paths):
38
- stack = []
39
- labels = []
40
- master = None
41
- shape, dtype = get_img_metadata(self.master_layer())
42
- for path in file_paths:
43
- try:
44
- label = path.split("/")[-1].split(".")[0]
45
- img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
46
- if shape is not None and dtype is not None:
47
- validate_image(img, shape, dtype)
48
- else:
49
- shape, dtype = get_img_metadata(img)
50
- label_x = label
51
- i = 0
52
- while label_x in labels:
53
- i += 1
54
- label_x = f"{label} ({i})"
55
- labels.append(label_x)
56
- stack.append(img)
57
- if master is None:
58
- master = img.copy()
59
- except Exception as e:
60
- raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
61
- return stack, labels, master
62
-
63
- def save_master(self, path):
64
- img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
65
- write_image_with_exif_data(self.exif_data, img, path)
66
-
67
- def set_exif_data(self, path):
68
- self.exif_path = path
69
- self.exif_data = get_exif(path)