shinestacker 1.8.1__py3-none-any.whl → 1.9.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.

@@ -185,7 +185,7 @@ class Vignetting(SubAction):
185
185
  for i, p in enumerate(self.percentiles):
186
186
  s1 = sigmoid_model(0, *params) / self.v0
187
187
  s2 = sigmoid_model(self.r_max, *params) / self.v0
188
- if s1 > p and s2 < p:
188
+ if s1 > p > s2:
189
189
  try:
190
190
  c = bisect(lambda x: sigmoid_model(x, *params) / self.v0 - p, 0, self.r_max)
191
191
  except Exception as e:
shinestacker/app/main.py CHANGED
@@ -199,7 +199,7 @@ class MainApp(QMainWindow):
199
199
  class Application(QApplication):
200
200
  def event(self, event):
201
201
  if event.type() == QEvent.Quit and event.spontaneous():
202
- if not self.quit():
202
+ if not self.main_app.quit():
203
203
  return True
204
204
  return super().event(event)
205
205
 
@@ -7,7 +7,6 @@ import os
7
7
  class _Constants:
8
8
  APP_TITLE = "Shine Stacker"
9
9
  APP_STRING = "ShineStacker"
10
- EXTENSIONS = set(["jpeg", "jpg", "png", "tif", "tiff"])
11
10
 
12
11
  NUM_UINT8 = 256
13
12
  NUM_UINT16 = 65536
@@ -6,6 +6,7 @@ from PySide6.QtCore import QTimer
6
6
  from PySide6.QtWidgets import QWidget, QLabel, QMessageBox, QStackedWidget
7
7
  from .. config.constants import constants
8
8
  from .. config.app_config import AppConfig
9
+ from .. algorithms.utils import EXTENSIONS_SUPPORTED
9
10
  from .. algorithms.align import validate_align_config
