shinestacker 0.5.0__py3-none-any.whl → 1.0.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.

Files changed (57) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +4 -12
  3. shinestacker/algorithms/balance.py +11 -9
  4. shinestacker/algorithms/depth_map.py +0 -30
  5. shinestacker/algorithms/utils.py +10 -0
  6. shinestacker/algorithms/vignetting.py +116 -70
  7. shinestacker/app/about_dialog.py +37 -16
  8. shinestacker/app/gui_utils.py +1 -1
  9. shinestacker/app/help_menu.py +1 -1
  10. shinestacker/app/main.py +2 -2
  11. shinestacker/app/project.py +2 -2
  12. shinestacker/config/constants.py +4 -1
  13. shinestacker/config/gui_constants.py +3 -4
  14. shinestacker/gui/action_config.py +5 -561
  15. shinestacker/gui/action_config_dialog.py +567 -0
  16. shinestacker/gui/base_form_dialog.py +18 -0
  17. shinestacker/gui/colors.py +5 -6
  18. shinestacker/gui/gui_logging.py +0 -1
  19. shinestacker/gui/gui_run.py +54 -106
  20. shinestacker/gui/ico/shinestacker.icns +0 -0
  21. shinestacker/gui/ico/shinestacker.ico +0 -0
  22. shinestacker/gui/ico/shinestacker.png +0 -0
  23. shinestacker/gui/ico/shinestacker.svg +60 -0
  24. shinestacker/gui/main_window.py +275 -371
  25. shinestacker/gui/menu_manager.py +236 -0
  26. shinestacker/gui/new_project.py +75 -20
  27. shinestacker/gui/project_converter.py +6 -6
  28. shinestacker/gui/project_editor.py +248 -165
  29. shinestacker/gui/project_model.py +2 -7
  30. shinestacker/gui/tab_widget.py +81 -0
  31. shinestacker/gui/time_progress_bar.py +95 -0
  32. shinestacker/retouch/base_filter.py +173 -40
  33. shinestacker/retouch/brush_preview.py +0 -10
  34. shinestacker/retouch/brush_tool.py +2 -5
  35. shinestacker/retouch/denoise_filter.py +5 -44
  36. shinestacker/retouch/exif_data.py +10 -13
  37. shinestacker/retouch/file_loader.py +1 -1
  38. shinestacker/retouch/filter_manager.py +1 -4
  39. shinestacker/retouch/image_editor_ui.py +318 -40
  40. shinestacker/retouch/image_viewer.py +34 -11
  41. shinestacker/retouch/io_gui_handler.py +34 -30
  42. shinestacker/retouch/layer_collection.py +2 -0
  43. shinestacker/retouch/shortcuts_help.py +12 -0
  44. shinestacker/retouch/unsharp_mask_filter.py +10 -10
  45. shinestacker/retouch/vignetting_filter.py +69 -0
  46. shinestacker/retouch/white_balance_filter.py +46 -14
  47. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/METADATA +14 -2
  48. shinestacker-1.0.0.dist-info/RECORD +90 -0
  49. shinestacker/app/app_config.py +0 -22
  50. shinestacker/gui/actions_window.py +0 -266
  51. shinestacker/retouch/image_editor.py +0 -197
  52. shinestacker/retouch/image_filters.py +0 -69
  53. shinestacker-0.5.0.dist-info/RECORD +0 -87
  54. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
  55. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
  56. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
  57. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/top_level.txt +0 -0
@@ -6,8 +6,8 @@ from .base_filter import BaseFilter
6
6
 
7
7
 
8
8
  class UnsharpMaskFilter(BaseFilter):
9
- def __init__(self, editor):
10
- super().__init__(editor)
9
+ def __init__(self, name, editor):
10
+ super().__init__(name, editor, preview_at_startup=True)
11
11
  self.max_range = 500.0
12
12
  self.max_radius = 4.0
13
13
  self.max_amount = 3.0
@@ -46,14 +46,14 @@ class UnsharpMaskFilter(BaseFilter):
46
46
  elif name == "Threshold":
47
47
  self.threshold_slider = slider
48
48
  value_labels[name] = value_label
