shinestacker 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/__init__.py +4 -1
- shinestacker/algorithms/align.py +117 -3
- shinestacker/algorithms/balance.py +362 -163
- shinestacker/algorithms/base_stack_algo.py +6 -0
- shinestacker/algorithms/depth_map.py +1 -1
- shinestacker/algorithms/multilayer.py +12 -2
- shinestacker/algorithms/noise_detection.py +1 -1
- shinestacker/algorithms/pyramid.py +3 -2
- shinestacker/algorithms/pyramid_auto.py +141 -0
- shinestacker/algorithms/pyramid_tiles.py +199 -44
- shinestacker/algorithms/stack.py +3 -3
- shinestacker/algorithms/stack_framework.py +13 -4
- shinestacker/algorithms/utils.py +175 -1
- shinestacker/algorithms/vignetting.py +23 -5
- shinestacker/config/constants.py +29 -6
- shinestacker/gui/action_config.py +6 -7
- shinestacker/gui/action_config_dialog.py +425 -280
- shinestacker/gui/base_form_dialog.py +11 -6
- shinestacker/gui/main_window.py +3 -2
- shinestacker/gui/menu_manager.py +12 -2
- shinestacker/gui/new_project.py +27 -22
- shinestacker/gui/project_controller.py +39 -23
- shinestacker/gui/project_converter.py +2 -8
- shinestacker/gui/project_editor.py +21 -7
- shinestacker/retouch/exif_data.py +5 -5
- shinestacker/retouch/shortcuts_help.py +4 -4
- shinestacker/retouch/vignetting_filter.py +12 -8
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/METADATA +1 -1
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/RECORD +34 -33
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -3,16 +3,21 @@ from PySide6.QtWidgets import QFormLayout, QDialog
|
|
|
3
3
|
from PySide6.QtCore import Qt
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
def create_form_layout(parent):
|
|
7
|
+
layout = QFormLayout(parent)
|
|
8
|
+
layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
9
|
+
layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
10
|
+
layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
11
|
+
layout.setLabelAlignment(Qt.AlignLeft)
|
|
12
|
+
return layout
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
class BaseFormDialog(QDialog):
|
|
7
16
|
def __init__(self, title, parent=None):
|
|
8
17
|
super().__init__(parent)
|
|
9
18
|
self.setWindowTitle(title)
|
|
10
19
|
self.resize(500, self.height())
|
|
11
|
-
self.
|
|
12
|
-
self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
13
|
-
self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
14
|
-
self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
15
|
-
self.layout.setLabelAlignment(Qt.AlignLeft)
|
|
20
|
+
self.form_layout = create_form_layout(self)
|
|
16
21
|
|
|
17
22
|
def add_row_to_layout(self, item):
|
|
18
|
-
self.
|
|
23
|
+
self.form_layout.addRow(item)
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -135,6 +135,8 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
135
135
|
self.project_editor.refresh_ui_signal.connect(self.refresh_ui)
|
|
136
136
|
self.project_editor.enable_delete_action_signal.connect(
|
|
137
137
|
self.menu_manager.delete_element_action.setEnabled)
|
|
138
|
+
self.project_editor.undo_manager.set_enabled_undo_action_requested.connect(
|
|
139
|
+
self.menu_manager.undo_action.setEnabled)
|
|
138
140
|
self.project_controller.update_title_requested.connect(self.update_title)
|
|
139
141
|
self.project_controller.refresh_ui_requested.connect(self.refresh_ui)
|
|
140
142
|
self.project_controller.activate_window_requested.connect(self.activateWindow)
|
|
@@ -407,13 +409,12 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
407
409
|
self.menu_manager.add_action_entry_action.setEnabled(False)
|
|
408
410
|
self.menu_manager.action_selector.setEnabled(False)
|
|
409
411
|
self.menu_manager.run_job_action.setEnabled(False)
|
|
410
|
-
self.menu_manager.run_all_jobs_action.setEnabled(False)
|
|
411
412
|
else:
|
|
412
413
|
self.menu_manager.add_action_entry_action.setEnabled(True)
|
|
413
414
|
self.menu_manager.action_selector.setEnabled(True)
|
|
414
415
|
self.menu_manager.delete_element_action.setEnabled(True)
|
|
415
416
|
self.menu_manager.run_job_action.setEnabled(True)
|
|
416
|
-
|
|
417
|
+
self.menu_manager.set_enabled_run_all_jobs(self.job_list_count() > 1)
|
|
417
418
|
|
|
418
419
|
def quit(self):
|
|
419
420
|
if self.project_controller.check_unsaved_changes():
|
shinestacker/gui/menu_manager.py
CHANGED
|
@@ -82,7 +82,10 @@ class MenuManager:
|
|
|
82
82
|
|
|
83
83
|
def add_edit_menu(self):
|
|
84
84
|
menu = self.menubar.addMenu("&Edit")
|
|
85
|
-
|
|
85
|
+
self.undo_action = self.action("&Undo")
|
|
86
|
+
self.undo_action.setEnabled(False)
|
|
87
|
+
menu.addAction(self.undo_action)
|
|
88
|
+
for name in ["&Cut", "Cop&y", "&Paste", "Duplicate"]:
|
|
86
89
|
menu.addAction(self.action(name))
|
|
87
90
|
self.delete_element_action = self.action("Delete")
|
|
88
91
|
self.delete_element_action.setEnabled(False)
|
|
@@ -113,7 +116,7 @@ class MenuManager:
|
|
|
113
116
|
self.run_job_action.setEnabled(False)
|
|
114
117
|
menu.addAction(self.run_job_action)
|
|
115
118
|
self.run_all_jobs_action = self.action("Run All Jobs")
|
|
116
|
-
self.
|
|
119
|
+
self.set_enabled_run_all_jobs(False)
|
|
117
120
|
menu.addAction(self.run_all_jobs_action)
|
|
118
121
|
|
|
119
122
|
def add_actions_menu(self):
|
|
@@ -234,3 +237,10 @@ class MenuManager:
|
|
|
234
237
|
self.sub_action_selector.setEnabled(enabled)
|
|
235
238
|
for a in self.sub_action_menu_entries:
|
|
236
239
|
a.setEnabled(enabled)
|
|
240
|
+
|
|
241
|
+
def set_enabled_run_all_jobs(self, enabled):
|
|
242
|
+
tooltip = self.tooltips["Run All Jobs"]
|
|
243
|
+
self.run_all_jobs_action.setEnabled(enabled)
|
|
244
|
+
if not enabled:
|
|
245
|
+
tooltip += " (requires more tha one job)"
|
|
246
|
+
self.run_all_jobs_action.setToolTip(tooltip)
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -36,11 +36,11 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
36
36
|
def add_bold_label(self, label):
|
|
37
37
|
label = QLabel(label)
|
|
38
38
|
label.setStyleSheet("font-weight: bold")
|
|
39
|
-
self.
|
|
39
|
+
self.form_layout.addRow(label)
|
|
40
40
|
|
|
41
41
|
def add_label(self, label):
|
|
42
42
|
label = QLabel(label)
|
|
43
|
-
self.
|
|
43
|
+
self.form_layout.addRow(label)
|
|
44
44
|
|
|
45
45
|
def create_form(self):
|
|
46
46
|
icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
|
|
@@ -49,10 +49,10 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
49
49
|
icon_label = QLabel()
|
|
50
50
|
icon_label.setPixmap(icon_pixmap)
|
|
51
51
|
icon_label.setAlignment(Qt.AlignCenter)
|
|
52
|
-
self.
|
|
52
|
+
self.form_layout.addRow(icon_label)
|
|
53
53
|
spacer = QLabel("")
|
|
54
54
|
spacer.setFixedHeight(10)
|
|
55
|
-
self.
|
|
55
|
+
self.form_layout.addRow(spacer)
|
|
56
56
|
|
|
57
57
|
self.input_folder, container = create_select_file_paths_widget(
|
|
58
58
|
'', 'input files folder', 'input files folder')
|
|
@@ -92,26 +92,26 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
92
92
|
self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
|
|
93
93
|
|
|
94
94
|
self.add_bold_label("1️⃣ Select input folder, all images therein will be merged. ")
|
|
95
|
-
self.
|
|
96
|
-
self.
|
|
95
|
+
self.form_layout.addRow("Input folder:", container)
|
|
96
|
+
self.form_layout.addRow("Number of frames: ", self.frames_label)
|
|
97
97
|
self.add_label("")
|
|
98
98
|
self.add_bold_label("2️⃣ Select basic options.")
|
|
99
99
|
if self.expert():
|
|
100
|
-
self.
|
|
101
|
-
self.
|
|
102
|
-
self.
|
|
103
|
-
self.
|
|
104
|
-
self.
|
|
105
|
-
self.
|
|
106
|
-
self.
|
|
107
|
-
self.
|
|
100
|
+
self.form_layout.addRow("Automatic noise detection:", self.noise_detection)
|
|
101
|
+
self.form_layout.addRow("Vignetting correction:", self.vignetting_correction)
|
|
102
|
+
self.form_layout.addRow("Align layers:", self.align_frames)
|
|
103
|
+
self.form_layout.addRow("Balance layers:", self.balance_frames)
|
|
104
|
+
self.form_layout.addRow("Bunch stack:", self.bunch_stack)
|
|
105
|
+
self.form_layout.addRow("Bunch frames:", self.bunch_frames)
|
|
106
|
+
self.form_layout.addRow("Bunch overlap:", self.bunch_overlap)
|
|
107
|
+
self.form_layout.addRow("Number of bunches: ", self.bunches_label)
|
|
108
108
|
if self.expert():
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
109
|
+
self.form_layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
|
|
110
|
+
self.form_layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
|
|
111
111
|
else:
|
|
112
|
-
self.
|
|
112
|
+
self.form_layout.addRow("Focus stack:", self.focus_stack_pyramid)
|
|
113
113
|
if self.expert():
|
|
114
|
-
self.
|
|
114
|
+
self.form_layout.addRow("Save multi layer TIFF:", self.multi_layer)
|
|
115
115
|
self.add_label("")
|
|
116
116
|
self.add_bold_label("3️⃣ Push 🆗 for further options, then press ▶️ to run.")
|
|
117
117
|
self.add_label("")
|
|
@@ -182,8 +182,9 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
182
182
|
height, width = img.shape[:2]
|
|
183
183
|
n_bytes = 1 if img.dtype == np.uint8 else 2
|
|
184
184
|
n_bits = 8 if img.dtype == np.uint8 else 16
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
n_gbytes = float(n_bytes * height * width * self.n_image_files) / constants.ONE_GIGA
|
|
186
|
+
print("GBytes: ", n_gbytes)
|
|
187
|
+
if n_gbytes > 1 and not self.bunch_stack.isChecked():
|
|
187
188
|
msg = QMessageBox()
|
|
188
189
|
msg.setStyleSheet("""
|
|
189
190
|
QMessageBox {
|
|
@@ -201,11 +202,15 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
201
202
|
msg.setWindowTitle("Too many frames")
|
|
202
203
|
msg.setText(f"You selected {self.n_image_files} images "
|
|
203
204
|
f"with resolution {width}×{height} pixels, {n_bits} bits depth. "
|
|
204
|
-
"Processing may require a significant amount
|
|
205
|
+
"Processing may require a significant amount "
|
|
206
|
+
"of memory or I/O buffering.\n\n"
|
|
205
207
|
"Continue anyway?")
|
|
206
208
|
msg.setInformativeText("You may consider to split the processing "
|
|
207
209
|
" using a bunch stack to reduce memory usage.\n\n"
|
|
208
|
-
'✅ Check the option "Bunch stack"
|
|
210
|
+
'✅ Check the option "Bunch stack".\n\n'
|
|
211
|
+
"➡️ Check expert options for the stacking algorithm."
|
|
212
|
+
'Go to "View" > "Expert Options".'
|
|
213
|
+
)
|
|
209
214
|
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
|
|
210
215
|
msg.setDefaultButton(QMessageBox.Cancel)
|
|
211
216
|
if msg.exec_() != QMessageBox.Ok:
|
|
@@ -136,6 +136,7 @@ class ProjectController(QObject):
|
|
|
136
136
|
self.clear_job_list()
|
|
137
137
|
self.clear_action_list()
|
|
138
138
|
self.mark_as_modified(False)
|
|
139
|
+
self.project_editor.reset_undo()
|
|
139
140
|
|
|
140
141
|
def new_project(self):
|
|
141
142
|
if not self.check_unsaved_changes():
|
|
@@ -151,65 +152,79 @@ class ProjectController(QObject):
|
|
|
151
152
|
dialog = NewProjectDialog(self.parent)
|
|
152
153
|
if dialog.exec() == QDialog.Accepted:
|
|
153
154
|
self.save_actions_set_enabled(True)
|
|
155
|
+
self.project_editor.reset_undo()
|
|
154
156
|
input_folder = dialog.get_input_folder().split('/')
|
|
155
157
|
working_path = '/'.join(input_folder[:-1])
|
|
156
158
|
input_path = input_folder[-1]
|
|
157
159
|
if dialog.get_noise_detection():
|
|
158
|
-
job_noise = ActionConfig(
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
job_noise = ActionConfig(
|
|
161
|
+
constants.ACTION_JOB,
|
|
162
|
+
{'name': f'{input_path}-detect-noise', 'working_path': working_path,
|
|
163
|
+
'input_path': input_path})
|
|
161
164
|
noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
|
|
162
|
-
{'name': 'detect-noise'})
|
|
165
|
+
{'name': f'{input_path}-detect-noise'})
|
|
163
166
|
job_noise.add_sub_action(noise_detection)
|
|
164
167
|
self.add_job_to_project(job_noise)
|
|
165
168
|
job = ActionConfig(constants.ACTION_JOB,
|
|
166
|
-
{'name': 'focus-stack',
|
|
169
|
+
{'name': f'{input_path}-focus-stack',
|
|
170
|
+
'working_path': working_path,
|
|
167
171
|
'input_path': input_path})
|
|
172
|
+
preprocess_name = ''
|
|
168
173
|
if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
|
|
169
174
|
dialog.get_align_frames() or dialog.get_balance_frames():
|
|
170
|
-
|
|
175
|
+
preprocess_name = f'{input_path}-preprocess'
|
|
176
|
+
combo_action = ActionConfig(
|
|
177
|
+
constants.ACTION_COMBO, {'name': preprocess_name})
|
|
171
178
|
if dialog.get_noise_detection():
|
|
172
|
-
mask_noise = ActionConfig(
|
|
179
|
+
mask_noise = ActionConfig(
|
|
180
|
+
constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
|
|
173
181
|
combo_action.add_sub_action(mask_noise)
|
|
174
182
|
if dialog.get_vignetting_correction():
|
|
175
|
-
vignetting = ActionConfig(
|
|
183
|
+
vignetting = ActionConfig(
|
|
184
|
+
constants.ACTION_VIGNETTING, {'name': 'vignetting'})
|
|
176
185
|
combo_action.add_sub_action(vignetting)
|
|
177
186
|
if dialog.get_align_frames():
|
|
178
|
-
align = ActionConfig(
|
|
187
|
+
align = ActionConfig(
|
|
188
|
+
constants.ACTION_ALIGNFRAMES, {'name': 'align'})
|
|
179
189
|
combo_action.add_sub_action(align)
|
|
180
190
|
if dialog.get_balance_frames():
|
|
181
|
-
balance = ActionConfig(
|
|
191
|
+
balance = ActionConfig(
|
|
192
|
+
constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
|
|
182
193
|
combo_action.add_sub_action(balance)
|
|
183
194
|
job.add_sub_action(combo_action)
|
|
184
195
|
if dialog.get_bunch_stack():
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
bunch_stack_name = f'{input_path}-bunches'
|
|
197
|
+
bunch_stack = ActionConfig(
|
|
198
|
+
constants.ACTION_FOCUSSTACKBUNCH,
|
|
199
|
+
{'name': bunch_stack_name, 'frames': dialog.get_bunch_frames(),
|
|
200
|
+
'overlap': dialog.get_bunch_overlap()})
|
|
188
201
|
job.add_sub_action(bunch_stack)
|
|
189
202
|
if dialog.get_focus_stack_pyramid():
|
|
203
|
+
focus_pyramid_name = f'{input_path}-focus-stack-pyramid'
|
|
190
204
|
focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
191
|
-
{'name':
|
|
205
|
+
{'name': focus_pyramid_name,
|
|
192
206
|
'stacker': constants.STACK_ALGO_PYRAMID})
|
|
193
207
|
job.add_sub_action(focus_pyramid)
|
|
194
208
|
if dialog.get_focus_stack_depth_map():
|
|
209
|
+
focus_depth_map_name = f'{input_path}-focus-stack-depth-map'
|
|
195
210
|
focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
196
|
-
{'name':
|
|
211
|
+
{'name': focus_depth_map_name,
|
|
197
212
|
'stacker': constants.STACK_ALGO_DEPTH_MAP})
|
|
198
213
|
job.add_sub_action(focus_depth_map)
|
|
199
214
|
if dialog.get_multi_layer():
|
|
200
|
-
|
|
215
|
+
multi_input_path = []
|
|
201
216
|
if dialog.get_focus_stack_pyramid():
|
|
202
|
-
|
|
217
|
+
multi_input_path.append(focus_pyramid_name)
|
|
203
218
|
if dialog.get_focus_stack_depth_map():
|
|
204
|
-
|
|
219
|
+
multi_input_path.append(focus_depth_map_name)
|
|
205
220
|
if dialog.get_bunch_stack():
|
|
206
|
-
|
|
221
|
+
multi_input_path.append(bunch_stack_name)
|
|
222
|
+
elif preprocess_name:
|
|
223
|
+
multi_input_path.append(preprocess_name)
|
|
207
224
|
multi_layer = ActionConfig(
|
|
208
225
|
constants.ACTION_MULTILAYER,
|
|
209
|
-
{
|
|
210
|
-
'
|
|
211
|
-
'input_path': constants.PATH_SEPARATOR.join(input_path)
|
|
212
|
-
})
|
|
226
|
+
{'name': f'{input_path}-multi-layer',
|
|
227
|
+
'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
|
|
213
228
|
job.add_sub_action(multi_layer)
|
|
214
229
|
self.add_job_to_project(job)
|
|
215
230
|
self.mark_as_modified(True)
|
|
@@ -231,6 +246,7 @@ class ProjectController(QObject):
|
|
|
231
246
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
232
247
|
self.set_project(project)
|
|
233
248
|
self.mark_as_modified(False)
|
|
249
|
+
self.project_editor.reset_undo()
|
|
234
250
|
self.refresh_ui(0, -1)
|
|
235
251
|
if self.job_list_count() > 0:
|
|
236
252
|
self.set_current_job(0)
|
|
@@ -9,8 +9,7 @@ from .. algorithms.vignetting import Vignetting
|
|
|
9
9
|
from .. algorithms.align import AlignFrames
|
|
10
10
|
from .. algorithms.balance import BalanceFrames
|
|
11
11
|
from .. algorithms.stack import FocusStack, FocusStackBunch
|
|
12
|
-
from .. algorithms.
|
|
13
|
-
from .. algorithms.pyramid_tiles import PyramidTilesStack
|
|
12
|
+
from .. algorithms.pyramid_auto import PyramidAutoStack
|
|
14
13
|
from .. algorithms.depth_map import DepthMapStack
|
|
15
14
|
from .. algorithms.multilayer import MultiLayer
|
|
16
15
|
from .project_model import Project, ActionConfig
|
|
@@ -107,11 +106,7 @@ class ProjectConverter:
|
|
|
107
106
|
if stacker == constants.STACK_ALGO_PYRAMID:
|
|
108
107
|
algo_dict, module_dict = self.filter_dict_keys(
|
|
109
108
|
action_config.params, 'pyramid_')
|
|
110
|
-
stack_algo =
|
|
111
|
-
elif stacker == constants.STACK_ALGO_PYRAMID_TILES:
|
|
112
|
-
algo_dict, module_dict = self.filter_dict_keys(
|
|
113
|
-
action_config.params, 'tiles_pyramid_')
|
|
114
|
-
stack_algo = PyramidTilesStack(**algo_dict)
|
|
109
|
+
stack_algo = PyramidAutoStack(**algo_dict)
|
|
115
110
|
elif stacker == constants.STACK_ALGO_DEPTH_MAP:
|
|
116
111
|
algo_dict, module_dict = self.filter_dict_keys(
|
|
117
112
|
action_config.params, 'depthmap_')
|
|
@@ -119,7 +114,6 @@ class ProjectConverter:
|
|
|
119
114
|
else:
|
|
120
115
|
raise InvalidOptionError('stacker', stacker, f"valid options are: "
|
|
121
116
|
f"{constants.STACK_ALGO_PYRAMID}, "
|
|
122
|
-
f"{constants.STACK_ALGO_PYRAMID_TILES}, "
|
|
123
117
|
f"{constants.STACK_ALGO_DEPTH_MAP}")
|
|
124
118
|
if action_config.type_name == constants.ACTION_FOCUSSTACK:
|
|
125
119
|
return FocusStack(**module_dict, stack_algo=stack_algo)
|
|
@@ -74,19 +74,30 @@ def new_row_after_clone(job, action_row, is_sub_action, cloned):
|
|
|
74
74
|
for action in job.sub_actions[:job.sub_actions.index(cloned)])
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
class ProjectUndoManager:
|
|
78
|
-
|
|
77
|
+
class ProjectUndoManager(QObject):
|
|
78
|
+
set_enabled_undo_action_requested = Signal(bool)
|
|
79
|
+
|
|
80
|
+
def __init__(self, parent=None):
|
|
81
|
+
super().__init__(parent)
|
|
79
82
|
self._undo_buffer = []
|
|
80
83
|
|
|
81
84
|
def add(self, item):
|
|
82
85
|
self._undo_buffer.append(item)
|
|
86
|
+
self.set_enabled_undo_action_requested.emit(True)
|
|
83
87
|
|
|
84
88
|
def pop(self):
|
|
85
|
-
|
|
89
|
+
last = self._undo_buffer.pop()
|
|
90
|
+
if len(self._undo_buffer) == 0:
|
|
91
|
+
self.set_enabled_undo_action_requested.emit(False)
|
|
92
|
+
return last
|
|
86
93
|
|
|
87
94
|
def filled(self):
|
|
88
95
|
return len(self._undo_buffer) != 0
|
|
89
96
|
|
|
97
|
+
def reset(self):
|
|
98
|
+
self._undo_buffer = []
|
|
99
|
+
self.set_enabled_undo_action_requested.emit(False)
|
|
100
|
+
|
|
90
101
|
|
|
91
102
|
class ProjectEditor(QObject):
|
|
92
103
|
INDENT_SPACE = " ↪ "
|
|
@@ -99,7 +110,7 @@ class ProjectEditor(QObject):
|
|
|
99
110
|
|
|
100
111
|
def __init__(self, parent=None):
|
|
101
112
|
super().__init__(parent)
|
|
102
|
-
self.
|
|
113
|
+
self.undo_manager = ProjectUndoManager()
|
|
103
114
|
self._modified = False
|
|
104
115
|
self._project = None
|
|
105
116
|
self._copy_buffer = None
|
|
@@ -108,14 +119,17 @@ class ProjectEditor(QObject):
|
|
|
108
119
|
self._action_list = QListWidget()
|
|
109
120
|
self.dialog = None
|
|
110
121
|
|
|
122
|
+
def reset_undo(self):
|
|
123
|
+
self.undo_manager.reset()
|
|
124
|
+
|
|
111
125
|
def add_undo(self, item):
|
|
112
|
-
self.
|
|
126
|
+
self.undo_manager.add(item)
|
|
113
127
|
|
|
114
128
|
def pop_undo(self):
|
|
115
|
-
return self.
|
|
129
|
+
return self.undo_manager.pop()
|
|
116
130
|
|
|
117
131
|
def filled_undo(self):
|
|
118
|
-
return self.
|
|
132
|
+
return self.undo_manager.filled()
|
|
119
133
|
|
|
120
134
|
def mark_as_modified(self, modified=True):
|
|
121
135
|
self._modified = modified
|
|
@@ -25,14 +25,14 @@ class ExifData(BaseFormDialog):
|
|
|
25
25
|
def add_bold_label(self, label):
|
|
26
26
|
label = QLabel(label)
|
|
27
27
|
label.setStyleSheet("font-weight: bold")
|
|
28
|
-
self.
|
|
28
|
+
self.form_layout.addRow(label)
|
|
29
29
|
|
|
30
30
|
def create_form(self):
|
|
31
|
-
self.
|
|
31
|
+
self.form_layout.addRow(icon_container())
|
|
32
32
|
|
|
33
33
|
spacer = QLabel("")
|
|
34
34
|
spacer.setFixedHeight(10)
|
|
35
|
-
self.
|
|
35
|
+
self.form_layout.addRow(spacer)
|
|
36
36
|
self.add_bold_label("EXIF data")
|
|
37
37
|
shortcuts = {}
|
|
38
38
|
if self.exif is None:
|
|
@@ -47,6 +47,6 @@ class ExifData(BaseFormDialog):
|
|
|
47
47
|
else:
|
|
48
48
|
d = f"{d}"
|
|
49
49
|
if "<<<" not in d and k != 'IPTCNAA':
|
|
50
|
-
self.
|
|
50
|
+
self.form_layout.addRow(f"<b>{k}:</b>", QLabel(d))
|
|
51
51
|
else:
|
|
52
|
-
self.
|
|
52
|
+
self.form_layout.addRow("-", QLabel("Empty EXIF dictionary"))
|
|
@@ -10,7 +10,7 @@ class ShortcutsHelp(QDialog):
|
|
|
10
10
|
super().__init__(parent)
|
|
11
11
|
self.setWindowTitle("Shortcut Help")
|
|
12
12
|
self.resize(600, self.height())
|
|
13
|
-
self.
|
|
13
|
+
self.main_layout = QVBoxLayout(self)
|
|
14
14
|
main_widget = QWidget()
|
|
15
15
|
main_layout = QHBoxLayout(main_widget)
|
|
16
16
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
@@ -28,14 +28,14 @@ class ShortcutsHelp(QDialog):
|
|
|
28
28
|
right_layout.setLabelAlignment(Qt.AlignLeft)
|
|
29
29
|
main_layout.addWidget(left_column)
|
|
30
30
|
main_layout.addWidget(right_column)
|
|
31
|
-
self.
|
|
31
|
+
self.main_layout.addWidget(main_widget)
|
|
32
32
|
self.create_form(left_layout, right_layout)
|
|
33
33
|
button_box = QHBoxLayout()
|
|
34
34
|
ok_button = QPushButton("OK")
|
|
35
35
|
ok_button.setFixedWidth(100)
|
|
36
36
|
ok_button.setFocus()
|
|
37
37
|
button_box.addWidget(ok_button)
|
|
38
|
-
self.
|
|
38
|
+
self.main_layout.addLayout(button_box)
|
|
39
39
|
ok_button.clicked.connect(self.accept)
|
|
40
40
|
|
|
41
41
|
def add_bold_label(self, layout, label):
|
|
@@ -44,7 +44,7 @@ class ShortcutsHelp(QDialog):
|
|
|
44
44
|
layout.addRow(label)
|
|
45
45
|
|
|
46
46
|
def create_form(self, left_layout, right_layout):
|
|
47
|
-
self.
|
|
47
|
+
self.main_layout.insertWidget(0, icon_container())
|
|
48
48
|
|
|
49
49
|
shortcuts = {
|
|
50
50
|
"M": "show master layer",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902
|
|
2
2
|
from PySide6.QtCore import Qt
|
|
3
|
-
from PySide6.QtWidgets import QSpinBox, QCheckBox, QLabel, QHBoxLayout, QSlider
|
|
3
|
+
from PySide6.QtWidgets import QSpinBox, QCheckBox, QLabel, QHBoxLayout, QSlider, QComboBox
|
|
4
4
|
from .. config.constants import constants
|
|
5
5
|
from .. algorithms.vignetting import correct_vignetting
|
|
6
6
|
from .base_filter import OneSliderBaseFilter
|
|
@@ -20,11 +20,15 @@ class VignettingFilter(OneSliderBaseFilter):
|
|
|
20
20
|
self.threshold_initial_value = constants.DEFAULT_BLACK_THRESHOLD
|
|
21
21
|
self.threshold_format = "{:.1f}"
|
|
22
22
|
|
|
23
|
+
def get_subsample_factor(self):
|
|
24
|
+
return constants.FIELD_SUBSAMPLE_VALUES[
|
|
25
|
+
constants.FIELD_SUBSAMPLE_OPTIONS.index(self.subsample_box.currentText())]
|
|
26
|
+
|
|
23
27
|
def apply(self, image, strength):
|
|
24
28
|
return correct_vignetting(image, max_correction=strength,
|
|
25
29
|
black_threshold=self.threshold_slider.value(),
|
|
26
30
|
r_steps=self.r_steps_box.value(),
|
|
27
|
-
subsample=self.
|
|
31
|
+
subsample=self.get_subsample_factor(),
|
|
28
32
|
fast_subsampling=True)
|
|
29
33
|
|
|
30
34
|
def add_widgets(self, layout, dlg):
|
|
@@ -42,11 +46,10 @@ class VignettingFilter(OneSliderBaseFilter):
|
|
|
42
46
|
layout.addLayout(threshold_layout)
|
|
43
47
|
subsample_layout = QHBoxLayout()
|
|
44
48
|
subsample_label = QLabel("Subsample:")
|
|
45
|
-
self.subsample_box =
|
|
46
|
-
self.subsample_box.
|
|
47
|
-
self.subsample_box.
|
|
48
|
-
self.subsample_box.
|
|
49
|
-
self.subsample_box.valueChanged.connect(self.threshold_changed)
|
|
49
|
+
self.subsample_box = QComboBox()
|
|
50
|
+
self.subsample_box.addItems(constants.FIELD_SUBSAMPLE_OPTIONS)
|
|
51
|
+
self.subsample_box.setFixedWidth(150)
|
|
52
|
+
self.subsample_box.currentTextChanged.connect(self.threshold_changed)
|
|
50
53
|
self.fast_subsampling_check = QCheckBox("Fast subsampling")
|
|
51
54
|
self.fast_subsampling_check.setChecked(constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
|
|
52
55
|
r_steps_label = QLabel("Radial steps:")
|
|
@@ -64,6 +67,7 @@ class VignettingFilter(OneSliderBaseFilter):
|
|
|
64
67
|
layout.addWidget(self.fast_subsampling_check)
|
|
65
68
|
|
|
66
69
|
def threshold_changed(self, val):
|
|
67
|
-
|
|
70
|
+
subsample = self.get_subsample_factor()
|
|
71
|
+
float_val = self.threshold_max_value * float(subsample) / self.threshold_max_range
|
|
68
72
|
self.threshold_label.setText(self.threshold_format.format(float_val))
|
|
69
73
|
self.param_changed(val)
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
3
|
-
shinestacker/algorithms/__init__.py,sha256=
|
|
4
|
-
shinestacker/algorithms/align.py,sha256=
|
|
5
|
-
shinestacker/algorithms/balance.py,sha256=
|
|
6
|
-
shinestacker/algorithms/base_stack_algo.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=5GblYyMbk8JySosj59Rvi2uzLqfP-DAs77ikwTafXT4,21
|
|
3
|
+
shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
|
|
4
|
+
shinestacker/algorithms/align.py,sha256=BsE3rp4EfJyWTTbC6U1leQQqURD724YLAmxY-1wM-ok,22558
|
|
5
|
+
shinestacker/algorithms/balance.py,sha256=rz_lmBwnJBYLnYt6yJ30BXWmMwpmmmA3rKUznEWY0eo,23463
|
|
6
|
+
shinestacker/algorithms/base_stack_algo.py,sha256=W-VSrCF0-lE_OOsxsnZvJ3BI0NqRKIRMciQV-ui5t_g,2515
|
|
7
7
|
shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
|
|
8
|
-
shinestacker/algorithms/depth_map.py,sha256=
|
|
8
|
+
shinestacker/algorithms/depth_map.py,sha256=m0_Qm8FLDeSWyQEMNx29PzXp_VFGar7gY3jWxq_10t8,5713
|
|
9
9
|
shinestacker/algorithms/exif.py,sha256=SM4ZDDe8hCJ3xY6053FNndOiwzEStzdp0WrXurlcHVc,9429
|
|
10
|
-
shinestacker/algorithms/multilayer.py,sha256
|
|
11
|
-
shinestacker/algorithms/noise_detection.py,sha256=
|
|
12
|
-
shinestacker/algorithms/pyramid.py,sha256=
|
|
13
|
-
shinestacker/algorithms/
|
|
10
|
+
shinestacker/algorithms/multilayer.py,sha256=5aIaIU2GX1eoZbGL4z-Xo8XiFlCvIcZKRDYnbjjdojI,9906
|
|
11
|
+
shinestacker/algorithms/noise_detection.py,sha256=lP5iJLFA5xWN-tpyNDH9AWuoZsL7rKhrxwDcBBc9nCk,9250
|
|
12
|
+
shinestacker/algorithms/pyramid.py,sha256=lmd-lw4bzrpcfBaLnBXHuOJ9J7-5sWq4dC9p_EejqXA,8881
|
|
13
|
+
shinestacker/algorithms/pyramid_auto.py,sha256=ByDH7Xblqj4YfNwsCWwN3wv2xL6gYp2lFnvpNPSEawM,6161
|
|
14
|
+
shinestacker/algorithms/pyramid_tiles.py,sha256=SzjTSheme8MP8nQXfOu8QHbzrtpuQX2aIsBVr5aM4Mc,12165
|
|
14
15
|
shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
|
|
15
|
-
shinestacker/algorithms/stack.py,sha256=
|
|
16
|
-
shinestacker/algorithms/stack_framework.py,sha256=
|
|
17
|
-
shinestacker/algorithms/utils.py,sha256=
|
|
18
|
-
shinestacker/algorithms/vignetting.py,sha256=
|
|
16
|
+
shinestacker/algorithms/stack.py,sha256=fLDps52QT8bsC3Pz8niU0h7cGc70q9goKsgA8wvu_Bg,5054
|
|
17
|
+
shinestacker/algorithms/stack_framework.py,sha256=YKk-JoLV6IVlTiCbo6e-Hg2dF3hdz4CE6WFIXhMXBy8,12818
|
|
18
|
+
shinestacker/algorithms/utils.py,sha256=jImR2XF73gsLRZMic0kv8cyCuO2Zq21tX4kUhaTcfzI,11301
|
|
19
|
+
shinestacker/algorithms/vignetting.py,sha256=stzrWTxJIIByq_mOI0ofE-7b0bL5MLm9dhlj_E-Kxv0,10165
|
|
19
20
|
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
20
21
|
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
22
|
shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
|
|
@@ -27,7 +28,7 @@ shinestacker/app/project.py,sha256=W0u715LZne_PNJvg9msSy27ybIjgDXiEAQdJ7_6BjYI,2
|
|
|
27
28
|
shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
|
|
28
29
|
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
29
30
|
shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
|
|
30
|
-
shinestacker/config/constants.py,sha256=
|
|
31
|
+
shinestacker/config/constants.py,sha256=ncuiYyA5NgggkGbazQbccJHGzXJnl37AOyxEB-GLoB0,7116
|
|
31
32
|
shinestacker/config/gui_constants.py,sha256=5DR-ET1oeMMD7lIsjvAwSuln89A7I9wy9VuAeRo2G64,2575
|
|
32
33
|
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
33
34
|
shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
|
|
@@ -36,20 +37,20 @@ shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk
|
|
|
36
37
|
shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7Kk,7447
|
|
37
38
|
shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
|
|
38
39
|
shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
-
shinestacker/gui/action_config.py,sha256
|
|
40
|
-
shinestacker/gui/action_config_dialog.py,sha256=
|
|
41
|
-
shinestacker/gui/base_form_dialog.py,sha256=
|
|
40
|
+
shinestacker/gui/action_config.py,sha256=-J4UCbXHHlIPxRDL0Veglt4brngJP7_zHvrWof9pzsM,16895
|
|
41
|
+
shinestacker/gui/action_config_dialog.py,sha256=USaF2QeQRNXgzLRbkV6hvVRzOMkMVyTtsAzEAMqCr-g,35191
|
|
42
|
+
shinestacker/gui/base_form_dialog.py,sha256=M_BvjpqUWe9YuecKg0pF3vUJ-1mqF24HjKujst2j3BA,753
|
|
42
43
|
shinestacker/gui/colors.py,sha256=m0pQQ-uvtIN1xmb_-N06BvC7pZYZZnq59ZSEJwutHuk,1432
|
|
43
44
|
shinestacker/gui/flow_layout.py,sha256=3yBU_z7VtvHKpx1H97CHVd81eq9pe1Dcja2EZBGGKcI,3791
|
|
44
45
|
shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
|
|
45
46
|
shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
|
|
46
47
|
shinestacker/gui/gui_run.py,sha256=ahbl6xMFR78QrcBbEDMuaQpkxw6DBFtSX8DCMIyr_7I,15439
|
|
47
|
-
shinestacker/gui/main_window.py,sha256=
|
|
48
|
-
shinestacker/gui/menu_manager.py,sha256=
|
|
49
|
-
shinestacker/gui/new_project.py,sha256=
|
|
50
|
-
shinestacker/gui/project_controller.py,sha256=
|
|
51
|
-
shinestacker/gui/project_converter.py,sha256=
|
|
52
|
-
shinestacker/gui/project_editor.py,sha256=
|
|
48
|
+
shinestacker/gui/main_window.py,sha256=kgozNZWqiPePZB5oX3Twdj_Z_f1RcdoLT2PFMzT0fRM,24361
|
|
49
|
+
shinestacker/gui/menu_manager.py,sha256=SvYV7Dmz4cqoFQtRrO4JQ0uECZ4rL3EQdQg5jNakV6Y,10015
|
|
50
|
+
shinestacker/gui/new_project.py,sha256=Iu8Gm3oRx6tCokv-Ywm_64wsNNxj_QhU1BsyoeLkJaQ,11211
|
|
51
|
+
shinestacker/gui/project_controller.py,sha256=ooHunFKY2-dRBoyx4r3T8vs7UOpGDZUHHaSSR5V8J_E,15821
|
|
52
|
+
shinestacker/gui/project_converter.py,sha256=bNyC1_D_MjcTOCPlQln6CIIlX818-sw_j2omrfQIGQs,7279
|
|
53
|
+
shinestacker/gui/project_editor.py,sha256=UPXNwjpKJi7lgaohlbtojkTkotXeGY7l_W8pVII-UM0,24208
|
|
53
54
|
shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
|
|
54
55
|
shinestacker/gui/select_path_widget.py,sha256=OfQImOmkzbvl5BBshmb7ePWrSGDJQ8VvyaAOypHAGd4,1023
|
|
55
56
|
shinestacker/gui/tab_widget.py,sha256=6iUifK-wu0EzjVFccKHirhA2fENglVi6xREKiD96aaY,2950
|
|
@@ -71,7 +72,7 @@ shinestacker/retouch/brush_preview.py,sha256=QKD3pL7n7YJbIibinUFYKv7lkyq_AWLpt6o
|
|
|
71
72
|
shinestacker/retouch/brush_tool.py,sha256=nxnEuvTioPNw1WeWsT20X1zl-LNZ8i-1ExOcihikEjk,8618
|
|
72
73
|
shinestacker/retouch/denoise_filter.py,sha256=TDUHzhRKlKvCa3D5SCYCZKTpjcl81kGwmONsgSDtO1k,440
|
|
73
74
|
shinestacker/retouch/display_manager.py,sha256=XPbOBmoYc_jNA791WkWkOSaFHb0ztCZechl2p2KSlwQ,9597
|
|
74
|
-
shinestacker/retouch/exif_data.py,sha256=
|
|
75
|
+
shinestacker/retouch/exif_data.py,sha256=WF40bTh0bwIqSQLMkGMCycEED06_q35-TqrBNAyaB-k,1889
|
|
75
76
|
shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
|
|
76
77
|
shinestacker/retouch/filter_manager.py,sha256=SdYIZkZBUvuB6wDG0moGWav5sfEvIcB9ioUJR5wJFts,388
|
|
77
78
|
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
@@ -80,14 +81,14 @@ shinestacker/retouch/image_viewer.py,sha256=3ebrLHTDtGd_EbIT2nNFRUjH836rblmmK7jZ
|
|
|
80
81
|
shinestacker/retouch/io_gui_handler.py,sha256=pT-49uP0GROMOjZ70LoMLgXHnqSDq8ieAlAKGw0t1TM,11418
|
|
81
82
|
shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
|
|
82
83
|
shinestacker/retouch/layer_collection.py,sha256=Q7zoCYRn__jYkfrEC2lY1uKHWfOUbsJ27xaYHIoKVxo,5730
|
|
83
|
-
shinestacker/retouch/shortcuts_help.py,sha256
|
|
84
|
+
shinestacker/retouch/shortcuts_help.py,sha256=-beAkhxVia-wyuYMiAn2uQZxnUdSNadmZM77EdReoOk,3789
|
|
84
85
|
shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
|
|
85
86
|
shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
|
|
86
|
-
shinestacker/retouch/vignetting_filter.py,sha256=
|
|
87
|
+
shinestacker/retouch/vignetting_filter.py,sha256=MA97rQkSL0D-Nh-n2L4AiPR064RoTROkvza4tw84g9U,3658
|
|
87
88
|
shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
|
|
88
|
-
shinestacker-1.
|
|
89
|
-
shinestacker-1.
|
|
90
|
-
shinestacker-1.
|
|
91
|
-
shinestacker-1.
|
|
92
|
-
shinestacker-1.
|
|
93
|
-
shinestacker-1.
|
|
89
|
+
shinestacker-1.2.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
90
|
+
shinestacker-1.2.0.dist-info/METADATA,sha256=01kq9TSpVY2_U67ZhjbPIORbMmWTOWpuiiOFty3IM1Y,6951
|
|
91
|
+
shinestacker-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
92
|
+
shinestacker-1.2.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
93
|
+
shinestacker-1.2.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
94
|
+
shinestacker-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|