shinestacker 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.0.0'
1
+ __version__ = '1.0.1'
@@ -0,0 +1,353 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, R0912, R0904, R0915, W0718
2
+ import os
3
+ import os.path
4
+ import traceback
5
+ import json
6
+ import jsonpickle
7
+ from PySide6.QtCore import Signal, QObject
8
+ from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
9
+ from .. config.constants import constants
10
+ from .. core.core_utils import get_app_base_path
11
+ from .project_model import ActionConfig
12
+ from .new_project import NewProjectDialog
13
+ from .project_model import Project
14
+ from .project_editor import ProjectEditor
15
+
16
+
17
+ class ProjectController(QObject):
18
+ update_title_requested = Signal()
19
+ refresh_ui_requested = Signal(int, int)
20
+ activate_window_requested = Signal()
21
+ enable_save_actions_requested = Signal(bool)
22
+
23
+ def __init__(self, parent):
24
+ super().__init__(parent)
25
+ self.parent = parent
26
+ self.project_editor = ProjectEditor(parent)
27
+
28
+ def refresh_ui(self, job_row=-1, action_row=-1):
29
+ self.refresh_ui_requested.emit(job_row, action_row)
30
+
31
+ def mark_as_modified(self, modified=True):
32
+ self.project_editor.mark_as_modified(modified)
33
+
34
+ def modified(self):
35
+ return self.project_editor.modified()
36
+
37
+ def set_project(self, project):
38
+ self.project_editor.set_project(project)
39
+
40
+ def project(self):
41
+ return self.project_editor.project()
42
+
43
+ def project_jobs(self):
44
+ return self.project_editor.project_jobs()
45
+
46
+ def project_job(self, i):
47
+ return self.project_editor.project_job(i)
48
+
49
+ def add_job_to_project(self, job):
50
+ self.project_editor.add_job_to_project(job)
51
+
52
+ def num_project_jobs(self):
53
+ return self.project_editor.num_project_jobs()
54
+
55
+ def save_actions_set_enabled(self, enabled):
56
+ self.enable_save_actions_requested.emit(enabled)
57
+
58
+ def current_file_path(self):
59
+ return self.project_editor.current_file_path()
60
+
61
+ def current_file_directory(self):
62
+ return self.project_editor.current_file_directory()
63
+
64
+ def current_file_name(self):
65
+ return self.project_editor.current_file_name()
66
+
67
+ def set_current_file_path(self, path):
68
+ self.project_editor.set_current_file_path(path)
69
+
70
+ def job_list(self):
71
+ return self.project_editor.job_list()
72
+
73
+ def action_list(self):
74
+ return self.project_editor.action_list()
75
+
76
+ def current_job_index(self):
77
+ return self.project_editor.current_job_index()
78
+
79
+ def current_action_index(self):
80
+ return self.project_editor.current_action_index()
81
+
82
+ def set_current_job(self, index):
83
+ return self.project_editor.set_current_job(index)
84
+
85
+ def set_current_action(self, index):
86
+ return self.project_editor.set_current_action(index)
87
+
88
+ def job_list_count(self):
89
+ return self.project_editor.job_list_count()
90
+
91
+ def action_list_count(self):
92
+ return self.project_editor.action_list_count()
93
+
94
+ def job_list_item(self, index):
95
+ return self.project_editor.job_list_item(index)
96
+
97
+ def action_list_item(self, index):
98
+ return self.project_editor.action_list_item(index)
99
+
100
+ def job_list_has_focus(self):
101
+ return self.project_editor.job_list_has_focus()
102
+
103
+ def action_list_has_focus(self):
104
+ return self.project_editor.action_list_has_focus()
105
+
106
+ def clear_job_list(self):
107
+ self.project_editor.clear_job_list()
108
+
109
+ def clear_action_list(self):
110
+ self.project_editor.clear_action_list()
111
+
112
+ def num_selected_jobs(self):
113
+ return self.project_editor.num_selected_jobs()
114
+
115
+ def num_selected_actions(self):
116
+ return self.project_editor.num_selected_actions()
117
+
118
+ def get_current_action_at(self, job, action_index):
119
+ return self.project_editor.get_current_action_at(job, action_index)
120
+
121
+ def action_config_dialog(self, action):
122
+ return self.project_editor.action_config_dialog(action)
123
+
124
+ def on_job_selected(self, index):
125
+ return self.project_editor.on_job_selected(index)
126
+
127
+ def update_title(self):
128
+ self.update_title_requested.emit()
129
+
130
+ def close_project(self):
131
+ if self.check_unsaved_changes():
132
+ self.set_project(Project())
133
+ self.set_current_file_path('')
134
+ self.update_title()
135
+ self.clear_job_list()
136
+ self.clear_action_list()
137
+ self.mark_as_modified(False)
138
+
139
+ def new_project(self):
140
+ if not self.check_unsaved_changes():
141
+ return
142
+ os.chdir(get_app_base_path())
143
+ self.set_current_file_path('')
144
+ self.mark_as_modified(False)
145
+ self.update_title()
146
+ self.clear_job_list()
147
+ self.clear_action_list()
148
+ self.set_project(Project())
149
+ self.save_actions_set_enabled(False)
150
+ dialog = NewProjectDialog(self.parent)
151
+ if dialog.exec() == QDialog.Accepted:
152
+ self.save_actions_set_enabled(True)
153
+ input_folder = dialog.get_input_folder().split('/')
154
+ working_path = '/'.join(input_folder[:-1])
155
+ input_path = input_folder[-1]
156
+ if dialog.get_noise_detection():
157
+ job_noise = ActionConfig(constants.ACTION_JOB,
158
+ {'name': 'detect-noise', 'working_path': working_path,
159
+ 'input_path': input_path})
160
+ noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
161
+ {'name': 'detect-noise'})
162
+ job_noise.add_sub_action(noise_detection)
163
+ self.add_job_to_project(job_noise)
164
+ job = ActionConfig(constants.ACTION_JOB,
165
+ {'name': 'focus-stack', 'working_path': working_path,
166
+ 'input_path': input_path})
167
+ if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
168
+ dialog.get_align_frames() or dialog.get_balance_frames():
169
+ combo_action = ActionConfig(constants.ACTION_COMBO, {'name': 'align'})
170
+ if dialog.get_noise_detection():
171
+ mask_noise = ActionConfig(constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
172
+ combo_action.add_sub_action(mask_noise)
173
+ if dialog.get_vignetting_correction():
174
+ vignetting = ActionConfig(constants.ACTION_VIGNETTING, {'name': 'vignetting'})
175
+ combo_action.add_sub_action(vignetting)
176
+ if dialog.get_align_frames():
177
+ align = ActionConfig(constants.ACTION_ALIGNFRAMES, {'name': 'align'})
178
+ combo_action.add_sub_action(align)
179
+ if dialog.get_balance_frames():
180
+ balance = ActionConfig(constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
181
+ combo_action.add_sub_action(balance)
182
+ job.add_sub_action(combo_action)
183
+ if dialog.get_bunch_stack():
184
+ bunch_stack = ActionConfig(constants.ACTION_FOCUSSTACKBUNCH,
185
+ {'name': 'bunches', 'frames': dialog.get_bunch_frames(),
186
+ 'overlap': dialog.get_bunch_overlap()})
187
+ job.add_sub_action(bunch_stack)
188
+ if dialog.get_focus_stack_pyramid():
189
+ focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
190
+ {'name': 'focus-stack-pyramid',
191
+ 'stacker': constants.STACK_ALGO_PYRAMID})
192
+ job.add_sub_action(focus_pyramid)
193
+ if dialog.get_focus_stack_depth_map():
194
+ focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
195
+ {'name': 'focus-stack-depth-map',
196
+ 'stacker': constants.STACK_ALGO_DEPTH_MAP})
197
+ job.add_sub_action(focus_depth_map)
198
+ if dialog.get_multi_layer():
199
+ input_path = []
200
+ if dialog.get_focus_stack_pyramid():
201
+ input_path.append("focus-stack-pyramid")
202
+ if dialog.get_focus_stack_depth_map():
203
+ input_path.append("focus-stack-depth-map")
204
+ if dialog.get_bunch_stack():
205
+ input_path.append("bunches")
206
+ else:
207
+ input_path.append(input_path)
208
+ multi_layer = ActionConfig(constants.ACTION_MULTILAYER,
209
+ {'name': 'multi-layer',
210
+ 'input_path': ','.join(input_path)})
211
+ job.add_sub_action(multi_layer)
212
+ self.add_job_to_project(job)
213
+ self.mark_as_modified(True)
214
+ self.refresh_ui(0, -1)
215
+
216
+ def open_project(self, file_path=False):
217
+ if not self.check_unsaved_changes():
218
+ return
219
+ if file_path is False:
220
+ file_path, _ = QFileDialog.getOpenFileName(
221
+ self.parent, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
222
+ if file_path:
223
+ try:
224
+ self.set_current_file_path(file_path)
225
+ with open(self.current_file_path(), 'r', encoding="utf-8") as file:
226
+ json_obj = json.load(file)
227
+ project = Project.from_dict(json_obj['project'])
228
+ if project is None:
229
+ raise RuntimeError(f"Project from file {file_path} produced a null project.")
230
+ self.set_project(project)
231
+ self.mark_as_modified(False)
232
+ self.refresh_ui(0, -1)
233
+ if self.job_list_count() > 0:
234
+ self.set_current_job(0)
235
+ except Exception as e:
236
+ traceback.print_tb(e.__traceback__)
237
+ QMessageBox.critical(
238
+ self.parent, "Error", f"Cannot open file {file_path}:\n{str(e)}")
239
+ if self.num_project_jobs() > 0:
240
+ self.set_current_job(0)
241
+ self.activate_window_requested.emit()
242
+ self.save_actions_set_enabled(True)
243
+ for job in self.project_jobs():
244
+ if 'working_path' in job.params.keys():
245
+ working_path = job.params['working_path']
246
+ if not os.path.isdir(working_path):
247
+ QMessageBox.warning(
248
+ self.parent, "Working path not found",
249
+ f'''The working path specified in the project file for the job:
250
+ "{job.params['name']}"
251
+ was not found.\n
252
+ Please, select a valid working path.''')
253
+ self.edit_action(job)
254
+ for action in job.sub_actions:
255
+ if 'working_path' in job.params.keys():
256
+ working_path = job.params['working_path']
257
+ if working_path != '' and not os.path.isdir(working_path):
258
+ QMessageBox.warning(
259
+ self.parent, "Working path not found",
260
+ f'''The working path specified in the project file for the job:
261
+ "{job.params['name']}"
262
+ was not found.\n
263
+ Please, select a valid working path.''')
264
+ self.edit_action(action)
265
+
266
+ def save_project(self):
267
+ path = self.current_file_path()
268
+ if path:
269
+ self.do_save(path)
270
+ else:
271
+ self.save_project_as()
272
+
273
+ def save_project_as(self):
274
+ file_path, _ = QFileDialog.getSaveFileName(
275
+ self.parent, "Save Project As", "", "Project Files (*.fsp);;All Files (*)")
276
+ if file_path:
277
+ if not file_path.endswith('.fsp'):
278
+ file_path += '.fsp'
279
+ self.do_save(file_path)
280
+ self.set_current_file_path(file_path)
281
+ os.chdir(os.path.dirname(file_path))
282
+
283
+ def do_save(self, file_path):
284
+ try:
285
+ json_obj = jsonpickle.encode({
286
+ 'project': self.project().to_dict(), 'version': 1
287
+ })
288
+ with open(file_path, 'w', encoding="utf-8") as f:
289
+ f.write(json_obj)
290
+ self.mark_as_modified(False)
291
+ except Exception as e:
292
+ QMessageBox.critical(self.parent, "Error", f"Cannot save file:\n{str(e)}")
293
+
294
+ def check_unsaved_changes(self) -> bool:
295
+ if self.modified():
296
+ reply = QMessageBox.question(
297
+ self.parent, "Unsaved Changes",
298
+ "The project has unsaved changes. Do you want to continue?",
299
+ QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
300
+ )
301
+ if reply == QMessageBox.Save:
302
+ self.save_project()
303
+ return True
304
+ return reply == QMessageBox.Discard
305
+ return True
306
+
307
+ def on_job_edit(self, item):
308
+ index = self.job_list().row(item)
309
+ if 0 <= index < self.num_project_jobs():
310
+ job = self.project_job(index)
311
+ dialog = self.action_config_dialog(job)
312
+ if dialog.exec() == QDialog.Accepted:
313
+ current_row = self.current_job_index()
314
+ if current_row >= 0:
315
+ self.job_list_item(current_row).setText(job.params['name'])
316
+ self.refresh_ui()
317
+
318
+ def on_action_edit(self, item):
319
+ job_index = self.current_job_index()
320
+ if 0 <= job_index < self.num_project_jobs():
321
+ job = self.project_job(job_index)
322
+ action_index = self.action_list().row(item)
323
+ current_action, is_sub_action = self.get_current_action_at(job, action_index)
324
+ if current_action:
325
+ if not is_sub_action:
326
+ self.set_enabled_sub_actions_gui(
327
+ current_action.type_name == constants.ACTION_COMBO)
328
+ dialog = self.action_config_dialog(current_action)
329
+ if dialog.exec() == QDialog.Accepted:
330
+ self.on_job_selected(job_index)
331
+ self.refresh_ui()
332
+ self.set_current_job(job_index)
333
+ self.set_current_action(action_index)
334
+
335
+ def edit_current_action(self):
336
+ current_action = None
337
+ job_row = self.current_job_index()
338
+ if 0 <= job_row < self.num_project_jobs():
339
+ job = self.project_job(job_row)
340
+ if self.job_list_has_focus():
341
+ current_action = job
342
+ elif self.action_list_has_focus():
343
+ job_row, _action_row, pos = self.get_current_action()
344
+ if pos.actions is not None:
345
+ current_action = pos.action if not pos.is_sub_action else pos.sub_action
346
+ if current_action is not None:
347
+ self.edit_action(current_action)
348
+
349
+ def edit_action(self, action):
350
+ dialog = self.action_config_dialog(action)
351
+ if dialog.exec() == QDialog.Accepted:
352
+ self.on_job_selected(self.current_job_index())
353
+ self.mark_as_modified()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -29,6 +29,8 @@ Provides-Extra: dev
29
29
  Requires-Dist: pytest; extra == "dev"
30
30
  Dynamic: license-file
31
31
 
32
+ <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="150" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
33
+
32
34
  # Shine Stacker
33
35
 
34
36
  ## Focus Stacking Processing Framework and GUI
@@ -76,14 +78,6 @@ The GUI has two main working areas:
76
78
 
77
79
  The first version of the core focus stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author. The implementation in the latest releases was rewritten from the original code.
78
80
 
79
- <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="256" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
80
-
81
- ## Logo attribution
82
-
83
- The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
84
- Copyright © Alessandro Lista. All rights reserved.
85
- The logo is not covered by the LGPL-3.0 license of this project.
86
-
87
81
  # Resources
88
82
 
89
83
  * [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
@@ -92,11 +86,11 @@ Pyramid methods in image processing
92
86
 
93
87
  # License
94
88
 
95
- - **Code**: <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
96
- The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
97
- - **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
98
- Copyright © Alessandro Lista. All rights reserved.
99
- The logo is not covered by the LGPL-3.0 license of this project.
89
+ <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
90
+ - **Code**: The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
91
+ <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="150" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
92
+
93
+ - **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.
100
94
 
101
95
  # Attribution request
102
96
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
@@ -1,5 +1,5 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=dHuoY6voK7np1A7oRzw1xyBy7CK9_KBAc4FCKW29uRQ,21
2
+ shinestacker/_version.py,sha256=bMIenWosteoeUs51RbaWVZetIuzRhWymuyy-n0rfK0I,21
3
3
  shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
4
4
  shinestacker/algorithms/align.py,sha256=XT4DJoD5ZvpkC1-J3W3GWmWRsXJg3qJ-3zr9erT8oW0,17514
5
5
  shinestacker/algorithms/balance.py,sha256=iSjO-pl0vQv58iEQ077EUcDTAExMKDBdtXmJXbMhazk,16721
@@ -45,6 +45,7 @@ shinestacker/gui/gui_run.py,sha256=jGrtXNT3Gn53qMPSU74aEY6PP7UQfc85SD3XIesWLuY,1
45
45
  shinestacker/gui/main_window.py,sha256=6zGFZkgBh3_UNWjCfNzciPeiCvBsZnjJiPy62U4ldAw,23988
46
46
  shinestacker/gui/menu_manager.py,sha256=_L6LOikB3impEYqilqwXc0WJuunishjz57ozZlrBn7Q,9616
47
47
  shinestacker/gui/new_project.py,sha256=zHmGrT27L7I6YHM1L8wjt7DzukLFPddFsbVyGVHfJoc,11004
48
+ shinestacker/gui/project_controller.py,sha256=jjM8JuPlIsQohmiqzuV1EndGVvqO4tOEdoMEUJ37SNU,15049
48
49
  shinestacker/gui/project_converter.py,sha256=_AFfU2HYKPX78l6iX6bXJrlKpdjSl63pmKzrc6kQpn8,7348
49
50
  shinestacker/gui/project_editor.py,sha256=uouzmUkrqouQlq-dqPOgSO16r1WOnGNV2v8jTcZlRXU,23749
50
51
  shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
@@ -82,9 +83,9 @@ shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-
82
83
  shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
83
84
  shinestacker/retouch/vignetting_filter.py,sha256=3WuoF38lQOIaU1MWmqviItuQn8NnbMN0nwV7pM9IJqU,3453
84
85
  shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
85
- shinestacker-1.0.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
86
- shinestacker-1.0.0.dist-info/METADATA,sha256=600w-Vjn2cDAvudN_Qt4nN3qkxJLmzgNo0a5j6-Hz3E,5957
87
- shinestacker-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
88
- shinestacker-1.0.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
89
- shinestacker-1.0.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
90
- shinestacker-1.0.0.dist-info/RECORD,,
86
+ shinestacker-1.0.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
87
+ shinestacker-1.0.1.dist-info/METADATA,sha256=txWeQBvdot0jMY-eCpwlZTqMataYiQn8MpSZECcxaUw,5902
88
+ shinestacker-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
89
+ shinestacker-1.0.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
90
+ shinestacker-1.0.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
91
+ shinestacker-1.0.1.dist-info/RECORD,,