49
- preview_check, preview_timer, button_box = self.create_base_widgets(
50
- layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
49
+ self.create_base_widgets(
50
+ layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
51
51
 
52
52
  def update_value(name, value, max_val, fmt):
53
53
  float_value = max_val * value / self.max_range
54
54
  value_labels[name].setText(fmt.format(float_value))
55
- if preview_check.isChecked():
56
- preview_timer.start()
55
+ if self.preview_check.isChecked():
56
+ self.preview_timer.start()
57
57
 
58
58
  self.radius_slider.valueChanged.connect(
59
59
  lambda v: update_value("Radius", v, self.max_radius, params["Radius"][2]))
@@ -61,10 +61,10 @@ class UnsharpMaskFilter(BaseFilter):
61
61
  lambda v: update_value("Amount", v, self.max_amount, params["Amount"][2]))
62
62
  self.threshold_slider.valueChanged.connect(
63
63
  lambda v: update_value("Threshold", v, self.max_threshold, params["Threshold"][2]))
64
- preview_timer.timeout.connect(do_preview)
65
- self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
66
- button_box.accepted.connect(dlg.accept)
67
- button_box.rejected.connect(dlg.reject)
64
+ self.preview_timer.timeout.connect(do_preview)
65
+ self.editor.connect_preview_toggle(self.preview_check, do_preview, restore_original)
66
+ self.button_box.accepted.connect(dlg.accept)
67
+ self.button_box.rejected.connect(dlg.reject)
68
68
  QTimer.singleShot(0, do_preview)
69
69
 
70
70
  def get_params(self):
@@ -0,0 +1,69 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902
2
+ from PySide6.QtCore import Qt
3
+ from PySide6.QtWidgets import QSpinBox, QCheckBox, QLabel, QHBoxLayout, QSlider
4
+ from .. config.constants import constants
5
+ from .. algorithms.vignetting import correct_vignetting
6
+ from .base_filter import OneSliderBaseFilter
7
+
8
+
9
+ class VignettingFilter(OneSliderBaseFilter):
10
+ def __init__(self, name, editor):
11
+ super().__init__(name, editor, 1.0, 0.90, "Vignetting correction",
12
+ allow_partial_preview=False, preview_at_startup=False)
13
+ self.subsample_box = None
14
+ self.fast_subsampling_check = None
15
+ self.r_steps_box = None
16
+ self.threshold_slider = None
17
+ self.threshold_label = None
18
+ self.threshold_max_range = 500
19
+ self.threshold_max_value = 128.0
20
+ self.threshold_initial_value = constants.DEFAULT_BLACK_THRESHOLD
21
+ self.threshold_format = "{:.1f}"
22
+
23
+ def apply(self, image, strength):
24
+ return correct_vignetting(image, max_correction=strength,
25
+ black_threshold=self.threshold_slider.value(),
26
+ r_steps=self.r_steps_box.value(),
27
+ subsample=self.subsample_box.value(),
28
+ fast_subsampling=True)
29
+
30
+ def add_widgets(self, layout, dlg):
31
+ threshold_layout = QHBoxLayout()
32
+ threshold_layout.addWidget(QLabel("Threshold:"))
33
+ self.threshold_slider = QSlider(Qt.Horizontal)
34
+ self.threshold_slider.setRange(0, self.threshold_max_range)
35
+ self.threshold_slider.setValue(
36
+ int(self.threshold_initial_value /
37
+ self.threshold_max_value * self.threshold_max_range))
38
+ self.threshold_slider.valueChanged.connect(self.threshold_changed)
39
+ self.threshold_label = QLabel(self.threshold_format.format(self.threshold_initial_value))
40
+ threshold_layout.addWidget(self.threshold_slider)
41
+ threshold_layout.addWidget(self.threshold_label)
42
+ layout.addLayout(threshold_layout)
43
+ subsample_layout = QHBoxLayout()
44
+ subsample_label = QLabel("Subsample:")
45
+ self.subsample_box = QSpinBox()
46
+ self.subsample_box.setFixedWidth(50)
47
+ self.subsample_box.setRange(1, 50)
48
+ self.subsample_box.setValue(constants.DEFAULT_VIGN_SUBSAMPLE)
49
+ self.subsample_box.valueChanged.connect(self.threshold_changed)
50
+ self.fast_subsampling_check = QCheckBox("Fast subsampling")
51
+ self.fast_subsampling_check.setChecked(constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
52
+ r_steps_label = QLabel("Radial steps:")
53
+ self.r_steps_box = QSpinBox()
54
+ self.r_steps_box.setFixedWidth(50)
55
+ self.r_steps_box.setRange(1, 200)
56
+ self.r_steps_box.setValue(constants.DEFAULT_R_STEPS)
57
+ self.r_steps_box.valueChanged.connect(self.param_changed)
58
+ subsample_layout.addWidget(subsample_label)
59
+ subsample_layout.addWidget(self.subsample_box)
60
+ subsample_layout.addWidget(r_steps_label)
61
+ subsample_layout.addWidget(self.r_steps_box)
62
+ subsample_layout.addStretch(1)
63
+ layout.addLayout(subsample_layout)
64
+ layout.addWidget(self.fast_subsampling_check)
65
+
66
+ def threshold_changed(self, val):
67
+ float_val = self.threshold_max_value * float(val) / self.threshold_max_range
68
+ self.threshold_label.setText(self.threshold_format.format(float_val))
69
+ self.param_changed(val)
@@ -1,6 +1,6 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917, R0902
2
2
  from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QFrame, QVBoxLayout, QLabel, QDialog,
3
- QApplication, QSlider, QDialogButtonBox)
3
+ QApplication, QSlider, QDialogButtonBox, QLineEdit)
4
4
  from PySide6.QtCore import Qt, QTimer
