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.
- shinestacker/__init__.py +3 -0
- shinestacker/_version.py +1 -0
- shinestacker/algorithms/__init__.py +14 -0
- shinestacker/algorithms/align.py +307 -0
- shinestacker/algorithms/balance.py +367 -0
- shinestacker/algorithms/core_utils.py +22 -0
- shinestacker/algorithms/depth_map.py +164 -0
- shinestacker/algorithms/exif.py +238 -0
- shinestacker/algorithms/multilayer.py +187 -0
- shinestacker/algorithms/noise_detection.py +182 -0
- shinestacker/algorithms/pyramid.py +176 -0
- shinestacker/algorithms/stack.py +112 -0
- shinestacker/algorithms/stack_framework.py +248 -0
- shinestacker/algorithms/utils.py +71 -0
- shinestacker/algorithms/vignetting.py +137 -0
- shinestacker/app/__init__.py +0 -0
- shinestacker/app/about_dialog.py +24 -0
- shinestacker/app/app_config.py +39 -0
- shinestacker/app/gui_utils.py +35 -0
- shinestacker/app/help_menu.py +16 -0
- shinestacker/app/main.py +176 -0
- shinestacker/app/open_frames.py +39 -0
- shinestacker/app/project.py +91 -0
- shinestacker/app/retouch.py +82 -0
- shinestacker/config/__init__.py +4 -0
- shinestacker/config/config.py +53 -0
- shinestacker/config/constants.py +174 -0
- shinestacker/config/gui_constants.py +85 -0
- shinestacker/core/__init__.py +5 -0
- shinestacker/core/colors.py +60 -0
- shinestacker/core/core_utils.py +52 -0
- shinestacker/core/exceptions.py +50 -0
- shinestacker/core/framework.py +210 -0
- shinestacker/core/logging.py +89 -0
- shinestacker/gui/__init__.py +0 -0
- shinestacker/gui/action_config.py +879 -0
- shinestacker/gui/actions_window.py +283 -0
- shinestacker/gui/colors.py +57 -0
- shinestacker/gui/gui_images.py +152 -0
- shinestacker/gui/gui_logging.py +213 -0
- shinestacker/gui/gui_run.py +393 -0
- shinestacker/gui/img/close-round-line-icon.png +0 -0
- shinestacker/gui/img/forward-button-icon.png +0 -0
- shinestacker/gui/img/play-button-round-icon.png +0 -0
- shinestacker/gui/img/plus-round-line-icon.png +0 -0
- shinestacker/gui/main_window.py +599 -0
- shinestacker/gui/new_project.py +170 -0
- shinestacker/gui/project_converter.py +148 -0
- shinestacker/gui/project_editor.py +539 -0
- shinestacker/gui/project_model.py +138 -0
- shinestacker/retouch/__init__.py +0 -0
- shinestacker/retouch/brush.py +9 -0
- shinestacker/retouch/brush_controller.py +57 -0
- shinestacker/retouch/brush_preview.py +126 -0
- shinestacker/retouch/exif_data.py +65 -0
- shinestacker/retouch/file_loader.py +104 -0
- shinestacker/retouch/image_editor.py +651 -0
- shinestacker/retouch/image_editor_ui.py +380 -0
- shinestacker/retouch/image_viewer.py +356 -0
- shinestacker/retouch/shortcuts_help.py +98 -0
- shinestacker/retouch/undo_manager.py +38 -0
- shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
- shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
- shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
- shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
- shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
- 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
|
+
[](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 @@
|
|
|
1
|
+
The software is provided as is under the GNU Lesser General Public License v3.0.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shinestacker
|