shinestacker 0.2.0.post1.dev1__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 (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,356 @@
1
+ import math
2
+ from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
3
+ from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence, QRadialGradient
4
+ from PySide6.QtCore import Qt, QRectF, QTime, QPoint, Signal
5
+ from .. config.gui_constants import gui_constants
6
+ from .brush_preview import BrushPreviewItem
7
+
8
+
9
+ def create_brush_gradient(center_x, center_y, radius, hardness, inner_color=None, outer_color=None, opacity=100):
10
+ gradient = QRadialGradient(center_x, center_y, float(radius))
11
+ inner = inner_color if inner_color is not None else QColor(*gui_constants.BRUSH_COLORS['inner'])
12
+ outer = outer_color if outer_color is not None else QColor(*gui_constants.BRUSH_COLORS['gradient_end'])
13
+ inner_with_opacity = QColor(inner)
14
+ inner_with_opacity.setAlpha(int(float(inner.alpha()) * float(opacity) / 100.0))
15
+ if hardness < 100:
16
+ hardness_normalized = float(hardness) / 100.0
17
+ gradient.setColorAt(0.0, inner_with_opacity)
18
+ gradient.setColorAt(hardness_normalized, inner_with_opacity)
19
+ gradient.setColorAt(1.0, outer)
20
+ else:
21
+ gradient.setColorAt(0.0, inner_with_opacity)
22
+ gradient.setColorAt(1.0, inner_with_opacity)
23
+ return gradient
24
+
25
+
26
+ class ImageViewer(QGraphicsView):
27
+ temp_view_requested = Signal(bool)
28
+
29
+ def __init__(self, parent=None):
30
+ super().__init__(parent)
31
+ self.image_editor = None
32
+ self.brush = None
33
+ self.cursor_style = gui_constants.DEFAULT_CURSOR_STYLE
34
+ self.scene = QGraphicsScene(self)
35
+ self.setScene(self.scene)
36
+ self.pixmap_item = QGraphicsPixmapItem()
37
+ self.scene.addItem(self.pixmap_item)
38
+ self.pixmap_item.setPixmap(QPixmap())
39
+ self.scene.setBackgroundBrush(QBrush(QColor(120, 120, 120)))
40
+ self.zoom_factor = 1.0
41
+ self.min_scale = 0.0
42
+ self.max_scale = 0.0
43
+ self.last_mouse_pos = None
44
+ self.setRenderHint(QPainter.Antialiasing)
45
+ self.setRenderHint(QPainter.SmoothPixmapTransform)
46
+ self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
47
+ self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
48
+ self.setDragMode(QGraphicsView.ScrollHandDrag)
49
+ self.brush_cursor = None
50
+ self.setMouseTracking(True)
51
+ self.space_pressed = False
52
+ self.control_pressed = False
53
+ self.setDragMode(QGraphicsView.NoDrag)
54
+ self.scrolling = False
55
+ self.dragging = False
56
+ self.last_update_time = QTime.currentTime()
57
+ self.brush_preview = BrushPreviewItem()
58
+ self.scene.addItem(self.brush_preview)
59
+ self.empty = True
60
+
61
+ def set_image(self, qimage):
62
+ pixmap = QPixmap.fromImage(qimage)
63
+ self.pixmap_item.setPixmap(pixmap)
64
+ self.setSceneRect(QRectF(pixmap.rect()))
65
+ img_width = pixmap.width()
66
+ self.min_scale = gui_constants.MIN_ZOOMED_IMG_WIDTH / img_width
67
+ self.max_scale = gui_constants.MAX_ZOOMED_IMG_PX_SIZE
68
+ if self.zoom_factor == 1.0:
69
+ self.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
70
+ self.zoom_factor = self.get_current_scale()
71
+ self.zoom_factor = max(self.min_scale, min(self.max_scale, self.zoom_factor))
72
+ self.resetTransform()
73
+ self.scale(self.zoom_factor, self.zoom_factor)
74
+ self.empty = False
75
+ self.setFocus()
76
+ self.activateWindow()
77
+
78
+ def clear_image(self):
79
+ self.scene.clear()
80
+ self.pixmap_item = QGraphicsPixmapItem()
81
+ self.scene.addItem(self.pixmap_item)
82
+ self.zoom_factor = 1.0
83
+ self.setup_brush_cursor()
84
+ self.brush_preview = BrushPreviewItem()
85
+ self.scene.addItem(self.brush_preview)
86
+ self.setCursor(Qt.ArrowCursor)
87
+ self.brush_cursor.hide()
88
+ self.empty = True
89
+
90
+ def keyPressEvent(self, event):
91
+ if self.empty:
92
+ return
93
+ if event.key() == Qt.Key_Space and not self.scrolling:
94
+ self.space_pressed = True
95
+ self.setCursor(Qt.OpenHandCursor)
96
+ if self.brush_cursor:
97
+ self.brush_cursor.hide()
98
+ elif event.key() == Qt.Key_X:
99
+ self.temp_view_requested.emit(True)
100
+ self.update_brush_cursor()
101
+ return
102
+ if event.key() == Qt.Key_Control and not self.scrolling:
103
+ self.control_pressed = True
104
+ super().keyPressEvent(event)
105
+
106
+ def keyReleaseEvent(self, event):
107
+ if self.empty:
108
+ return
109
+ self.update_brush_cursor()
110
+ if event.key() == Qt.Key_Space:
111
+ self.space_pressed = False
112
+ if not self.scrolling:
113
+ self.setCursor(Qt.BlankCursor)
114
+ if self.brush_cursor:
115
+ self.brush_cursor.show()
116
+ elif event.key() == Qt.Key_X:
117
+ self.temp_view_requested.emit(False)
118
+ return
119
+ if event.key() == Qt.Key_Control:
120
+ self.control_pressed = False
121
+ super().keyReleaseEvent(event)
122
+
123
+ def mousePressEvent(self, event):
124
+ if self.empty:
125
+ return
126
+ if event.button() == Qt.LeftButton and self.image_editor.master_layer is not None:
127
+ if self.space_pressed:
128
+ self.scrolling = True
129
+ self.last_mouse_pos = event.position()
130
+ self.setCursor(Qt.ClosedHandCursor)
131
+ if self.brush_cursor:
132
+ self.brush_cursor.hide()
133
+ else:
134
+ self.last_brush_pos = event.position()
135
+ self.image_editor.begin_copy_brush_area(event.position().toPoint())
136
+ self.dragging = True
137
+ if self.brush_cursor:
138
+ self.brush_cursor.show()
139
+ super().mousePressEvent(event)
140
+
141
+ def mouseMoveEvent(self, event):
142
+ if self.empty:
143
+ return
144
+ position = event.position()
145
+ brush_size = self.brush.size
146
+ if not self.space_pressed:
147
+ self.update_brush_cursor()
148
+ if self.dragging and event.buttons() & Qt.LeftButton:
149
+ current_time = QTime.currentTime()
150
+ if self.last_update_time.msecsTo(current_time) >= gui_constants.PAINT_REFRESH_TIMER:
151
+ min_step = brush_size * gui_constants.MIN_MOUSE_STEP_BRUSH_FRACTION * self.zoom_factor
152
+ x, y = position.x(), position.y()
153
+ xp, yp = self.last_brush_pos.x(), self.last_brush_pos.y()
154
+ distance = math.sqrt((x - xp)**2 + (y - yp)**2)
155
+ n_steps = int(float(distance) / min_step)
156
+ if n_steps > 0:
157
+ delta_x = (position.x() - self.last_brush_pos.x()) / n_steps
158
+ delta_y = (position.y() - self.last_brush_pos.y()) / n_steps
159
+ for i in range(0, n_steps + 1):
160
+ pos = QPoint(self.last_brush_pos.x() + i * delta_x,
161
+ self.last_brush_pos.y() + i * delta_y)
162
+ self.image_editor.continue_copy_brush_area(pos)
163
+ self.last_brush_pos = position
164
+ self.last_update_time = current_time
165
+ if self.scrolling and event.buttons() & Qt.LeftButton:
166
+ if self.space_pressed:
167
+ self.setCursor(Qt.ClosedHandCursor)
168
+ if self.brush_cursor:
169
+ self.brush_cursor.hide()
170
+ delta = position - self.last_mouse_pos
171
+ self.last_mouse_pos = position
172
+ self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x())
173
+ self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y())
174
+ else:
175
+ super().mouseMoveEvent(event)
176
+
177
+ def mouseReleaseEvent(self, event):
178
+ if self.empty:
179
+ return
180
+ if self.space_pressed:
181
+ self.setCursor(Qt.OpenHandCursor)
182
+ if self.brush_cursor:
183
+ self.brush_cursor.hide()
184
+ else:
185
+ self.setCursor(Qt.BlankCursor)
186
+ if self.brush_cursor:
187
+ self.brush_cursor.show()
188
+ if event.button() == Qt.LeftButton:
189
+ if self.scrolling:
190
+ self.scrolling = False
191
+ self.last_mouse_pos = None
192
+ elif hasattr(self, 'dragging') and self.dragging:
193
+ self.dragging = False
194
+ self.image_editor.end_copy_brush_area()
195
+ super().mouseReleaseEvent(event)
196
+
197
+ def wheelEvent(self, event):
198
+ if self.empty:
199
+ return
200
+ if self.control_pressed:
201
+ if event.angleDelta().y() > 0:
202
+ self.image_editor.decrease_brush_size()
203
+ else:
204
+ self.image_editor.increase_brush_size()
205
+ else:
206
+ zoom_in_factor = 1.10
207
+ zoom_out_factor = 1 / zoom_in_factor
208
+ current_scale = self.get_current_scale()
209
+ if event.angleDelta().y() > 0: # Zoom in
210
+ new_scale = current_scale * zoom_in_factor
211
+ if new_scale <= self.max_scale:
212
+ self.scale(zoom_in_factor, zoom_in_factor)
213
+ self.zoom_factor = new_scale
214
+ else: # Zoom out
215
+ new_scale = current_scale * zoom_out_factor
216
+ if new_scale >= self.min_scale:
217
+ self.scale(zoom_out_factor, zoom_out_factor)
218
+ self.zoom_factor = new_scale
219
+ self.update_brush_cursor()
220
+
221
+ def setup_brush_cursor(self):
222
+ self.setCursor(Qt.BlankCursor)
223
+ pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
224
+ brush = QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner']))
225
+ self.brush_cursor = self.scene.addEllipse(0, 0, self.brush.size, self.brush.size, pen, brush)
226
+ self.brush_cursor.setZValue(1000)
227
+ self.brush_cursor.hide()
228
+
229
+ def update_brush_cursor(self):
230
+ if self.empty:
231
+ return
232
+ if not self.brush_cursor or not self.isVisible():
233
+ return
234
+ size = self.brush.size
235
+ mouse_pos = self.mapFromGlobal(QCursor.pos())
236
+ if not self.rect().contains(mouse_pos):
237
+ self.brush_cursor.hide()
238
+ return
239
+ scene_pos = self.mapToScene(mouse_pos)
240
+ center_x = scene_pos.x()
241
+ center_y = scene_pos.y()
242
+ radius = size / 2
243
+ self.brush_cursor.setRect(center_x - radius, center_y - radius, size, size)
244
+ allow_cursor_preview = self.image_editor.allow_cursor_preview()
245
+ if self.cursor_style == 'preview' and allow_cursor_preview:
246
+ self._setup_outline_style()
247
+ self.brush_cursor.hide()
248
+ self.brush_preview.update(self.image_editor, QCursor.pos(), int(size))
249
+ else:
250
+ self.brush_preview.hide()
251
+ if self.cursor_style == 'outline' or not allow_cursor_preview:
252
+ self._setup_outline_style()
253
+ else:
254
+ self._setup_simple_brush_style(center_x, center_y, radius)
255
+ if not self.brush_cursor.isVisible():
256
+ self.brush_cursor.show()
257
+
258
+ def _setup_outline_style(self):
259
+ self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
260
+ gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor))
261
+ self.brush_cursor.setBrush(Qt.NoBrush)
262
+
263
+ def _setup_simple_brush_style(self, center_x, center_y, radius):
264
+ gradient = create_brush_gradient(
265
+ center_x, center_y, radius,
266
+ self.brush.hardness,
267
+ inner_color=QColor(*gui_constants.BRUSH_COLORS['inner']),
268
+ outer_color=QColor(*gui_constants.BRUSH_COLORS['gradient_end']),
269
+ opacity=self.brush.opacity
270
+ )
271
+ self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
272
+ gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor))
273
+ self.brush_cursor.setBrush(QBrush(gradient))
274
+
275
+ def enterEvent(self, event):
276
+ self.activateWindow()
277
+ self.setFocus()
278
+ if not self.empty:
279
+ self.setCursor(Qt.BlankCursor)
280
+ if self.brush_cursor:
281
+ self.brush_cursor.show()
282
+ super().enterEvent(event)
283
+
284
+ def leaveEvent(self, event):
285
+ if not self.empty:
286
+ self.setCursor(Qt.ArrowCursor)
287
+ if self.brush_cursor:
288
+ self.brush_cursor.hide()
289
+ super().leaveEvent(event)
290
+
291
+ def setup_shortcuts(self):
292
+ prev_layer = QShortcut(QKeySequence(Qt.Key_Up), self, context=Qt.ApplicationShortcut)
293
+ prev_layer.activated.connect(self.prev_layer)
294
+ next_layer = QShortcut(QKeySequence(Qt.Key_Down), self, context=Qt.ApplicationShortcut)
295
+ next_layer.activated.connect(self.next_layer)
296
+
297
+ def zoom_in(self):
298
+ if self.empty:
299
+ return
300
+ current_scale = self.get_current_scale()
301
+ new_scale = current_scale * gui_constants.ZOOM_IN_FACTOR
302
+ if new_scale <= self.max_scale:
303
+ self.scale(gui_constants.ZOOM_IN_FACTOR, gui_constants.ZOOM_IN_FACTOR)
304
+ self.zoom_factor = new_scale
305
+ self.update_brush_cursor()
306
+
307
+ def zoom_out(self):
308
+ if self.empty:
309
+ return
310
+ current_scale = self.get_current_scale()
311
+ new_scale = current_scale * gui_constants.ZOOM_OUT_FACTOR
312
+ if new_scale >= self.min_scale:
313
+ self.scale(gui_constants.ZOOM_OUT_FACTOR, gui_constants.ZOOM_OUT_FACTOR)
314
+ self.zoom_factor = new_scale
315
+ self.update_brush_cursor()
316
+
317
+ def reset_zoom(self):
318
+ if self.empty:
319
+ return
320
+ self.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
321
+ self.zoom_factor = self.get_current_scale()
322
+ self.zoom_factor = max(self.min_scale, min(self.max_scale, self.zoom_factor))
323
+ self.resetTransform()
324
+ self.scale(self.zoom_factor, self.zoom_factor)
325
+ self.update_brush_cursor()
326
+
327
+ def actual_size(self):
328
+ if self.empty:
329
+ return
330
+ self.zoom_factor = max(self.min_scale, min(self.max_scale, 1.0))
331
+ self.resetTransform()
332
+ self.scale(self.zoom_factor, self.zoom_factor)
333
+ self.update_brush_cursor()
334
+
335
+ def get_current_scale(self):
336
+ return self.transform().m11()
337
+
338
+ def get_view_state(self):
339
+ return {
340
+ 'zoom': self.zoom_factor,
341
+ 'h_scroll': self.horizontalScrollBar().value(),
342
+ 'v_scroll': self.verticalScrollBar().value()
343
+ }
344
+
345
+ def set_view_state(self, state):
346
+ if state:
347
+ self.resetTransform()
348
+ self.scale(state['zoom'], state['zoom'])
349
+ self.horizontalScrollBar().setValue(state['h_scroll'])
350
+ self.verticalScrollBar().setValue(state['v_scroll'])
351
+ self.zoom_factor = state['zoom']
352
+
353
+ def set_cursor_style(self, style):
354
+ self.cursor_style = style
355
+ if self.brush_cursor:
356
+ self.update_brush_cursor()
@@ -0,0 +1,98 @@
1
+ import os
2
+ from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton, QDialog,
3
+ QLabel, QVBoxLayout, QWidget)
4
+ from PySide6.QtGui import QIcon
5
+ from PySide6.QtCore import Qt
6
+ from .. core.core_utils import get_app_base_path
7
+
8
+
9
+ class ShortcutsHelp(QDialog):
10
+ def __init__(self, parent=None):
11
+ super().__init__(parent)
12
+ self.setWindowTitle("Shortcut Help")
13
+ self.resize(600, self.height())
14
+ self.layout = QVBoxLayout(self)
15
+ main_widget = QWidget()
16
+ main_layout = QHBoxLayout(main_widget)
17
+ main_layout.setContentsMargins(0, 0, 0, 0)
18
+ left_column = QWidget()
19
+ left_layout = QFormLayout(left_column)
20
+ left_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
21
+ left_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
22
+ left_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
23
+ left_layout.setLabelAlignment(Qt.AlignLeft)
24
+ right_column = QWidget()
25
+ right_layout = QFormLayout(right_column)
26
+ right_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
27
+ right_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
28
+ right_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
29
+ right_layout.setLabelAlignment(Qt.AlignLeft)
30
+ main_layout.addWidget(left_column)
31
+ main_layout.addWidget(right_column)
32
+ self.layout.addWidget(main_widget)
33
+ self.create_form(left_layout, right_layout)
34
+ button_box = QHBoxLayout()
35
+ ok_button = QPushButton("OK")
36
+ ok_button.setFocus()
37
+ button_box.addWidget(ok_button)
38
+ self.layout.addLayout(button_box)
39
+ ok_button.clicked.connect(self.accept)
40
+
41
+ def add_bold_label(self, layout, label):
42
+ label = QLabel(label)
43
+ label.setStyleSheet("font-weight: bold")
44
+ layout.addRow(label)
45
+
46
+ def create_form(self, left_layout, right_layout):
47
+ icon_path = f'{get_app_base_path()}'
48
+ if os.path.exists(f'{icon_path}/ico'):
49
+ icon_path = f'{icon_path}/ico'
50
+ else:
51
+ icon_path = f'{icon_path}/../ico'
52
+ icon_path = f'{icon_path}/shinestacker.png'
53
+ app_icon = QIcon(icon_path)
54
+ icon_pixmap = app_icon.pixmap(128, 128)
55
+ icon_label = QLabel()
56
+ icon_label.setPixmap(icon_pixmap)
57
+ icon_label.setAlignment(Qt.AlignCenter)
58
+ icon_container = QWidget()
59
+ icon_container_layout = QHBoxLayout(icon_container)
60
+ icon_container_layout.addWidget(icon_label)
61
+ icon_container_layout.setAlignment(Qt.AlignCenter)
62
+ self.layout.insertWidget(0, icon_container)
63
+
64
+ shortcuts = {
65
+ "M": "show master layer",
66
+ "L": "show selected layer",
67
+ "X": "temp. toggle between master and source layer",
68
+ "↑": "select one layer up",
69
+ "↓": "selcet one layer down",
70
+ "Ctrl + O": "open file",
71
+ "Ctrl + S": "save multilayer tiff",
72
+ "Crtl + Z": "undo brush draw",
73
+ "Ctrl + M": "copy selected layer to master",
74
+ "Ctrl + Cmd + F": "full screen mode",
75
+ "Ctrl + +": "zoom in",
76
+ "Ctrl + -": "zoom out",
77
+ "Ctrl + 0": "adapt to screen",
78
+ "Ctrl + =": "actual size",
79
+ "[": "increase brush size",
80
+ "]": "decrease brush size",
81
+ "{": "increase brush hardness",
82
+ "}": "decrease brush hardness"
83
+ }
84
+
85
+ self.add_bold_label(left_layout, "Keyboard Shortcuts")
86
+ for k, v in shortcuts.items():
87
+ left_layout.addRow(f"<b>{k}</b>", QLabel(v))
88
+
89
+ mouse_controls = {
90
+ "Space + Drag": "pan",
91
+ "Wheel": "zoom in/out",
92
+ "Ctrl + Wheel": "adjust brush size",
93
+ "Left Click": "brush action",
94
+ }
95
+
96
+ self.add_bold_label(right_layout, "Mouse Controls")
97
+ for k, v in mouse_controls.items():
98
+ right_layout.addRow(f"<b>{k}</b>", QLabel(v))
@@ -0,0 +1,38 @@
1
+ from .. config.gui_constants import gui_constants
2
+
3
+
4
+ class UndoManager:
5
+ def __init__(self):
6
+ self.undo_stack = []
7
+ self.max_undo_steps = gui_constants.MAX_UNDO_STEPS
8
+ self.reset_undo_area()
9
+
10
+ def reset_undo_area(self):
11
+ self.x_end = self.y_end = 0
12
+ self.x_start = self.y_start = gui_constants.MAX_UNDO_SIZE
13
+
14
+ def extend_undo_area(self, x_start, y_start, x_end, y_end):
15
+ self.x_start = min(self.x_start, x_start)
16
+ self.y_start = min(self.y_start, y_start)
17
+ self.x_end = max(self.x_end, x_end)
18
+ self.y_end = max(self.y_end, y_end)
19
+
20
+ def save_undo_state(self, layer):
21
+ if layer is None:
22
+ return
23
+ undo_state = {
24
+ 'master': layer[self.y_start:self.y_end, self.x_start:self.x_end],
25
+ 'area': (self.x_start, self.y_start, self.x_end, self.y_end)
26
+ }
27
+ if len(self.undo_stack) >= self.max_undo_steps:
28
+ self.undo_stack.pop(0)
29
+ self.undo_stack.append(undo_state)
30
+
31
+ def undo(self, layer):
32
+ if layer is None or not self.undo_stack or len(self.undo_stack) == 0:
33
+ return False
34
+ else:
35
+ undo_state = self.undo_stack.pop()
36
+ x_start, y_start, x_end, y_end = undo_state['area']
37
+ layer[y_start:y_end, x_start:x_end] = undo_state['master']
38
+ return True
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: shinestacker
3
+ Version: 0.2.0.post1.dev1
4
+ Summary: ShineStacker
5
+ Author-email: Luca Lista <luka.lista@gmail.com>
6
+ License-Expression: LGPL-3.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: argparse
13
+ Requires-Dist: imagecodecs
14
+ Requires-Dist: ipywidgets
15
+ Requires-Dist: jsonpickle
16
+ Requires-Dist: matplotlib
17
+ Requires-Dist: numpy
18
+ Requires-Dist: opencv_python
19
+ Requires-Dist: pillow
20
+ Requires-Dist: psdtags
21
+ Requires-Dist: PySide6
22
+ Requires-Dist: scipy
23
+ Requires-Dist: tifffile
24
+ Requires-Dist: tqdm
25
+ Requires-Dist: setuptools-scm
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # Shine Stacker Processing Framework
31
+
32
+ [![CI multiplatform](https://github.com/lucalista/shinestacker/actions/workflows/ci-multiplatform.yml/badge.svg)](https://github.com/lucalista/shinestacker/actions/workflows/ci-multiplatform.yml)
33
+
34
+ <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400">
35
+
36
+ ## Documentation
37
+
38
+ 📖 [Main documentation](https://github.com/lucalista/shinestacker/blob/main/docs/main.md) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
39
+
40
+
41
+ # Credits:
42
+
43
+ The main pyramid stack algorithm was inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar. The latest implementation was rewritten from the original code that was used under permission of the author for initial versions of this package.
44
+
45
+ # Resources
46
+
47
+ * [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
48
+ Pyramid methods in image processing
49
+ * [A Multi-focus Image Fusion Method Based on Laplacian Pyramid](http://www.jcomputers.us/vol6/jcp0612-07.pdf), Wencheng Wang, Faliang Chang, Journal of Computers 6 (12), 2559, December 2011
50
+ * Another [original implementation on GitHub](https://github.com/bznick98/Focus_Stacking) by Zongnan Bao
51
+
52
+ # License
53
+
54
+ The software is provided as is under the [GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/).
55
+
@@ -0,0 +1,67 @@
1
+ shinestacker/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
2
+ shinestacker/_version.py,sha256=8SnsniWXGASAhyv0_iGZNz3rdNxykDCUL9eNy-R71E4,32
3
+ shinestacker/algorithms/__init__.py,sha256=XKMSOCBqcpeXng5PJ88wLhxhvSIwBJ6xuBFHfjda4ow,519
4
+ shinestacker/algorithms/align.py,sha256=93szP69KN6BVRmMlU4TsNwBMgpYxEU9fxNzoK07n0Rw,16575
5
+ shinestacker/algorithms/balance.py,sha256=UOmyUPJVBswUYWvYIB8WdlfTAxUAahZrnxQUSrYJ3I4,15649
6
+ shinestacker/algorithms/core_utils.py,sha256=u1aw2-cdA1-RiALxA4rnj38oN2pe2s3BP3T_flvuKMQ,550
7
+ shinestacker/algorithms/depth_map.py,sha256=hx6yyCjLKiu3Wi5uZFXiixEfOBxfieNQ9xH9cp5zD0Y,7844
8
+ shinestacker/algorithms/exif.py,sha256=VS3qbLRVlbZ8_z8hYgGVV4BSmwWzR0reN_G0XV3hzAI,9364
9
+ shinestacker/algorithms/multilayer.py,sha256=Fm-WZqH4DAEOAyC0QpPaMH3VaG5cEPX9rqKsAyWpELs,8694
10
+ shinestacker/algorithms/noise_detection.py,sha256=ra4mkprxPcb5WqHsOdKFUflAmIJ4_nQegYv1EhwH7ts,8280
11
+ shinestacker/algorithms/pyramid.py,sha256=iUbgRI0p0uzEXcZetAm3hgzwiXhF4mIaNxYMUUpJFeE,8589
12
+ shinestacker/algorithms/stack.py,sha256=kEaV2tTYcLbCv_rClYrD_VEQpKy-v-vlQEhu7OLervg,5213
13
+ shinestacker/algorithms/stack_framework.py,sha256=aJBjkQFxiPNjnsnZyoF8lXKR17tm0rTO7puEu_ZvASU,10928
14
+ shinestacker/algorithms/utils.py,sha256=TV3NaGe6_2JTgGdFe4kKNzgihjt6vcK-SLy-HHnbts0,2054
15
+ shinestacker/algorithms/vignetting.py,sha256=EiD4O8GJPGOqByjDAfFj-de4cb64Qf685RujqlBgvX0,6774
16
+ shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ shinestacker/app/about_dialog.py,sha256=G_2hRQFpo96q0H8z02fMZ0Jl_2-PpnwNxqISY7Xn5Kg,885
18
+ shinestacker/app/app_config.py,sha256=tQ1JBTG2gtHO1UbJYJjjYUBCakmtZpafvMUTHb5OyUo,1117
19
+ shinestacker/app/gui_utils.py,sha256=C5ehbYyoIcBweFTfdQcjsILAcWpMPrVLMbYz0ZM-EcM,1571
20
+ shinestacker/app/help_menu.py,sha256=ofFhZMPTz7lQ-_rsu30yAxbLA-Zq_kkRGXxPMukaG08,476
21
+ shinestacker/app/main.py,sha256=jh53KYyQ6SG9hsPqBpm64LmSxDWWwd7SkXbSSIjKHEk,7101
22
+ shinestacker/app/open_frames.py,sha256=uqUihAkP1K2p4bQ8LAQK02XwVddkRJUPy_vzwUgI8jc,1356
23
+ shinestacker/app/project.py,sha256=xsBoPlrZOVVScxiF1ITxnGzrEdfapjiMc_Jf94o3kIg,3254
24
+ shinestacker/app/retouch.py,sha256=cHPV1LhEBk1ggvLl2Cqfn6kRDVZIPlsSuE36COHRVIM,3022
25
+ shinestacker/config/__init__.py,sha256=l9Cg0Rp9sUsY_F7gR2kznuVBkQ-arO7ZOcjS1FoWPtM,121
26
+ shinestacker/config/config.py,sha256=Vif1_-zFZQhI6KW1CAX3Yt-cgwXMJ7YDwDJrzoGVE48,1459
27
+ shinestacker/config/constants.py,sha256=yOt1L7LiJyBPrGezIW-Vx_1I4r1Os0rPibfqroN30nk,5724
28
+ shinestacker/config/gui_constants.py,sha256=PtizNIsLHM_P_GdkrhsSVZuuS5abxOETXozu5eSRt9w,2440
29
+ shinestacker/core/__init__.py,sha256=1Iyzh0CXNlRQhpBJg1QR5Ip3bVS0e1hlOBhTDyrTUBw,301
30
+ shinestacker/core/colors.py,sha256=f4_iaNgDYpHfLaoooCsltDxem5fI950GPZlw2lFPqYM,1330
31
+ shinestacker/core/core_utils.py,sha256=EkDJY8g3zLdAqT9hTRZ2_jh3jo8GMF2L6ZBINyG6L6Y,1398
32
+ shinestacker/core/exceptions.py,sha256=TzguZ88XxjgbPs5jMFdrXV2cQRkl5mFfXZin9GRMSRY,1555
33
+ shinestacker/core/framework.py,sha256=7N7awCefwT-SUsUE97eM9DBCCbNnrHk1Yab-DGE5iNI,6871
34
+ shinestacker/core/logging.py,sha256=YkKqCduU9FVYCBi26izHKmjyxlGMbwwpYbfn9AY1KeI,3155
35
+ shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ shinestacker/gui/action_config.py,sha256=v-BWr-en11nyp0bFKoPqUQnVfSv9Ov14KW7oEaVJ9I8,47690
37
+ shinestacker/gui/actions_window.py,sha256=DTMyi2WTugJxqliD6QdKcui_nhtaq0Pq-0SHuLBJGh0,13136
38
+ shinestacker/gui/colors.py,sha256=Af4ccwKfiElzA2poyx2QSU2xvxOdA5fFz0Y2YIqTj9Y,1292
39
+ shinestacker/gui/gui_images.py,sha256=3HySCrldbYhUyxO0B1ckC2TLVg5nq94bBXjqsQzCyCA,5546
40
+ shinestacker/gui/gui_logging.py,sha256=sN82OsGhMcZdgFMY4z-VbUYiRIsReN-ICaxi31M1J6E,8147
41
+ shinestacker/gui/gui_run.py,sha256=LpBi1V91NrJpVpgS098lSgLtiege0aqcWIGwSbB8cL4,15701
42
+ shinestacker/gui/main_window.py,sha256=O7eydSmYa1ijzUHrYeeVUp9x4YGuHwhDGZQ1PxH1MJI,27101
43
+ shinestacker/gui/new_project.py,sha256=-2d9ts4bcPnEo_qufKwLgRYupsx3EBs_5_u4Wz2pwX4,7226
44
+ shinestacker/gui/project_converter.py,sha256=d66pbBzaBgANpudJLW0tGUSfSy0PXNhs1M6R2o_Fd5E,7390
45
+ shinestacker/gui/project_editor.py,sha256=i3vPOWNtJ4zkOQ4y0EIgv30DlpTv3tWEQC9DBnGmXcY,21791
46
+ shinestacker/gui/project_model.py,sha256=buzpxppLuoNWao7M2_FOPVpCBex2WgEcvqyq9dxvrz8,4524
47
+ shinestacker/gui/img/close-round-line-icon.png,sha256=9HZwCjgni1s_JGUPUb_MoOfoe4tRZgM5OWzk92XFZlE,8019
48
+ shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzdsHvmDAjlbE18Pgo,4788
49
+ shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
50
+ shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
51
+ shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ shinestacker/retouch/brush.py,sha256=49YNdZp1dbfowh6HmLfxuHKz7Py9wkFQsN9-pH38P7Q,319
53
+ shinestacker/retouch/brush_controller.py,sha256=M1vXrgL8ETJheT9PJ8EVIHlMqiVRLePvL5GlEcEaZW4,2949
54
+ shinestacker/retouch/brush_preview.py,sha256=ePyoZPdSY4ytK4uXV-eByQgfsqvhHJqXpleycSxshDg,5247
55
+ shinestacker/retouch/exif_data.py,sha256=eNrkhe9WA_1LPkaoxLHeM4k8s5HJinD0-O79Ik4xMRU,2451
56
+ shinestacker/retouch/file_loader.py,sha256=TbPjiD9Vv-i3LCsPVcumYJ2DxgnLmQA6xYCiLCqbEcg,4565
57
+ shinestacker/retouch/image_editor.py,sha256=WNJGUpWV14Q5kJQ5fcHAqVx_mb3vK27gfVztwHLYtOc,27637
58
+ shinestacker/retouch/image_editor_ui.py,sha256=wKHqNhcjrPB_qm4zKsTQUw_rXIWzWO3uQUDZvhsbnys,15752
59
+ shinestacker/retouch/image_viewer.py,sha256=5t7JRNoNwSgS7btn7zWmSXPPzXvZKbWBiZMm-YA7Xhg,14827
60
+ shinestacker/retouch/shortcuts_help.py,sha256=yR4KqkCG3-X62KCQWAR_Tyv8aRbonTsiK753DkplehY,3966
61
+ shinestacker/retouch/undo_manager.py,sha256=R7O0fbdGFRK2ZZMVpbOcqB5stJLpFa-o2p7Kto8daSI,1355
62
+ shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE,sha256=cBN0P3F6BWFkfOabkhuTxwJnK1B0v50jmmzZJjGGous,80
63
+ shinestacker-0.2.0.post1.dev1.dist-info/METADATA,sha256=1k3Te_FPd1jRkcASiGH0E9Ul0JlV0Ka1EFVgIm2M7Sg,2447
64
+ shinestacker-0.2.0.post1.dev1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
+ shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
66
+ shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
67
+ shinestacker-0.2.0.post1.dev1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ shinestacker = shinestacker.app.main:main
3
+ shinestacker-project = shinestacker.app.project:main
4
+ shinestacker-retouch = shinestacker.app.retouch:main
@@ -0,0 +1 @@
1
+ The software is provided as is under the GNU Lesser General Public License v3.0.
@@ -0,0 +1 @@
1
+ shinestacker