5
5
  from PySide6.QtGui import QCursor
6
6
  from .. algorithms.white_balance import white_balance_from_rgb
@@ -8,12 +8,13 @@ from .base_filter import BaseFilter
8
8
 
9
9
 
10
10
  class WhiteBalanceFilter(BaseFilter):
11
- def __init__(self, editor):
12
- super().__init__(editor)
11
+ def __init__(self, name, editor):
12
+ super().__init__(name, editor, preview_at_startup=True)
13
13
  self.max_range = 255
14
14
  self.initial_val = (128, 128, 128)
15
15
  self.sliders = {}
16
16
  self.value_labels = {}
17
+ self.rgb_hex = None
17
18
  self.color_preview = None
18
19
  self.preview_timer = None
19
20
  self.original_mouse_press = None
@@ -45,29 +46,60 @@ class WhiteBalanceFilter(BaseFilter):
45
46
  self.value_labels[name] = val_label
46
47
  row_layout.addLayout(sliders_layout)
47
48
  layout.addLayout(row_layout)
49
+
50
+ rbg_layout = QHBoxLayout()
51
+ rbg_layout.addWidget(QLabel("RBG hex:"))
52
+ self.rgb_hex = QLineEdit(self.hex_color(self.initial_val))
53
+ self.rgb_hex.setFixedWidth(60)
54
+ self.rgb_hex.textChanged.connect(self.on_rgb_change)
55
+ rbg_layout.addWidget(self.rgb_hex)
56
+ rbg_layout.addStretch(1)
57
+ layout.addLayout(rbg_layout)
58
+
48
59
  pick_button = QPushButton("Pick Color")
49
60
  layout.addWidget(pick_button)
50
- preview_check, self.preview_timer, button_box = self.create_base_widgets(
61
+ self.create_base_widgets(
51
62
  layout,
52
63
  QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel,
53
- 200)
64
+ 200, dlg)
54
65
  for slider in self.sliders.values():
55
66
  slider.valueChanged.connect(self.on_slider_change)
56
67
  self.preview_timer.timeout.connect(do_preview)
57
- self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
68
+ self.editor.connect_preview_toggle(self.preview_check, do_preview, restore_original)
58
69
  pick_button.clicked.connect(self.start_color_pick)
59
- button_box.accepted.connect(dlg.accept)
60
- button_box.rejected.connect(dlg.reject)
61
- button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
70
+ self.button_box.accepted.connect(dlg.accept)
71
+ self.button_box.rejected.connect(dlg.reject)
72
+ self.button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
62
73
  QTimer.singleShot(0, do_preview)
63
74
 
75
+ def hex_color(self, val):
76
+ return "".join([f"{int(c):0>2X}" for c in val])
77
+
78
+ def apply_preview(self, rgb):
79
+ self.color_preview.setStyleSheet(f"background-color: rgb{tuple(rgb)};")
80
+ if self.preview_timer:
81
+ self.preview_timer.start()
82
+
64
83
  def on_slider_change(self):
65
84
  for name in ("R", "G", "B"):
66
85
  self.value_labels[name].setText(str(self.sliders[name].value()))
67
86
  rgb = tuple(self.sliders[n].value() for n in ("R", "G", "B"))
68
- self.color_preview.setStyleSheet(f"background-color: rgb{rgb};")
69
- if self.preview_timer:
70
- self.preview_timer.start()
87
+ self.rgb_hex.blockSignals(True)
88
+ self.rgb_hex.setText(self.hex_color(rgb))
89
+ self.rgb_hex.blockSignals(False)
90
+ self.apply_preview(rgb)
91
+
92
+ def on_rgb_change(self):
93
+ txt = self.rgb_hex.text()
94
+ if len(txt) != 6:
95
+ return
96
+ rgb = [int(txt[i:i + 2], 16) for i in range(0, 6, 2)]
97
+ for name, c in zip(("R", "G", "B"), rgb):
98
+ self.sliders[name].blockSignals(True)
99
+ self.sliders[name].setValue(c)
100
+ self.sliders[name].blockSignals(False)
101
+ self.value_labels[name].setText(str(c))
102
+ self.apply_preview(rgb)
71
103
 
