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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/exif.py +364 -58
- shinestacker/algorithms/multilayer.py +6 -4
- shinestacker/algorithms/stack.py +26 -14
- shinestacker/algorithms/stack_framework.py +2 -2
- shinestacker/algorithms/utils.py +18 -2
- shinestacker/algorithms/vignetting.py +1 -1
- shinestacker/app/main.py +1 -1
- shinestacker/config/constants.py +0 -1
- shinestacker/gui/action_config_dialog.py +2 -5
- shinestacker/gui/config_dialog.py +6 -5
- shinestacker/gui/folder_file_selection.py +3 -2
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/new_project.py +5 -5
- shinestacker/retouch/exif_data.py +57 -34
- shinestacker/retouch/file_loader.py +3 -3
- shinestacker/retouch/image_editor_ui.py +45 -2
- shinestacker/retouch/io_gui_handler.py +13 -15
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.1.dist-info}/METADATA +3 -1
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.1.dist-info}/RECORD +24 -24
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
|
shinestacker/config/constants.py
CHANGED
|
@@ -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()
|
|
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
|
|
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 (
|
|
77
|
+
f"Image files ({EXTENSIONS_GUI_STR})"
|
|
77
78
|
)
|
|
78
79
|
if files:
|
|
79
80
|
parent_dir = os.path.dirname(files[0])
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -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
|
|
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
|
|
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]}.")
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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 =
|
|
288
|
-
if n_gbytes >
|
|
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
|
|
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 .
|
|
7
|
-
from .. gui.base_form_dialog import BaseFormDialog
|
|
8
|
+
from .. gui.config_dialog import ConfigDialog
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class ExifData(
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
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 (
|
|
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.
|
|
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=
|
|
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=
|
|
13
|
-
shinestacker/algorithms/multilayer.py,sha256=
|
|
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=
|
|
20
|
-
shinestacker/algorithms/stack_framework.py,sha256=
|
|
21
|
-
shinestacker/algorithms/utils.py,sha256=
|
|
22
|
-
shinestacker/algorithms/vignetting.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
93
|
-
shinestacker/retouch/file_loader.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
113
|
-
shinestacker-1.
|
|
114
|
-
shinestacker-1.
|
|
115
|
-
shinestacker-1.
|
|
116
|
-
shinestacker-1.
|
|
117
|
-
shinestacker-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|