10
11
  from . action_config import (
11
12
  DefaultActionConfigurator, add_tab, create_tab_layout, create_tab_widget,
@@ -122,7 +123,7 @@ class JobConfigurator(DefaultActionConfigurator):
122
123
  return 0
123
124
  count = 0
124
125
  for filename in os.listdir(path):
125
- if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff')):
126
+ if os.path.splitext(filename)[-1][1:].lower() in EXTENSIONS_SUPPORTED:
126
127
  count += 1
127
128
  return count
128
129
 
@@ -371,10 +372,6 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
371
372
  self.add_field_to_layout(
372
373
  self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
373
374
  default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
374
- self.add_field_to_layout(
375
- self.general_tab_layout, 'scratch_output_dir', FIELD_BOOL,
376
- 'Scratch output folder before run',
377
- required=False, default=True)
378
375
  self.add_field_to_layout(
379
376
  self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
380
377
  'Delete output at end of job',
@@ -12,6 +12,7 @@ class ConfigDialog(QDialog):
12
12
  self.form_layout = create_form_layout(self)
13
13
  scroll_area = QScrollArea()
14
14
  scroll_area.setWidgetResizable(True)
15
+ scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
15
16
  container_widget = QWidget()
16
17
  self.container_layout = QFormLayout(container_widget)
17
18
  self.container_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
@@ -19,19 +20,19 @@ class ConfigDialog(QDialog):
19
20
  self.container_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
20
21
  self.container_layout.setLabelAlignment(Qt.AlignLeft)
21
22
  scroll_area.setWidget(container_widget)
22
- button_box = QHBoxLayout()
23
+ self.button_box = QHBoxLayout()
23
24
  self.ok_button = QPushButton("OK")
24
25
  self.ok_button.setFocus()
25
26
  self.cancel_button = QPushButton("Cancel")
26
27
  self.reset_button = QPushButton("Reset")
27
- button_box.addWidget(self.ok_button)
28
- button_box.addWidget(self.cancel_button)
29
- button_box.addWidget(self.reset_button)
28
+ self.button_box.addWidget(self.ok_button)
29
+ self.button_box.addWidget(self.cancel_button)
30
+ self.button_box.addWidget(self.reset_button)
30
31
  self.reset_button.clicked.connect(self.reset_to_defaults)
31
32
  self.ok_button.clicked.connect(self.accept)
32
33
  self.cancel_button.clicked.connect(self.reject)
33
34
  self.form_layout.addRow(scroll_area)
34
- self.form_layout.addRow(button_box)
35
+ self.form_layout.addRow(self.button_box)
35
36
  QTimer.singleShot(0, self.adjust_dialog_size)
36
37
  self.create_form_content()
37
38
 
@@ -1,8 +1,9 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611
2
2
  import os
3
+ from PySide6.QtCore import Qt
3
4
  from PySide6.QtWidgets import (QWidget, QRadioButton, QButtonGroup, QLineEdit,
4
5
  QPushButton, QHBoxLayout, QVBoxLayout, QFileDialog, QMessageBox)
5
- from PySide6.QtCore import Qt
6
+ from .. algorithms.utils import EXTENSIONS_GUI_STR
6
7
 
7
8
 
8
9
  class FolderFileSelectionWidget(QWidget):
@@ -73,7 +74,7 @@ class FolderFileSelectionWidget(QWidget):
73
74
  def browse_files(self):
74
75
  files, _ = QFileDialog.getOpenFileNames(
75
76
  self, "Select Input Files", "",
76
- "Image files (*.png *.jpg *.jpeg *.tif *.tiff)"
77
+ f"Image files ({EXTENSIONS_GUI_STR})"
77
78
  )
78
79
  if files:
79
80
  parent_dir = os.path.dirname(files[0])
@@ -9,7 +9,7 @@ from PySide6.QtCore import Signal, Slot
9
9
  from .. config.constants import constants
10
10
  from .. config.gui_constants import gui_constants
11
11
  from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
12
- from .. algorithms.utils import extension_jpg_tif_png, extension_pdf
12
+ from .. algorithms.utils import extension_supported, extension_pdf
13
13
  from .gui_logging import LogWorker, QTextEditLogger
14
14
  from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
15
15
  from .colors import (
@@ -209,7 +209,7 @@ class RunWindow(QTextEditLogger):
209
209
  try:
210
210
  if extension_pdf(path):
211
211
  image_view = GuiPdfView(path, self)
212
- elif extension_jpg_tif_png(path):
212
+ elif extension_supported(path):
213
213
  image_view = GuiImageView(path, self)
214
214
  else:
215
215
  raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
@@ -8,7 +8,7 @@ from PySide6.QtCore import Qt
8
8
  from .. config.gui_constants import gui_constants
9
9
  from .. config.constants import constants
10
10
  from .. config.app_config import AppConfig
11
- from .. algorithms.utils import read_img, extension_tif_jpg
11
+ from .. algorithms.utils import read_img, extension_supported
12
12
  from .. algorithms.stack import get_bunches
13
13
  from .folder_file_selection import FolderFileSelectionWidget
14
14
  from .base_form_dialog import BaseFormDialog
@@ -208,7 +208,7 @@ class NewProjectDialog(BaseFormDialog):
208
208
  return 0
209
209
  count = 0
210
210
  for filename in os.listdir(path):
211
- if extension_tif_jpg(filename):
211
+ if extension_supported(filename):
212
212
  count += 1
213
213
  return count
214
214
  if self.input_widget.get_selection_mode() == 'files' and \
@@ -273,7 +273,7 @@ class NewProjectDialog(BaseFormDialog):
273
273
  file_path = None
274
274
  for filename in files:
275
275
  full_path = os.path.join(path, filename)
276
- if extension_tif_jpg(full_path):
276
+ if extension_supported(full_path):
277
277
  file_path = full_path
278
278
  break
279
279
  if file_path is None:
@@ -284,8 +284,8 @@ class NewProjectDialog(BaseFormDialog):
284
284
  height, width = img.shape[:2]
285
285
  n_bytes = 1 if img.dtype == np.uint8 else 2
286
286
  n_bits = 8 if img.dtype == np.uint8 else 16
287
- n_gbytes = float(n_bytes * height * width * self.n_image_files) / constants.ONE_GIGA
288
- if n_gbytes > 1 and not self.bunch_stack.isChecked():
287
+ n_gbytes = 3.0 * n_bytes * height * width * self.n_image_files / constants.ONE_GIGA
288
+ if n_gbytes > 4 and not self.bunch_stack.isChecked():
289
289
  msg = QMessageBox()
290
290
  msg.setStyleSheet("""
291
291
  QMessageBox {
@@ -1,42 +1,47 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0718
2
+ from xml.dom import minidom
2
3
  from PIL.TiffImagePlugin import IFDRational
3
- from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QLabel
4
+ from PySide6.QtWidgets import QLabel, QTextEdit
4
5
  from PySide6.QtCore import Qt
6
+ from PySide6.QtGui import QFontDatabase
5
7
  from .. algorithms.exif import exif_dict
6
- from .icon_container import icon_container
7
- from .. gui.base_form_dialog import BaseFormDialog
8
+ from .. gui.config_dialog import ConfigDialog
8
9
 
9
10
 
10
- class ExifData(BaseFormDialog):
11
- def __init__(self, exif, parent=None):
12
- super().__init__("EXIF data", parent=parent)
11
+ class ExifData(ConfigDialog):
12
+ def __init__(self, exif, title="EXIF Data", parent=None, show_buttons=True):
13
13
  self.exif = exif
14
- self.create_form()
15
- button_container = QWidget()
16
- button_layout = QHBoxLayout(button_container)
17
- button_layout.setAlignment(Qt.AlignCenter)
18
- ok_button = QPushButton("OK")
19
- ok_button.setFixedWidth(100)
20
- ok_button.setFocus()
21
- button_layout.addWidget(ok_button)
22
- self.add_row_to_layout(button_container)
23
- ok_button.clicked.connect(self.accept)
14
+ super().__init__(title, parent)
15
+ self.reset_button.setVisible(False)
16
+ self.cancel_button.setVisible(show_buttons)
17
+ if not show_buttons:
18
+ self.ok_button.setFixedWidth(100)
19
+ self.button_box.setAlignment(Qt.AlignCenter)
24
20
 
25
- def add_bold_label(self, label):
26
- label = QLabel(label)
27
- label.setStyleSheet("font-weight: bold")
28
- self.form_layout.addRow(label)
21
+ def is_likely_xml(self, text):
22
+ if not isinstance(text, str):
23
+ return False
24
+ text = text.strip()
25
+ return (text.startswith('<?xml') or
26
+ text.startswith('<x:xmpmeta') or
27
+ text.startswith('<rdf:RDF') or
28
+ text.startswith('<?xpacket') or
29
+ (text.startswith('<') and text.endswith('>') and
30
+ any(tag in text for tag in ['<rdf:', '<xmp:', '<dc:', '<tiff:'])))
29
31
 
30
- def create_form(self):
31
- self.form_layout.addRow(icon_container())
32
+ def prettify_xml(self, xml_string):
33
+ try:
34
+ parsed = minidom.parseString(xml_string)
35
+ pretty_xml = parsed.toprettyxml(indent=" ")
36
+ lines = [line for line in pretty_xml.split('\n') if line.strip()]
37
+ if lines and lines[0].startswith('<?xml version="1.0" ?>'):
38
+ lines = lines[1:]
39
+ return '\n'.join(lines)
40
+ except Exception:
41
+ return xml_string
32
42
 
33
- spacer = QLabel("")
34
- spacer.setFixedHeight(10)
35
- self.form_layout.addRow(spacer)
36
- self.add_bold_label("EXIF data")
37
- shortcuts = {}
43
+ def create_form_content(self):
38
44
  if self.exif is None:
39
- shortcuts['Warning:'] = 'no EXIF data found'
40
45
  data = {}
41
46
  else:
42
47
  data = exif_dict(self.exif)
@@ -44,9 +49,27 @@ class ExifData(BaseFormDialog):
44
49
  for k, (_, d) in data.items():
45
50
  if isinstance(d, IFDRational):
46
51
  d = f"{d.numerator}/{d.denominator}"
47
- else:
48
- d = f"{d}"
49
- if "<<<" not in d and k != 'IPTCNAA':
50
- self.form_layout.addRow(f"<b>{k}:</b>", QLabel(d))
52
+ d_str = str(d)
53
+ if "<<<" not in d_str and k != 'IPTCNAA':
54
+ if len(d_str) <= 40:
55
+ self.container_layout.addRow(f"<b>{k}:</b>", QLabel(d_str))
56
+ else:
57
+ if self.is_likely_xml(d_str):
58
+ d_str = self.prettify_xml(d_str)
59
+ text_edit = QTextEdit()
60
+ text_edit.setPlainText(d_str)
61
+ text_edit.setReadOnly(True)
62
+ text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
63
+ text_edit.setLineWrapMode(QTextEdit.WidgetWidth)
64
+ text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
65
+ text_edit.setFixedWidth(400)
66
+ font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
67
+ font.setPointSize(10)
68
+ text_edit.setFont(font)
69
+ font.setPointSize(11)
70
+ text_edit.setFont(font)
71
+ text_edit.setFixedHeight(200)
72
+ text_edit.setFixedHeight(100)
73
+ self.container_layout.addRow(f"<b>{k}:</b>", text_edit)
51
74
  else:
52
- self.form_layout.addRow("-", QLabel("Empty EXIF dictionary"))
75
+ self.container_layout.addRow("No EXIF Data", QLabel(''))
@@ -5,7 +5,7 @@ import numpy as np
5
5
  import cv2
6
6
  from psdtags import PsdChannelId
7
7
  from PySide6.QtCore import QThread, Signal
8
- from .. algorithms.utils import read_img, extension_tif, extension_jpg
8
+ from .. algorithms.utils import read_img, extension_tif, extension_jpg, extension_png
9
9
  from .. algorithms.multilayer import read_multilayer_tiff
10
10
 
11
11
 
@@ -50,10 +50,10 @@ class FileLoader(QThread):
50
50
  raise RuntimeError(f"Path {path} does not exist.")
51
51
  if not os.path.isfile(path):
52
52
  raise RuntimeError(f"Path {path} is not a file.")
53
- if extension_jpg(path):
53
+ if extension_jpg(path) or extension_png(path):
54
54
  try:
55
55
  stack = np.array([cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)])
56
- return stack, [path.split('/')[-1].split('.')[0]]
56
+ return stack, [os.path.splitext(os.path.basename(path))[0]]
57
57
  except Exception as e:
58
58
  traceback.print_tb(e.__traceback__)
59
59
  return None, None
@@ -1,7 +1,8 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904, W0108
2
2
  from functools import partial
3
3
  from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QMenu,
4
- QListWidget, QSlider, QMainWindow, QMessageBox)
4
+ QFileDialog, QListWidget, QSlider, QMainWindow, QMessageBox,
5
+ QDialog)
5
6
  from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
6
7
  from PySide6.QtCore import Qt
7
8
  from PySide6.QtGui import QGuiApplication
@@ -9,6 +10,7 @@ from .. config.constants import constants
9
10
  from .. config.app_config import AppConfig
10
11
  from .. config.gui_constants import gui_constants
11
12
  from .. gui.recent_file_manager import RecentFileManager
13
+ from .. algorithms.exif import get_exif
12
14
  from .image_viewer import ImageViewer
13
15
  from .shortcuts_help import ShortcutsHelp
14
16
  from .brush import Brush
@@ -26,6 +28,7 @@ from .white_balance_filter import WhiteBalanceFilter
26
28
  from .vignetting_filter import VignettingFilter
27
29
  from .adjustments import LumiContrastFilter, SaturationVibranceFilter
28
30
  from .transformation_manager import TransfromationManager
31
+ from .exif_data import ExifData
29
32
 
30
33
 
31
34
  class ImageEditorUI(QMainWindow, LayerCollectionHandler):
@@ -183,6 +186,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
183
186
  self.thumbnail_list.setFixedWidth(gui_constants.THUMB_WIDTH)
184
187
  self.thumbnail_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
185
188
  self.thumbnail_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
189
+ self.exif_dialog = None
186
190
 
187
191
  def change_layer_item(item):
188
192
  layer_idx = self.thumbnail_list.row(item)
@@ -266,8 +270,17 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
266
270
 
267
271
  file_menu.addAction("&Close", self.close_file, "Ctrl+W")
268
272
  file_menu.addSeparator()
273
+ show_exif_action = QAction("Show EXIF Data", self)
274
+ show_exif_action.triggered.connect(self.show_exif_data)
275
+ show_exif_action.setProperty("requires_file", True)
276
+ file_menu.addAction(show_exif_action)
277
+ delete_exif_action = QAction("Delete EXIF Data", self)
278
+ delete_exif_action.triggered.connect(self.delete_exif_data)
279
+ delete_exif_action.setProperty("requires_file", True)
280
+ file_menu.addAction(delete_exif_action)
281
+ file_menu.addSeparator()
269
282
  file_menu.addAction("&Import Frames", self.io_gui_handler.import_frames)
270
- file_menu.addAction("Import &EXIF Data", self.io_gui_handler.select_exif_path)
283
+ file_menu.addAction("Import &EXIF Data", self.select_exif_path)
271
284
 
272
285
  edit_menu = menubar.addMenu("&Edit")
273
286
  self.undo_action = QAction("Undo", self)
@@ -676,6 +689,36 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
676
689
  self.redo_action.setText("Redo")
677
690
  self.redo_action.setEnabled(False)
678
691
 
692
+ def select_exif_path(self):
693
+ path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
694
+ if path:
695
+ temp_exif_data = get_exif(path)
696
+ self.exif_dialog = ExifData(temp_exif_data, "Import Selected EXIF Data",
697
+ self.parent(), show_buttons=True)
698
+ result = self.exif_dialog.exec()
699
+ if result == QDialog.Accepted:
700
+ self.io_gui_handler.set_exif_data(temp_exif_data, path)
701
+ self.show_status_message(f"EXIF data loaded from {path}.")
702
+ else:
703
+ self.show_status_message("EXIF data loading cancelled.")
704
+
705
+ def show_exif_data(self):
706
+ self.exif_dialog = ExifData(self.io_gui_handler.exif_data, "EXIF Data",
707
+ self.parent(), show_buttons=False)
708
+ self.exif_dialog.exec()
709
+
710
+ def delete_exif_data(self):
711
+ reply = QMessageBox.question(
712
+ self,
713
+ "Confirm Delete",
714
+ "Warning: the current EXIF data will be erased.\n\nDo you want to continue?",
715
+ QMessageBox.Yes | QMessageBox.No,
716
+ QMessageBox.No
717
+ )
718
+ if reply == QMessageBox.Yes:
719
+ self.io_gui_handler.exif_data = None
720
+ self.io_gui_handler.exif_path = ''
721
+
679
722
  def luminosity_filter(self):
680
723
  self.filter_manager.apply("Luminosity, Contrast")
681
724
 
@@ -7,9 +7,9 @@ from PySide6.QtWidgets import (QFileDialog, QMessageBox, QVBoxLayout, QLabel, QD
7
7
  QApplication, QProgressBar)
8
8
  from PySide6.QtGui import QGuiApplication, QCursor
9
9
  from PySide6.QtCore import Qt, QObject, QTimer, Signal
10
+ from .. algorithms.utils import EXTENSIONS_GUI_STR, EXTENSIONS_GUI_SAVE_STR
10
11
  from .. algorithms.exif import get_exif, write_image_with_exif_data
11
12
  from .file_loader import FileLoader
12
- from .exif_data import ExifData
13
13
  from .io_threads import FileMultilayerSaver, FrameImporter
14
14
  from .layer_collection import LayerCollectionHandler
15
15
 
@@ -32,7 +32,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
32
32
  self.image_viewer = None
33
33
  self.loading_dialog = None
34
34
  self.loading_timer = None
35
- self.exif_dialog = None
36
35
  self.saver_thread = None
37
36
  self.saving_dialog = None
38
37
  self.saving_timer = None
@@ -46,6 +45,10 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
46
45
  self.exif_data = None
47
46
  self.exif_path = ''
48
47
 
48
+ def set_exif_data(self, data, path):
49
+ self.exif_data = data
50
+ self.exif_path = path
51
+
49
52
  def current_file_path(self):
50
53
  return self.current_file_path_master if self.save_master_only.isChecked() \
51
54
  else self.current_file_path_multi
@@ -135,7 +138,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
135
138
  if file_paths is None:
136
139
  file_paths, _ = QFileDialog.getOpenFileNames(
137
140
  self.parent(), "Open Image", "",
138
- "Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
141
+ F"Images ({EXTENSIONS_GUI_STR});;All Files (*)")
139
142
  if not file_paths:
140
143
  return
141
144
  if self.loader_thread and self.loader_thread.isRunning():
@@ -163,11 +166,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
163
166
  self.loader_thread.finished.connect(self.on_file_loaded)
164
167
  self.loader_thread.error.connect(self.on_file_error)
165
168
  self.loader_thread.start()
169
+ self.exif_path = self.current_file_path_master
170
+ self.exif_data = get_exif(self.exif_path)
166
171
 
167
172
  def import_frames(self):
168
173
  file_paths, _ = QFileDialog.getOpenFileNames(
169
174
  self.parent(), "Select frames", "",
170
- "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
175
+ f"Images Images ({EXTENSIONS_GUI_STR});;All Files (*)")
171
176
  if file_paths:
172
177
  self.import_frames_from_files(file_paths)
173
178
 
@@ -195,6 +200,9 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
195
200
  self.frame_importer_thread.error.connect(self.on_frames_import_error)
196
201
  self.frame_importer_thread.progress.connect(self.update_import_progress)
197
202
  self.frame_importer_thread.start()
203
+ if self.exif_data is None:
204
+ self.exif_path = file_paths[0]
205
+ self.exif_data = get_exif(self.exif_path)
198
206
 
199
207
  def update_import_progress(self, percent, filename):
200
208
  if hasattr(self, 'progress_bar'):
@@ -286,8 +294,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
286
294
  if self.layer_stack() is None:
287
295
  return
288
296
  path, _ = QFileDialog.getSaveFileName(
289
- self.parent(), "Save Image", "",
290
- "TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
297
+ self.parent(), "Save Image", "", EXTENSIONS_GUI_SAVE_STR)
291
298
  if path:
292
299
  self.save_master_to_path(path)
293
300
 
@@ -304,15 +311,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
304
311
  traceback.print_tb(e.__traceback__)
305
312
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
306
313
 
307
- def select_exif_path(self):
308
- path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
309
- if path:
310
- self.exif_path = path
311
- self.exif_data = get_exif(path)
312
- self.status_message_requested.emit(f"EXIF data extracted from {path}.")
313
- self.exif_dialog = ExifData(self.exif_data, self.parent())
314
- self.exif_dialog.exec()
315
-
316
314
  def close_file(self):
317
315
  self.mark_as_modified_requested.emit(False)
318
316
  self.layer_collection.reset()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.8.1
3
+ Version: 1.9.1
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -105,11 +105,13 @@ Pyramid methods in image processing
105
105
  - **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista). Copyright © Alessandro Lista. All rights reserved. The logo is not covered by the LGPL-3.0 license of this project.
106
106
 
107
107
  ## Attribution request
108
+
108
109
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
109
110
 
110
111
  *Created with Shine Stacker – https://github.com/lucalista/shinestacker*
111
112
 
112
113
  This is not mandatory, but highly appreciated.
114
+
113
115
  ---
114
116
  > Developed and maintained by [Luca Lista](https://github.com/lucalista).
115
117
  > 💡 Contributions, feedback, and feature suggestions are warmly welcome.
@@ -1,5 +1,5 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=VeCo3jG4eMq5JgUgBBkmfX0RYN_MfWUdUGiQ16h2kPg,21
2
+ shinestacker/_version.py,sha256=zzuY_qaJa652YnCSIsHTbX-QYu0qr0lMpakTuIQMbtg,21
3
3
  shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
4
4
  shinestacker/algorithms/align.py,sha256=840SLh38JePGQv9vgG2H6jHkgHSAYzSpbNDDTxV5ghg,37915
5
5
  shinestacker/algorithms/align_auto.py,sha256=DsHuAkFXSHbtFwp6XRaV3Sy1LGcUZWYAFijJXWrd1Bo,3833
@@ -9,24 +9,24 @@ shinestacker/algorithms/base_stack_algo.py,sha256=mqCCRufLc9k5fZV5Su41AsN1ecHrZJ
9
9
  shinestacker/algorithms/corrections.py,sha256=DrfLM33D20l4svuuBtoOiH-KGUH_BL1mAV7mHCA_nGA,1094
10
10
  shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
11
11
  shinestacker/algorithms/depth_map.py,sha256=nRBrZQWbdUqFOtYMEQx9UNdnybrBTeAOr1eV91FlN8U,5611
12
- shinestacker/algorithms/exif.py,sha256=SM4ZDDe8hCJ3xY6053FNndOiwzEStzdp0WrXurlcHVc,9429
13
- shinestacker/algorithms/multilayer.py,sha256=EEMDr2NlCU9DCFO5ykbBrY-2q9oBUD0-ctm7x0IXU5U,9911
12
+ shinestacker/algorithms/exif.py,sha256=jdUl3qMUif3wdQr7z8TnDFUo8iR84Zjmg57nmAmskxc,21729
13
+ shinestacker/algorithms/multilayer.py,sha256=SX4digCMvPxvm9KRrwroUwoAc83ScbmjIjN8s5au3wg,10053
14
14
  shinestacker/algorithms/noise_detection.py,sha256=SbWcxSPZIxnThXITAe7koPLKhQZ_gciQby50u3QfkGs,9464
15
15
  shinestacker/algorithms/pyramid.py,sha256=Z7tlp8Hh3ploAXJCr0VNe33d8H9GNrlqHXq_LapgRwo,8205
16
16
  shinestacker/algorithms/pyramid_auto.py,sha256=fl_jXNYLWsBiX0M0UghzCLqai0SGXlmKYHU7Z9SUYSo,6173
17
17
  shinestacker/algorithms/pyramid_tiles.py,sha256=t04_06oYF6QkSSyFQEivHh-GDTska2dQEmfCYoscy-c,12216
18
18
  shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
19
- shinestacker/algorithms/stack.py,sha256=VvYo6w01sGbMIWS3w_4fVz6k7qOTpuEREXTP4kze6B0,5738
20
- shinestacker/algorithms/stack_framework.py,sha256=YEuaxDqs3gytZMUnub69gcuv5CnbwU3cLxxPQj6HsXQ,14612
21
- shinestacker/algorithms/utils.py,sha256=VJFOT-OBtDe5ds64VgwyZKa8AX1N91SSJlsgUVC9vMs,12303
22
- shinestacker/algorithms/vignetting.py,sha256=KFLPbBv8EAKB5PpO4QIqMbTLEufg-lvxOGkVkiOSEqI,10855
19
+ shinestacker/algorithms/stack.py,sha256=dRaxNF3Uap18Q6uXWgPMKHSd18Ci0QooEJZciH68_VE,6495
20
+ shinestacker/algorithms/stack_framework.py,sha256=HwB0gDncjJEKHdaR9fFcc2XoRrgxFNrrFDfVyeO4NRM,14616
21
+ shinestacker/algorithms/utils.py,sha256=1RCsOSQ5TSM8y10Wg5JBDWCAEf-vEQReN_5VMtrLW7o,13127
22
+ shinestacker/algorithms/vignetting.py,sha256=Y-K_CTjtNpl0YX86PaM0te-HFxuEcWozhWoB7-g_S7Y,10849
23
23
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
24
24
  shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
26
26
  shinestacker/app/args_parser_opts.py,sha256=G3jQjxBYk87ycmyf8Idk40c5H90O1l0owz0asTodm88,2183
27
27
  shinestacker/app/gui_utils.py,sha256=rNDtC6vQ1hAJ5F3Vd-VKglCE06mhleu5eiw-oitgnxU,3656
28
28
  shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
29
- shinestacker/app/main.py,sha256=c9_rax1eemOfkJqWV7tT7-ZkEBkqz6n4kBST8iXsjD4,10662
29
+ shinestacker/app/main.py,sha256=0dEtkLsshD5xEjK107EyYaM8fJhqFm88taTbf6BMPrk,10671
30
30
  shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
31
31
  shinestacker/app/project.py,sha256=nwvXllD2FBLQ4ChePQdIGVug46Wh2ubjrJ0sC7klops,2596
32
32
  shinestacker/app/retouch.py,sha256=8XcYMv7-feG6yxNCpvlijZQRPlhmRK0OfZO5MuBju-0,2552
@@ -34,7 +34,7 @@ shinestacker/app/settings_dialog.py,sha256=x4-mYEUcB1I9SoQmzDpxFzfLI5JU0hbeqmIyd
34
34
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
35
35
  shinestacker/config/app_config.py,sha256=rM1Rndk1GDa5c0AhcVNEN9zSAzxPZixzQYfjODbJUwE,771
36
36
  shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
37
- shinestacker/config/constants.py,sha256=fdZ8qp4S5KM_MfXnLCOTnHemNQMcHEio-VatGkDH9-E,8530
37
+ shinestacker/config/constants.py,sha256=qpQ7uuf7qnFesiq4zvt6A7ASjLbyADbbeMzkW-GCbe4,8470
38
38
  shinestacker/config/gui_constants.py,sha256=PNxzwmVEppJ2mV_vwp68NhWzJOEitVy1Pk9SwSmRsho,2882
39
39
  shinestacker/config/settings.py,sha256=jdRMJRT6AzO-dnvmOCwEGURsGBt36ILH-xszNIvE0ew,4845
40
40
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
@@ -45,18 +45,18 @@ shinestacker/core/framework.py,sha256=i-_4v--ZtimmlPUs2DmkEVvbsvEDZmbCmOtMVfCxww
45
45
  shinestacker/core/logging.py,sha256=pN4FGcHwI5ouJKwCVoDWQx_Tg3t84mmPh0xhqszDDkw,3111
46
46
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  shinestacker/gui/action_config.py,sha256=OWW32h55OTvM6lbfJc3ZhPoa0vVEvsH63iCbTWo6r6E,25843
48
- shinestacker/gui/action_config_dialog.py,sha256=GYOnTXoEjHHWMS5RECaqtkMMI37OjkP6W5WXPKjYjwE,40727
48
+ shinestacker/gui/action_config_dialog.py,sha256=YdVtDKehjIxBYKi7AsFcS8RR-cJT_Xmm6VP8LIlTQ1s,40582
49
49
  shinestacker/gui/base_form_dialog.py,sha256=KAUQNtmJazttmOIe4E4pFifbtvcByTAhtCmcIYeA4UE,766
50
50
  shinestacker/gui/colors.py,sha256=-HaFprDuzRSKjXoZfX1rdOuvawQAkazqdgLBEiZcFII,1476
51
- shinestacker/gui/config_dialog.py,sha256=yt3nvh0HPHQuCn3AFlzlIHUJnnxcz-Rrw3W3jS9ZYiE,3447
51
+ shinestacker/gui/config_dialog.py,sha256=vJao8UH8YeE5AuSbYE5Aj9atqo-38DdceHLGsuGvnlw,3544
52
52
  shinestacker/gui/flow_layout.py,sha256=3yBU_z7VtvHKpx1H97CHVd81eq9pe1Dcja2EZBGGKcI,3791
53
- shinestacker/gui/folder_file_selection.py,sha256=IYWfZQFkoD5iO7zJ7BxVVDP9F3Dc0EXLILAhL4q-Cb8,4117
53
+ shinestacker/gui/folder_file_selection.py,sha256=CwussPYMguMk8WuyuUKk28VneafwGR-5yiqPo0bp_XE,4158
54
54
  shinestacker/gui/gui_images.py,sha256=KxGBFLL2ztfNmvL4pconi3z5HJCoD2HXxpYZP70aUfM,6803
55
55
  shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
56
- shinestacker/gui/gui_run.py,sha256=38ke2Zq7KfQBZDNCzfw6RVIUdDTElLKf-tawBarlWyw,15684
56
+ shinestacker/gui/gui_run.py,sha256=Tp3BQTbASdfyELQonJPM10dX9mWb7TdecsIjzCnVQsA,15680
57
57
  shinestacker/gui/main_window.py,sha256=VYGX-w-A8sy1zsQAJEfLpImax8oB-inx_nZ2XofDEBQ,25777
58
58
  shinestacker/gui/menu_manager.py,sha256=mS-pRMymd1yYimbr6Z5YXjMA5AsNuaNcezs8MYWF2DU,12364
59
- shinestacker/gui/new_project.py,sha256=gvFNToDqLxWbgGBJ19u_2Tn_DlfXTVWQ5wE0dHErXL0,16723
59
+ shinestacker/gui/new_project.py,sha256=fnTWxT0YS390T4CTu6Cdl7pWrjsCiphnKZJvDLzXGlE,16728
60
60
  shinestacker/gui/project_controller.py,sha256=h2x7Z1MFKXQGB4dGmdLcXQgcDTtId9RMi3m-4pSli2Y,16963
61
61
  shinestacker/gui/project_converter.py,sha256=Gmna0HwbvACcXiX74TaQYumif8ZV8sZ2APLTMM-L1mU,7436
62
62
  shinestacker/gui/project_editor.py,sha256=9KEH-CkIbK_yLKRo184C08uYXQ9_aqepEGQrKRqhfUg,25991
@@ -89,14 +89,14 @@ shinestacker/retouch/brush_preview.py,sha256=cOFVMCbEsgR_alzmr_-LLghtGU_unrE-hAj
89
89
  shinestacker/retouch/brush_tool.py,sha256=8uVncTA375uC3Nhp2YM0eZjpOR-nN47i2eGjN8tJzOU,8714
90
90
  shinestacker/retouch/denoise_filter.py,sha256=QVXFU54MDcylNWtiIcdQSZ3eClW_xNWZhCMIeoEQ8zk,576
91
91
  shinestacker/retouch/display_manager.py,sha256=fTZTGbvmX5DXagexuvbNgOF5GiH2Vv-stLUQQwoglp8,10181
92
- shinestacker/retouch/exif_data.py,sha256=LF-fRXW-reMq-xJ_QRE5j8DC2LVGKIlC6MR3QbC1cdg,1896
93
- shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
92
+ shinestacker/retouch/exif_data.py,sha256=9m2_XwSZk58u3EJQnySLFB-IVdMdyrVWkiLPhcKEfPk,3298
93
+ shinestacker/retouch/file_loader.py,sha256=FTOGOuQRHekofESFDsCvnUU5XnZH_GbLfxXwKnoxZ4s,4832
94
94
  shinestacker/retouch/filter_manager.py,sha256=tOGIWj5HjViL1-iXHkd91X-sZ1c1G531pDmLO0x6zx0,866
95
95
  shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
96
- shinestacker/retouch/image_editor_ui.py,sha256=w6tyeYm1Arjyr-MxbLNKYvURV0qEZqigK0iUoqGy92o,34244
96
+ shinestacker/retouch/image_editor_ui.py,sha256=a48GiU-Pm6viNe54KEwq6y_Re-YqsB6juxLLj4-C53Y,36189
97
97
  shinestacker/retouch/image_view_status.py,sha256=2rWi2ugdyjMhWCtRJkwOnb7-tCtVfnGfCY_54qpZhwM,1970
98
98
  shinestacker/retouch/image_viewer.py,sha256=xf1vYZRPb9ClCQbqrqAFhPubdqIIpku7DgcY8O5bvYU,4694
99
- shinestacker/retouch/io_gui_handler.py,sha256=tyHMmR6_uE_IEI-CIcJibVhupxarKYHzX2fuhnesHaI,14616
99
+ shinestacker/retouch/io_gui_handler.py,sha256=iXVCNIWxLwF28g5H-BePYYzAZgCuksUreITmOO8MI9E,14508
100
100
  shinestacker/retouch/io_threads.py,sha256=r0X4it2PfwnmiAU7eStniIfcHhPvuaqdqf5VlnvjZ-4,2832
101
101
  shinestacker/retouch/layer_collection.py,sha256=xx8INSLCXIeTQn_nxfCo4QljAmQK1qukSYO1Zk4rqqo,6183
102
102
  shinestacker/retouch/overlaid_view.py,sha256=QTTdegUWs99YBZZPlIRdPI5O80U3t_c3HnyegbRqNbA,7029
@@ -109,9 +109,9 @@ shinestacker/retouch/unsharp_mask_filter.py,sha256=SO-6ZgPPDAO9em_MMefVvvSvt01-2
109
109
  shinestacker/retouch/view_strategy.py,sha256=jZxB_vX3_0notH0ClxKkLzbdtx4is3vQiYoIP-sDv3M,30216
110
110
  shinestacker/retouch/vignetting_filter.py,sha256=M7PZGPdVSq4bqo6wkEznrILMIG3-mTT7iwpgK4Hieyg,3794
111
111
  shinestacker/retouch/white_balance_filter.py,sha256=UaH4yxG3fU4vPutBAkV5oTXIQyUTN09x0uTywAzv3sY,8286
112
- shinestacker-1.8.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
113
- shinestacker-1.8.1.dist-info/METADATA,sha256=3Lq1O-DEp4lKM1EcwARqdZ6T92cjXw5TXerHxIC30Ws,6881
114
- shinestacker-1.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
- shinestacker-1.8.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
116
- shinestacker-1.8.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
117
- shinestacker-1.8.1.dist-info/RECORD,,
112
+ shinestacker-1.9.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
113
+ shinestacker-1.9.1.dist-info/METADATA,sha256=Udu0wqbX3XEcA_H38Rc9ZIjFMruYS3vEOTS4kYpKfOY,6883
114
+ shinestacker-1.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
+ shinestacker-1.9.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
116
+ shinestacker-1.9.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
117
+ shinestacker-1.9.1.dist-info/RECORD,,