72
104
  def start_color_pick(self):
73
105
  for widget in QApplication.topLevelWidgets():
@@ -94,7 +126,7 @@ class WhiteBalanceFilter(BaseFilter):
94
126
  self.editor.image_viewer.mousePressEvent = self.original_mouse_press
95
127
  self.editor.image_viewer.brush_cursor.show()
96
128
  self.editor.brush_preview.show()
97
- new_filter = WhiteBalanceFilter(self.editor)
129
+ new_filter = WhiteBalanceFilter(self.name, self.editor)
98
130
  new_filter.run_with_preview(init_val=rgb)
99
131
 
100
132
  def reset_rgb(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 0.5.0
3
+ Version: 1.0.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -41,6 +41,7 @@ Dynamic: license-file
41
41
  [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
42
42
  [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
43
43
  [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
44
+ [![PyPI Downloads](https://static.pepy.tech/badge/shinestacker)](https://pepy.tech/projects/shinestacker)
44
45
 
45
46
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
46
47
 
@@ -75,6 +76,14 @@ The GUI has two main working areas:
75
76
 
76
77
  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.
77
78
 
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
+
78
87
  # Resources
79
88
 
80
89
  * [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
@@ -83,8 +92,11 @@ Pyramid methods in image processing
83
92
 
84
93
  # License
85
94
 
86
- <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
95
+ - **Code**: <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
87
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.
88
100
 
89
101
  # Attribution request
90
102
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
@@ -0,0 +1,90 @@
1
+ shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
+ shinestacker/_version.py,sha256=dHuoY6voK7np1A7oRzw1xyBy7CK9_KBAc4FCKW29uRQ,21
3
+ shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
4
+ shinestacker/algorithms/align.py,sha256=XT4DJoD5ZvpkC1-J3W3GWmWRsXJg3qJ-3zr9erT8oW0,17514
5
+ shinestacker/algorithms/balance.py,sha256=iSjO-pl0vQv58iEQ077EUcDTAExMKDBdtXmJXbMhazk,16721
6
+ shinestacker/algorithms/base_stack_algo.py,sha256=AFV2QkcFNaTcnISpsWHuAVy2De9hhaPcBNjE1O0h50I,1430
7
+ shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
8
+ shinestacker/algorithms/depth_map.py,sha256=FOR5M0brO5-9NnXDY7TWpc3OtKKSuzrOSoBMe0cP6Ho,6076
9
+ shinestacker/algorithms/exif.py,sha256=gY9s6Cd4g4swo5qEjSbzuVIvl1GImCYu6ytOO9WrV0I,9435
10
+ shinestacker/algorithms/multilayer.py,sha256=5JA6TW8oO_R3mu6cOvPno9by4md8q5sXUb8ZfsRRpmY,9259
11
+ shinestacker/algorithms/noise_detection.py,sha256=CDnN8pglxufY5Y-dT3mVooD4zPySdSq9CMgtDGMXBnA,8970
12
+ shinestacker/algorithms/pyramid.py,sha256=_Pk19lRQ21b3W3aHQ6DgAe9VVOfbsi2a9jrynF0qFVw,8610
13
+ shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
14
+ shinestacker/algorithms/stack.py,sha256=FCU89Of-s6C_DuMleG06c8V6fnIm9MFInvkkKtTsGBo,4906
15
+ shinestacker/algorithms/stack_framework.py,sha256=frw7sbc9qOfVBYP3ZOFZEaIn9O27Wms8j_mxSW79uI0,12460
16
+ shinestacker/algorithms/utils.py,sha256=2XFa16Q8JVq4C2iXZFOvv98GVKqxceFG73yFZNHJSqI,2475
17
+ shinestacker/algorithms/vignetting.py,sha256=yW-1TF4tesLWfKQOS0XxRkOEN82U-YDmMaj09C9cH4M,9552
18
+ shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
19
+ shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
21
+ shinestacker/app/gui_utils.py,sha256=08TrCj2gFGsNsF6hG7ySO2y7wcQakM5PzERkeplqNFs,2344
22
+ shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
23
+ shinestacker/app/main.py,sha256=geZwxShTDg8dDOnsGu9TvWMw2Wru6MaX9CvlGHMnRvQ,6474
24
+ shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
25
+ shinestacker/app/project.py,sha256=W0u715LZne_PNJvg9msSy27ybIjgDXiEAQdJ7_6BjYI,2774
26
+ shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
27
+ shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
28
+ shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
29
+ shinestacker/config/constants.py,sha256=l7RcRxXlIThkBXcy2GVRBWFZQRXNgqYShNEVfDpgjEU,6096
30
+ shinestacker/config/gui_constants.py,sha256=5DR-ET1oeMMD7lIsjvAwSuln89A7I9wy9VuAeRo2G64,2575
31
+ shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
32
+ shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
33
+ shinestacker/core/core_utils.py,sha256=ulJhzen5McAb5n6wWNA_KB4U_PdTEr-H2TCQkVKUaOw,1421
34
+ shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk2c,1618
35
+ shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7Kk,7447
36
+ shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
37
+ shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ shinestacker/gui/action_config.py,sha256=yXNDv0MyONbHk4iUrkvMkLKKaDvpJyzA5Yr0Eikgo0c,16986
39
+ shinestacker/gui/action_config_dialog.py,sha256=6Xjbtj7oHGXBNiogcnPoFHqcuTOnZFlMzWCZXv8eBAI,32623
40
+ shinestacker/gui/base_form_dialog.py,sha256=yYqMee1mzw9VBx8siBS0jDk1qqsTIKJUgdjh92aprQk,687
41
+ shinestacker/gui/colors.py,sha256=m0pQQ-uvtIN1xmb_-N06BvC7pZYZZnq59ZSEJwutHuk,1432
42
+ shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
43
+ shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
44
+ shinestacker/gui/gui_run.py,sha256=jGrtXNT3Gn53qMPSU74aEY6PP7UQfc85SD3XIesWLuY,15147
45
+ shinestacker/gui/main_window.py,sha256=6zGFZkgBh3_UNWjCfNzciPeiCvBsZnjJiPy62U4ldAw,23988
46
+ shinestacker/gui/menu_manager.py,sha256=_L6LOikB3impEYqilqwXc0WJuunishjz57ozZlrBn7Q,9616
47
+ shinestacker/gui/new_project.py,sha256=zHmGrT27L7I6YHM1L8wjt7DzukLFPddFsbVyGVHfJoc,11004
48
+ shinestacker/gui/project_converter.py,sha256=_AFfU2HYKPX78l6iX6bXJrlKpdjSl63pmKzrc6kQpn8,7348
49
+ shinestacker/gui/project_editor.py,sha256=uouzmUkrqouQlq-dqPOgSO16r1WOnGNV2v8jTcZlRXU,23749
50
+ shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
51
+ shinestacker/gui/select_path_widget.py,sha256=OfQImOmkzbvl5BBshmb7ePWrSGDJQ8VvyaAOypHAGd4,1023
52
+ shinestacker/gui/tab_widget.py,sha256=6iUifK-wu0EzjVFccKHirhA2fENglVi6xREKiD96aaY,2950
53
+ shinestacker/gui/time_progress_bar.py,sha256=Ty7pNTfbKU44Y_0YQNYtgEcxpOD-Bbi4lC8g-u9bno0,3012
54
+ shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
55
+ shinestacker/gui/ico/shinestacker.icns,sha256=3IshIOv0uFexYsAEPkE9xiyuw8mB5X5gffekOUhFlt0,45278
56
+ shinestacker/gui/ico/shinestacker.ico,sha256=8IMRk-toObWUz8iDXA-zHBWQ8Ps3vXN5u5ZEyw7sP3c,109613
57
+ shinestacker/gui/ico/shinestacker.png,sha256=VybGY-nig_-wMW8g_uImGxRYvcnltWcAVEPSX6AZUHM,22448
58
+ shinestacker/gui/ico/shinestacker.svg,sha256=r8jx5aiIT9K70MRP0ANWniFE0ctRCqH7LMQ7vcGJ8ss,6571
59
+ shinestacker/gui/img/close-round-line-icon.png,sha256=9HZwCjgni1s_JGUPUb_MoOfoe4tRZgM5OWzk92XFZlE,8019
60
+ shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzdsHvmDAjlbE18Pgo,4788
61
+ shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
62
+ shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
63
+ shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
+ shinestacker/retouch/base_filter.py,sha256=7UfuhZV3wEZefQbExVIHJ6IqKnXlMF2MelxswTMaqOo,10356
65
+ shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
66
+ shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
67
+ shinestacker/retouch/brush_preview.py,sha256=QKD3pL7n7YJbIibinUFYKv7lkyq_AWLpt6oyqnltrwQ,4893
68
+ shinestacker/retouch/brush_tool.py,sha256=nxnEuvTioPNw1WeWsT20X1zl-LNZ8i-1ExOcihikEjk,8618
69
+ shinestacker/retouch/denoise_filter.py,sha256=TDUHzhRKlKvCa3D5SCYCZKTpjcl81kGwmONsgSDtO1k,440
70
+ shinestacker/retouch/display_manager.py,sha256=XPbOBmoYc_jNA791WkWkOSaFHb0ztCZechl2p2KSlwQ,9597
71
+ shinestacker/retouch/exif_data.py,sha256=giqoIaMlhN6H3x8BAd73ghVHODmWIcD_QbSoDymQycU,1864
72
+ shinestacker/retouch/file_loader.py,sha256=x8lzKShlgElcJYLFiDoeszeVEToUUiUbUrozAeF5SMU,4812
73
+ shinestacker/retouch/filter_manager.py,sha256=SdYIZkZBUvuB6wDG0moGWav5sfEvIcB9ioUJR5wJFts,388
74
+ shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
75
+ shinestacker/retouch/image_editor_ui.py,sha256=GNozK_P7orgl8EioemJeyR4e_LLMbKVZi4xhYW9m38U,29893
76
+ shinestacker/retouch/image_viewer.py,sha256=3ebrLHTDtGd_EbIT2nNFRUjH836rblmmK7jZ62YcJ2U,19564
77
+ shinestacker/retouch/io_gui_handler.py,sha256=M9rlfqiIaP1wWCm6p-Ew15w0_pH8-DoBXJdEkTUeVCU,11525
78
+ shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
79
+ shinestacker/retouch/layer_collection.py,sha256=Q7zoCYRn__jYkfrEC2lY1uKHWfOUbsJ27xaYHIoKVxo,5730
80
+ shinestacker/retouch/shortcuts_help.py,sha256=SN4vNa_6yRAFaWxt5HpWn8FHgwmHrIs_wYwjl4iyDmg,3769
81
+ shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
82
+ shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
83
+ shinestacker/retouch/vignetting_filter.py,sha256=3WuoF38lQOIaU1MWmqviItuQn8NnbMN0nwV7pM9IJqU,3453
84
+ 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,,
@@ -1,22 +0,0 @@
1
- # pylint: disable=C0114, C0115, C0116, C0103, W0201
2
- from .. config.config import _ConfigBase
3
-
4
-
5
- class _AppConfig(_ConfigBase):
6
- def __new__(cls):
7
- return _ConfigBase.__new__(cls)
8
-
9
- def _init_defaults(self):
10
- self._DONT_USE_NATIVE_MENU = True
11
- self._COMBINED_APP = False
12
-
13
- @property
14
- def DONT_USE_NATIVE_MENU(self):
15
- return self._DONT_USE_NATIVE_MENU
16
-
17
- @property
18
- def COMBINED_APP(self):
19
- return self._COMBINED_APP
20
-
21
-
22
- app_config = _AppConfig()
@@ -1,266 +0,0 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0914, R0912, R0915, W0718
2
- import os.path
3
- import os
4
- # import traceback
5
- import json
6
- import jsonpickle
7
- from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
8
- from .. core.core_utils import get_app_base_path
9
- from .. config.constants import constants
10
- from .project_model import ActionConfig
11
- from .project_editor import ProjectEditor
12
- from .new_project import NewProjectDialog
13
- from .project_model import Project
14
-
15
-
16
- class ActionsWindow(ProjectEditor):
17
- def __init__(self):
18
- super().__init__()
19
- self.update_title()
20
-
21
- def update_title(self):
22
- title = constants.APP_TITLE
23
- file_name = self.current_file_name()
24
- if file_name:
25
- title += f" - {file_name}"
26
- if self._modified_project:
27
- title += " *"
28
- self.window().setWindowTitle(title)
29
-
30
- def mark_as_modified(self):
31
- self._modified_project = True
32
- self.project_buffer.append(self.project.clone())
33
- self.save_actions_set_enabled(True)
34
- self.update_title()
35
-
36
- def close_project(self):
37
- if self._check_unsaved_changes():
38
- self.set_project(Project())
39
- self.set_current_file_path('')
40
- self.update_title()
41
- self.job_list.clear()
42
- self.action_list.clear()
43
- self._modified_project = False
44
- self.save_actions_set_enabled(False)
45
-
46
- def new_project(self):
47
- if not self._check_unsaved_changes():
48
- return
49
- os.chdir(get_app_base_path())
50
- self.set_current_file_path('')
51
- self._modified_project = False
52
- self.update_title()
53
- self.job_list.clear()
54
- self.action_list.clear()
55
- self.set_project(Project())
56
- self.save_actions_set_enabled(False)
57
- dialog = NewProjectDialog(self)
58
- if dialog.exec() == QDialog.Accepted:
59
- self.save_actions_set_enabled(True)
60
- input_folder = dialog.get_input_folder().split('/')
61
- working_path = '/'.join(input_folder[:-1])
62
- input_path = input_folder[-1]
63
- if dialog.get_noise_detection():
64
- job_noise = ActionConfig(constants.ACTION_JOB,
65
- {'name': 'detect-noise', 'working_path': working_path,
66
- 'input_path': input_path})
67
- noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
68
- {'name': 'detect-noise'})
69
- job_noise.add_sub_action(noise_detection)
70
- self.project.jobs.append(job_noise)
71
- job = ActionConfig(constants.ACTION_JOB,
72
- {'name': 'focus-stack', 'working_path': working_path,
73
- 'input_path': input_path})
74
- if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
75
- dialog.get_align_frames() or dialog.get_balance_frames():
76
- combo_action = ActionConfig(constants.ACTION_COMBO, {'name': 'align'})
77
- if dialog.get_noise_detection():
78
- mask_noise = ActionConfig(constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
79
- combo_action.add_sub_action(mask_noise)
80
- if dialog.get_vignetting_correction():
81
- vignetting = ActionConfig(constants.ACTION_VIGNETTING, {'name': 'vignetting'})
82
- combo_action.add_sub_action(vignetting)
83
- if dialog.get_align_frames():
84
- align = ActionConfig(constants.ACTION_ALIGNFRAMES, {'name': 'align'})
85
- combo_action.add_sub_action(align)
86
- if dialog.get_balance_frames():
87
- balance = ActionConfig(constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
88
- combo_action.add_sub_action(balance)
89
- job.add_sub_action(combo_action)
90
- if dialog.get_bunch_stack():
91
- bunch_stack = ActionConfig(constants.ACTION_FOCUSSTACKBUNCH,
92
- {'name': 'bunches', 'frames': dialog.get_bunch_frames(),
93
- 'overlap': dialog.get_bunch_overlap()})
94
- job.add_sub_action(bunch_stack)
95
- if dialog.get_focus_stack_pyramid():
96
- focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
97
- {'name': 'focus-stack-pyramid',
98
- 'stacker': constants.STACK_ALGO_PYRAMID})
99
- job.add_sub_action(focus_pyramid)
100
- if dialog.get_focus_stack_depth_map():
101
- focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
102
- {'name': 'focus-stack-depth-map',
103
- 'stacker': constants.STACK_ALGO_DEPTH_MAP})
104
- job.add_sub_action(focus_depth_map)
105
- if dialog.get_multi_layer():
106
- input_path = []
107
- if dialog.get_focus_stack_pyramid():
108
- input_path.append("focus-stack-pyramid")
109
- if dialog.get_focus_stack_depth_map():
110
- input_path.append("focus-stack-depth-map")
111
- if dialog.get_bunch_stack():
112
- input_path.append("bunches")
113
- else:
114
- input_path.append(input_path)
115
- multi_layer = ActionConfig(constants.ACTION_MULTILAYER,
116
- {'name': 'multi-layer',
117
- 'input_path': ','.join(input_path)})
118
- job.add_sub_action(multi_layer)
119
- self.project.jobs.append(job)
120
- self._modified_project = True
121
- self.refresh_ui(0, -1)
122
-
123
- def open_project(self, file_path=False):
124
- if not self._check_unsaved_changes():
125
- return
126
- if file_path is False:
127
- file_path, _ = QFileDialog.getOpenFileName(
128
- self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
129
- if file_path:
130
- try:
131
- self.set_current_file_path(file_path)
132
- with open(self.current_file_path(), 'r', encoding="utf-8") as file:
133
- json_obj = json.load(file)
134
- project = Project.from_dict(json_obj['project'])
135
- if project is None:
136
- raise RuntimeError(f"Project from file {file_path} produced a null project.")
137
- self.set_project(project)
138
- self._modified_project = False
139
- self.update_title()
140
- self.refresh_ui(0, -1)
141
- if self.job_list.count() > 0:
142
- self.job_list.setCurrentRow(0)
143
- except Exception as e:
144
- # traceback.print_tb(e.__traceback__)
145
- QMessageBox.critical(self, "Error", f"Cannot open file {file_path}:\n{str(e)}")
146
- if len(self.project.jobs) > 0:
147
- self.job_list.setCurrentRow(0)
148
- self.activateWindow()
149
- self.save_actions_set_enabled(True)
150
- for job in self.project.jobs:
151
- if 'working_path' in job.params.keys():
152
- working_path = job.params['working_path']
153
- if not os.path.isdir(working_path):
154
- QMessageBox.warning(
155
- self, "Working path not found",
156
- f'''The working path specified in the project file for the job:
157
- "{job.params['name']}"
158
- was not found.\n
159
- Please, select a valid working path.''')
160
- self.edit_action(job)
161
- for action in job.sub_actions:
162
- if 'working_path' in job.params.keys():
163
- working_path = job.params['working_path']
164
- if working_path != '' and not os.path.isdir(working_path):
165
- QMessageBox.warning(
166
- self, "Working path not found",
167
- f'''The working path specified in the project file for the job:
168
- "{job.params['name']}"
169
- was not found.\n
170
- Please, select a valid working path.''')
171
- self.edit_action(action)
172
-
173
- def save_project(self):
174
- path = self.current_file_path()
175
- if path:
176
- self.do_save(path)
177
- else:
178
- self.save_project_as()
179
-
180
- def save_project_as(self):
181
- file_path, _ = QFileDialog.getSaveFileName(
182
- self, "Save Project As", "", "Project Files (*.fsp);;All Files (*)")
183
- if file_path:
184
- if not file_path.endswith('.fsp'):
185
- file_path += '.fsp'
186
- self.do_save(file_path)
187
- self.set_current_file_path(file_path)
188
- self._modified_project = False
189
- self.update_title()
190
- os.chdir(os.path.dirname(file_path))
191
-
192
- def do_save(self, file_path):
193
- try:
194
- json_obj = jsonpickle.encode({
195
- 'project': self.project.to_dict(),
196
- 'version': 1
197
- })
198
- with open(file_path, 'w', encoding="utf-8") as f:
199
- f.write(json_obj)
200
- self._modified_project = False
201
- except Exception as e:
202
- QMessageBox.critical(self, "Error", f"Cannot save file:\n{str(e)}")
203
-
204
- def _check_unsaved_changes(self) -> bool:
205
- if self._modified_project:
206
- reply = QMessageBox.question(
207
- self, "Unsaved Changes",
208
- "The project has unsaved changes. Do you want to continue?",
209
- QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
210
- )
211
- if reply == QMessageBox.Save:
212
- self.save_project()
213
- return True
214
- return reply == QMessageBox.Discard
215
- return True
216
-
217
- def on_job_edit(self, item):
218
- index = self.job_list.row(item)
219
- if 0 <= index < len(self.project.jobs):
220
- job = self.project.jobs[index]
221
- dialog = self.action_config_dialog(job)
222
- if dialog.exec() == QDialog.Accepted:
223
- current_row = self.job_list.currentRow()
224
- if current_row >= 0:
225
- self.job_list.item(current_row).setText(job.params['name'])
226
- self.refresh_ui()
227
-
228
- def on_action_edit(self, item):
229
- job_index = self.job_list.currentRow()
230
- if 0 <= job_index < len(self.project.jobs):
231
- job = self.project.jobs[job_index]
232
- action_index = self.action_list.row(item)
233
- current_action, is_sub_action = self.get_current_action_at(job, action_index)
234
- if current_action:
235
- if not is_sub_action:
236
- self.set_enabled_sub_actions_gui(
237
- current_action.type_name == constants.ACTION_COMBO)
238
- dialog = self.action_config_dialog(current_action)
239
- if dialog.exec() == QDialog.Accepted:
240
- self.on_job_selected(job_index)
241
- self.refresh_ui()
242
- self.job_list.setCurrentRow(job_index)
243
- self.action_list.setCurrentRow(action_index)
244
-
245
- def edit_current_action(self):
246
- current_action = None
247
- job_row = self.job_list.currentRow()
248
- if 0 <= job_row < len(self.project.jobs):
249
- job = self.project.jobs[job_row]
250
- if self.job_list.hasFocus():
251
- current_action = job
252
- elif self.action_list.hasFocus():
253
- job_row, _action_row, pos = self.get_current_action()
254
- if pos.actions is not None:
255
- current_action = pos.action if not pos.is_sub_action else pos.sub_action
256
- if current_action is not None:
257
- self.edit_action(current_action)
258
-
259
- def edit_action(self, action):
260
- dialog = self.action_config_dialog(action)
261
- if dialog.exec() == QDialog.Accepted:
262
- self.on_job_selected(self.job_list.currentRow())
263
- self.mark_as_modified()
264
-
265
- def save_actions_set_enabled(self, enabled):
266
- pass