shinestacker 1.4.0__py3-none-any.whl → 1.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- shinestacker/_version.py +1 -1
- shinestacker/app/args.py +23 -0
- shinestacker/app/main.py +18 -9
- shinestacker/app/project.py +2 -3
- shinestacker/app/retouch.py +8 -4
- shinestacker/config/gui_constants.py +7 -2
- shinestacker/gui/new_project.py +17 -14
- shinestacker/retouch/base_filter.py +8 -10
- shinestacker/retouch/brush_preview.py +28 -15
- shinestacker/retouch/display_manager.py +42 -42
- shinestacker/retouch/image_editor_ui.py +67 -53
- shinestacker/retouch/image_view_status.py +4 -0
- shinestacker/retouch/image_viewer.py +8 -4
- shinestacker/retouch/io_gui_handler.py +0 -3
- shinestacker/retouch/layer_collection.py +3 -0
- shinestacker/retouch/overlaid_view.py +99 -84
- shinestacker/retouch/shortcuts_help.py +35 -31
- shinestacker/retouch/sidebyside_view.py +141 -178
- shinestacker/retouch/transformation_manager.py +43 -0
- shinestacker/retouch/undo_manager.py +22 -3
- shinestacker/retouch/view_strategy.py +234 -64
- {shinestacker-1.4.0.dist-info → shinestacker-1.5.1.dist-info}/METADATA +7 -7
- {shinestacker-1.4.0.dist-info → shinestacker-1.5.1.dist-info}/RECORD +27 -25
- {shinestacker-1.4.0.dist-info → shinestacker-1.5.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.4.0.dist-info → shinestacker-1.5.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.4.0.dist-info → shinestacker-1.5.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.4.0.dist-info → shinestacker-1.5.1.dist-info}/top_level.txt +0 -0
|
@@ -6,8 +6,9 @@ from .. config.gui_constants import gui_constants
|
|
|
6
6
|
class UndoManager(QObject):
|
|
7
7
|
stack_changed = Signal(bool, str, bool, str)
|
|
8
8
|
|
|
9
|
-
def __init__(self):
|
|
9
|
+
def __init__(self, transformation_manager):
|
|
10
10
|
super().__init__()
|
|
11
|
+
self.transformation_manager = transformation_manager
|
|
11
12
|
self.x_start = None
|
|
12
13
|
self.y_start = None
|
|
13
14
|
self.x_end = None
|
|
@@ -59,7 +60,16 @@ class UndoManager(QObject):
|
|
|
59
60
|
'description': undo_state['description']
|
|
60
61
|
}
|
|
61
62
|
self.redo_stack.append(redo_state)
|
|
62
|
-
|
|
63
|
+
descr = undo_state['description']
|
|
64
|
+
if descr.startswith(gui_constants.ROTATE_LABEL):
|
|
65
|
+
if descr == gui_constants.ROTATE_90_CW_LABEL:
|
|
66
|
+
self.transformation_manager.rotate_90_ccw(False)
|
|
67
|
+
elif descr == gui_constants.ROTATE_90_CCW_LABEL:
|
|
68
|
+
self.transformation_manager.rotate_90_cw(False)
|
|
69
|
+
elif descr == gui_constants.ROTATE_180_LABEL:
|
|
70
|
+
self.transformation_manager.rotate_180(False)
|
|
71
|
+
else:
|
|
72
|
+
layer[y_start:y_end, x_start:x_end] = undo_state['master']
|
|
63
73
|
undo_desc = self.undo_stack[-1]['description'] if self.undo_stack else ""
|
|
64
74
|
redo_desc = redo_state['description']
|
|
65
75
|
self.stack_changed.emit(bool(self.undo_stack), undo_desc, bool(self.redo_stack), redo_desc)
|
|
@@ -76,7 +86,16 @@ class UndoManager(QObject):
|
|
|
76
86
|
'description': redo_state['description']
|
|
77
87
|
}
|
|
78
88
|
self.undo_stack.append(undo_state)
|
|
79
|
-
|
|
89
|
+
descr = undo_state['description']
|
|
90
|
+
if descr.startswith(gui_constants.ROTATE_LABEL):
|
|
91
|
+
if descr == gui_constants.ROTATE_90_CW_LABEL:
|
|
92
|
+
self.transformation_manager.rotate_90_cw(False)
|
|
93
|
+
elif descr == gui_constants.ROTATE_90_CCW_LABEL:
|
|
94
|
+
self.transformation_manager.rotate_90_ccw(False)
|
|
95
|
+
elif descr == gui_constants.ROTATE_180_LABEL:
|
|
96
|
+
self.transformation_manager.rotate_180(False)
|
|
97
|
+
else:
|
|
98
|
+
layer[y_start:y_end, x_start:x_end] = redo_state['master']
|
|
80
99
|
undo_desc = undo_state['description']
|
|
81
100
|
redo_desc = self.redo_stack[-1]['description'] if self.redo_stack else ""
|
|
82
101
|
self.stack_changed.emit(bool(self.undo_stack), undo_desc, bool(self.redo_stack), redo_desc)
|
|
@@ -1,17 +1,79 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0904, R0903, R0902, E1101, R0914
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0904, R0903, R0902, E1101, R0914, R0913, R0917
|
|
2
2
|
import math
|
|
3
3
|
from abc import abstractmethod
|
|
4
4
|
import numpy as np
|
|
5
|
-
from PySide6.QtCore import Qt, QPointF, QTime, QPoint, Signal
|
|
6
|
-
from PySide6.QtGui import QImage, QPainter, QColor, QBrush, QPen, QCursor
|
|
5
|
+
from PySide6.QtCore import Qt, QPointF, QTime, QPoint, Signal, QRectF
|
|
6
|
+
from PySide6.QtGui import QImage, QPainter, QColor, QBrush, QPen, QCursor, QPixmap, QPainterPath
|
|
7
7
|
from PySide6.QtWidgets import (
|
|
8
|
-
QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
|
|
8
|
+
QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
|
|
9
|
+
QGraphicsItemGroup, QGraphicsPathItem)
|
|
9
10
|
from .. config.gui_constants import gui_constants
|
|
10
11
|
from .layer_collection import LayerCollectionHandler
|
|
11
12
|
from .brush_gradient import create_default_brush_gradient
|
|
12
13
|
from .brush_preview import BrushPreviewItem
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
class BrushCursor(QGraphicsItemGroup):
|
|
17
|
+
def __init__(self, x0, y0, size, pen, brush):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self._pen = pen
|
|
20
|
+
self._radius = size / 2
|
|
21
|
+
self._brush = brush
|
|
22
|
+
self._rect = QRectF(x0 - self._radius, y0 - self._radius, size, size)
|
|
23
|
+
self._arc_items = []
|
|
24
|
+
self._create_arcs()
|
|
25
|
+
|
|
26
|
+
def _point_on_circle(self, phi_deg):
|
|
27
|
+
phi = phi_deg / 180.0 * math.pi
|
|
28
|
+
x0 = self._rect.x() + self._radius
|
|
29
|
+
y0 = self._rect.y() + self._radius
|
|
30
|
+
return x0 + self._radius * math.cos(phi), y0 - self._radius * math.sin(phi)
|
|
31
|
+
|
|
32
|
+
def _create_arcs(self):
|
|
33
|
+
for item in self._arc_items:
|
|
34
|
+
self.removeFromGroup(item)
|
|
35
|
+
if item.scene():
|
|
36
|
+
item.scene().removeItem(item)
|
|
37
|
+
self._arc_items = []
|
|
38
|
+
half_gap = 20
|
|
39
|
+
arcs = [half_gap, 90 + half_gap, 180 + half_gap, 270 + half_gap]
|
|
40
|
+
span_angle = 90 - 2 * half_gap
|
|
41
|
+
for start_angle in arcs:
|
|
42
|
+
path = QPainterPath()
|
|
43
|
+
path.moveTo(*self._point_on_circle(start_angle))
|
|
44
|
+
path.arcTo(self._rect, start_angle, span_angle)
|
|
45
|
+
arc_item = QGraphicsPathItem(path)
|
|
46
|
+
arc_item.setPen(self._pen)
|
|
47
|
+
arc_item.setBrush(Qt.NoBrush)
|
|
48
|
+
self.addToGroup(arc_item)
|
|
49
|
+
self._arc_items.append(arc_item)
|
|
50
|
+
|
|
51
|
+
# pylint: disable=C0103
|
|
52
|
+
def setPen(self, pen):
|
|
53
|
+
self._pen = pen
|
|
54
|
+
for item in self._arc_items:
|
|
55
|
+
item.setPen(pen)
|
|
56
|
+
|
|
57
|
+
def pen(self):
|
|
58
|
+
return self._pen
|
|
59
|
+
|
|
60
|
+
def setBrush(self, brush):
|
|
61
|
+
self._brush = brush
|
|
62
|
+
for item in self._arc_items:
|
|
63
|
+
item.setBrush(Qt.NoBrush)
|
|
64
|
+
|
|
65
|
+
def brush(self):
|
|
66
|
+
return self._brush
|
|
67
|
+
|
|
68
|
+
def setRect(self, x, y, w, h):
|
|
69
|
+
self._rect = QRectF(x, y, w, h)
|
|
70
|
+
self._create_arcs()
|
|
71
|
+
|
|
72
|
+
def rect(self):
|
|
73
|
+
return self._rect
|
|
74
|
+
# pylint: enable=C0103
|
|
75
|
+
|
|
76
|
+
|
|
15
77
|
class ViewSignals:
|
|
16
78
|
temp_view_requested = Signal(bool)
|
|
17
79
|
brush_operation_started = Signal(QPoint)
|
|
@@ -52,7 +114,6 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
52
114
|
self.pinch_center_view = None
|
|
53
115
|
self.pinch_center_scene = None
|
|
54
116
|
self.pinch_start_scale = None
|
|
55
|
-
self.last_scroll_pos = None
|
|
56
117
|
self.scrolling = False
|
|
57
118
|
self.dragging = False
|
|
58
119
|
self.last_brush_pos = None
|
|
@@ -72,19 +133,19 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
72
133
|
pass
|
|
73
134
|
|
|
74
135
|
@abstractmethod
|
|
75
|
-
def
|
|
136
|
+
def get_master_view(self):
|
|
76
137
|
pass
|
|
77
138
|
|
|
78
139
|
@abstractmethod
|
|
79
|
-
def
|
|
140
|
+
def get_current_view(self):
|
|
80
141
|
pass
|
|
81
142
|
|
|
82
143
|
@abstractmethod
|
|
83
|
-
def
|
|
144
|
+
def get_master_scene(self):
|
|
84
145
|
pass
|
|
85
146
|
|
|
86
147
|
@abstractmethod
|
|
87
|
-
def
|
|
148
|
+
def get_current_scene(self):
|
|
88
149
|
pass
|
|
89
150
|
|
|
90
151
|
@abstractmethod
|
|
@@ -103,12 +164,52 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
103
164
|
def get_master_pixmap(self):
|
|
104
165
|
pass
|
|
105
166
|
|
|
167
|
+
@abstractmethod
|
|
168
|
+
def get_current_pixmap(self):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
@abstractmethod
|
|
106
172
|
def show_master(self):
|
|
107
173
|
pass
|
|
108
174
|
|
|
175
|
+
@abstractmethod
|
|
109
176
|
def show_current(self):
|
|
110
177
|
pass
|
|
111
178
|
|
|
179
|
+
@abstractmethod
|
|
180
|
+
def arrange_images(self):
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
def update_master_display(self):
|
|
184
|
+
if not self.empty():
|
|
185
|
+
master_qimage = self.numpy_to_qimage(self.master_layer())
|
|
186
|
+
if master_qimage:
|
|
187
|
+
pixmap = QPixmap.fromImage(master_qimage)
|
|
188
|
+
self.get_master_pixmap().setPixmap(pixmap)
|
|
189
|
+
self.get_master_scene().setSceneRect(QRectF(pixmap.rect()))
|
|
190
|
+
self.get_master_view().horizontalScrollBar().setValue(self.status.h_scroll)
|
|
191
|
+
self.get_master_view().verticalScrollBar().setValue(self.status.v_scroll)
|
|
192
|
+
self.arrange_images()
|
|
193
|
+
|
|
194
|
+
def update_current_display(self):
|
|
195
|
+
if not self.empty() and self.number_of_layers() > 0:
|
|
196
|
+
current_qimage = self.numpy_to_qimage(self.current_layer())
|
|
197
|
+
if current_qimage:
|
|
198
|
+
pixmap = QPixmap.fromImage(current_qimage)
|
|
199
|
+
self.get_current_pixmap().setPixmap(pixmap)
|
|
200
|
+
self.get_current_scene().setSceneRect(QRectF(pixmap.rect()))
|
|
201
|
+
self.get_current_view().horizontalScrollBar().setValue(self.status.h_scroll)
|
|
202
|
+
self.get_current_view().verticalScrollBar().setValue(self.status.v_scroll)
|
|
203
|
+
self.arrange_images()
|
|
204
|
+
|
|
205
|
+
def update_cursor_pen_width(self):
|
|
206
|
+
width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
|
|
207
|
+
if self.brush_cursor is not None:
|
|
208
|
+
pen = self.brush_cursor.pen()
|
|
209
|
+
pen.setWidthF(width)
|
|
210
|
+
self.brush_cursor.setPen(pen)
|
|
211
|
+
return width
|
|
212
|
+
|
|
112
213
|
def set_allow_cursor_preview(self, state):
|
|
113
214
|
self.allow_cursor_preview = state
|
|
114
215
|
|
|
@@ -154,10 +255,10 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
154
255
|
return self.cursor_style
|
|
155
256
|
|
|
156
257
|
def handle_key_press_event(self, _event):
|
|
157
|
-
return
|
|
258
|
+
return True
|
|
158
259
|
|
|
159
260
|
def handle_key_release_event(self, _event):
|
|
160
|
-
return
|
|
261
|
+
return True
|
|
161
262
|
|
|
162
263
|
def clear_image(self):
|
|
163
264
|
for scene in self.get_scenes():
|
|
@@ -171,8 +272,16 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
171
272
|
if self.brush_cursor:
|
|
172
273
|
self.brush_cursor.hide()
|
|
173
274
|
|
|
275
|
+
def cleanup_brush_preview(self):
|
|
276
|
+
if self.brush_cursor:
|
|
277
|
+
self.brush_cursor.hide()
|
|
278
|
+
self.brush_preview.hide()
|
|
279
|
+
|
|
174
280
|
def set_master_image_np(self, img):
|
|
175
281
|
self.set_master_image(self.numpy_to_qimage(img))
|
|
282
|
+
if self.brush_cursor is None:
|
|
283
|
+
self.setup_brush_cursor()
|
|
284
|
+
self.show_master()
|
|
176
285
|
|
|
177
286
|
def numpy_to_qimage(self, array):
|
|
178
287
|
if array is None:
|
|
@@ -201,9 +310,14 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
201
310
|
return pixmap_item
|
|
202
311
|
|
|
203
312
|
def refresh_display(self):
|
|
204
|
-
self.update_brush_cursor()
|
|
205
313
|
for scene in self.get_scenes():
|
|
206
314
|
scene.update()
|
|
315
|
+
self.update_brush_cursor()
|
|
316
|
+
|
|
317
|
+
def set_max_min_scales(self, img_width, img_height):
|
|
318
|
+
self.set_min_scale(min(gui_constants.MIN_ZOOMED_IMG_WIDTH / img_width,
|
|
319
|
+
gui_constants.MIN_ZOOMED_IMG_HEIGHT / img_height))
|
|
320
|
+
self.set_max_scale(gui_constants.MAX_ZOOMED_IMG_PX_SIZE)
|
|
207
321
|
|
|
208
322
|
def zoom_in(self):
|
|
209
323
|
if self.empty():
|
|
@@ -218,6 +332,15 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
218
332
|
self.set_zoom_factor(new_scale)
|
|
219
333
|
master_view.centerOn(old_center)
|
|
220
334
|
self.update_brush_cursor()
|
|
335
|
+
self.update_cursor_pen_width()
|
|
336
|
+
|
|
337
|
+
def apply_zoom(self):
|
|
338
|
+
if self.empty():
|
|
339
|
+
return
|
|
340
|
+
for view in self.get_views():
|
|
341
|
+
current_scale = view.transform().m11()
|
|
342
|
+
scale_factor = self.zoom_factor() / current_scale
|
|
343
|
+
view.scale(scale_factor, scale_factor)
|
|
221
344
|
|
|
222
345
|
def zoom_out(self):
|
|
223
346
|
if self.empty():
|
|
@@ -232,12 +355,12 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
232
355
|
self.set_zoom_factor(new_scale)
|
|
233
356
|
master_view.centerOn(old_center)
|
|
234
357
|
self.update_brush_cursor()
|
|
358
|
+
self.update_cursor_pen_width()
|
|
235
359
|
|
|
236
360
|
def reset_zoom(self):
|
|
237
361
|
if self.empty():
|
|
238
362
|
return
|
|
239
363
|
self.pinch_start_scale = 1.0
|
|
240
|
-
self.last_scroll_pos = QPointF()
|
|
241
364
|
self.gesture_active = False
|
|
242
365
|
self.pinch_center_view = None
|
|
243
366
|
self.pinch_center_scene = None
|
|
@@ -249,6 +372,7 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
249
372
|
view.resetTransform()
|
|
250
373
|
view.scale(self.zoom_factor(), self.zoom_factor())
|
|
251
374
|
self.update_brush_cursor()
|
|
375
|
+
self.update_cursor_pen_width()
|
|
252
376
|
|
|
253
377
|
def actual_size(self):
|
|
254
378
|
if self.empty():
|
|
@@ -258,11 +382,7 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
258
382
|
view.resetTransform()
|
|
259
383
|
view.scale(self.zoom_factor(), self.zoom_factor())
|
|
260
384
|
self.update_brush_cursor()
|
|
261
|
-
|
|
262
|
-
def setup_outline_style(self):
|
|
263
|
-
self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
|
|
264
|
-
gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()))
|
|
265
|
-
self.brush_cursor.setBrush(Qt.NoBrush)
|
|
385
|
+
self.update_cursor_pen_width()
|
|
266
386
|
|
|
267
387
|
def setup_simple_brush_style(self, center_x, center_y, radius):
|
|
268
388
|
gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
|
|
@@ -270,25 +390,47 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
270
390
|
gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()))
|
|
271
391
|
self.brush_cursor.setBrush(QBrush(gradient))
|
|
272
392
|
|
|
273
|
-
def
|
|
274
|
-
if not self.brush:
|
|
275
|
-
return
|
|
276
|
-
scene = self.get_master_scene()
|
|
393
|
+
def create_circle(self, scene, line_style=Qt.SolidLine):
|
|
277
394
|
for item in scene.items():
|
|
278
395
|
if isinstance(item, QGraphicsEllipseItem) and item != self.brush_preview:
|
|
279
396
|
scene.removeItem(item)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
397
|
+
pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
|
|
398
|
+
pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), pen_width, line_style)
|
|
399
|
+
brush = Qt.NoBrush
|
|
400
|
+
scene_center = scene.sceneRect().center()
|
|
401
|
+
brush_cursor = scene.addEllipse(
|
|
402
|
+
scene_center.x(), scene_center.y(),
|
|
403
|
+
self.brush.size, self.brush.size, pen, brush)
|
|
404
|
+
brush_cursor.setZValue(1000)
|
|
405
|
+
brush_cursor.hide()
|
|
406
|
+
return brush_cursor
|
|
407
|
+
|
|
408
|
+
def create_alt_circle(self, scene, line_style=Qt.SolidLine):
|
|
409
|
+
for item in scene.items():
|
|
410
|
+
if isinstance(item, BrushCursor) and item != self.brush_preview:
|
|
411
|
+
scene.removeItem(item)
|
|
412
|
+
pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
|
|
413
|
+
pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), pen_width, line_style)
|
|
414
|
+
brush = Qt.NoBrush
|
|
415
|
+
scene_center = scene.sceneRect().center()
|
|
416
|
+
brush_cursor = BrushCursor(
|
|
417
|
+
scene_center.x(), scene_center.y(),
|
|
418
|
+
self.brush.size, pen, brush
|
|
419
|
+
)
|
|
420
|
+
brush_cursor.setZValue(1000)
|
|
421
|
+
brush_cursor.hide()
|
|
422
|
+
scene.addItem(brush_cursor)
|
|
423
|
+
return brush_cursor
|
|
286
424
|
|
|
287
|
-
def
|
|
288
|
-
if self.
|
|
425
|
+
def setup_brush_cursor(self):
|
|
426
|
+
if not self.brush:
|
|
289
427
|
return
|
|
290
|
-
|
|
428
|
+
self.brush_cursor = self.create_circle(self.get_master_scene())
|
|
429
|
+
|
|
430
|
+
def update_brush_cursor(self):
|
|
431
|
+
if self.empty() or self.brush_cursor is None or not self.isVisible():
|
|
291
432
|
return
|
|
433
|
+
self.update_cursor_pen_width()
|
|
292
434
|
master_view = self.get_master_view()
|
|
293
435
|
mouse_pos = master_view.mapFromGlobal(QCursor.pos())
|
|
294
436
|
if not master_view.rect().contains(mouse_pos):
|
|
@@ -299,21 +441,19 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
299
441
|
radius = size / 2
|
|
300
442
|
self.brush_cursor.setRect(scene_pos.x() - radius, scene_pos.y() - radius, size, size)
|
|
301
443
|
allow_cursor_preview = self.display_manager.allow_cursor_preview()
|
|
302
|
-
if self.cursor_style == 'preview'
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
444
|
+
if self.cursor_style == 'preview':
|
|
445
|
+
if allow_cursor_preview:
|
|
446
|
+
self.brush_cursor.hide()
|
|
447
|
+
pos = QCursor.pos()
|
|
448
|
+
if isinstance(pos, QPointF):
|
|
449
|
+
scene_pos = pos
|
|
450
|
+
else:
|
|
451
|
+
cursor_pos = master_view.mapFromGlobal(pos)
|
|
452
|
+
scene_pos = master_view.mapToScene(cursor_pos)
|
|
453
|
+
self.brush_preview.update(scene_pos, int(size))
|
|
312
454
|
else:
|
|
313
455
|
self.brush_preview.hide()
|
|
314
|
-
if self.cursor_style
|
|
315
|
-
self.setup_outline_style()
|
|
316
|
-
else:
|
|
456
|
+
if self.cursor_style != 'outline':
|
|
317
457
|
self.setup_simple_brush_style(scene_pos.x(), scene_pos.y(), radius)
|
|
318
458
|
if not self.brush_cursor.isVisible():
|
|
319
459
|
self.brush_cursor.show()
|
|
@@ -354,41 +494,52 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
354
494
|
def keyPressEvent(self, event):
|
|
355
495
|
if self.empty():
|
|
356
496
|
return
|
|
357
|
-
master_view = self.get_master_view()
|
|
358
497
|
if event.key() == Qt.Key_Space and not self.scrolling:
|
|
359
498
|
self.space_pressed = True
|
|
360
|
-
|
|
499
|
+
self.get_master_view().setCursor(Qt.OpenHandCursor)
|
|
361
500
|
if self.brush_cursor:
|
|
362
501
|
self.brush_cursor.hide()
|
|
363
|
-
self.handle_key_press_event(event)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
502
|
+
if self.handle_key_press_event(event):
|
|
503
|
+
if event.key() == Qt.Key_Control and not self.scrolling:
|
|
504
|
+
self.control_pressed = True
|
|
505
|
+
super().keyPressEvent(event)
|
|
367
506
|
|
|
368
507
|
def keyReleaseEvent(self, event):
|
|
369
508
|
if self.empty():
|
|
370
509
|
return
|
|
371
|
-
master_view = self.get_master_view()
|
|
372
|
-
self.update_brush_cursor()
|
|
373
510
|
if event.key() == Qt.Key_Space:
|
|
374
511
|
self.space_pressed = False
|
|
375
512
|
if not self.scrolling:
|
|
376
|
-
|
|
513
|
+
self.get_master_view().setCursor(Qt.BlankCursor)
|
|
377
514
|
if self.brush_cursor:
|
|
378
515
|
self.brush_cursor.show()
|
|
379
|
-
self.handle_key_release_event(event)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
516
|
+
if self.handle_key_release_event(event):
|
|
517
|
+
if event.key() == Qt.Key_Control:
|
|
518
|
+
self.control_pressed = False
|
|
519
|
+
super().keyReleaseEvent(event)
|
|
383
520
|
|
|
384
521
|
def leaveEvent(self, event):
|
|
385
|
-
if
|
|
522
|
+
if self.empty():
|
|
523
|
+
self.setCursor(Qt.ArrowCursor)
|
|
524
|
+
else:
|
|
386
525
|
self.get_master_view().setCursor(Qt.ArrowCursor)
|
|
387
526
|
if self.brush_cursor:
|
|
388
527
|
self.brush_cursor.hide()
|
|
389
528
|
super().leaveEvent(event)
|
|
390
529
|
# pylint: enable=C0103
|
|
391
530
|
|
|
531
|
+
def scroll_view(self, view, delta_x, delta_y):
|
|
532
|
+
view.horizontalScrollBar().setValue(
|
|
533
|
+
view.horizontalScrollBar().value() - delta_x)
|
|
534
|
+
view.verticalScrollBar().setValue(
|
|
535
|
+
view.verticalScrollBar().value() - delta_y)
|
|
536
|
+
self.status.set_scroll(view.horizontalScrollBar().value(),
|
|
537
|
+
view.verticalScrollBar().value())
|
|
538
|
+
|
|
539
|
+
def center_image(self, view):
|
|
540
|
+
view.horizontalScrollBar().setValue(self.status.h_scroll)
|
|
541
|
+
view.verticalScrollBar().setValue(self.status.v_scroll)
|
|
542
|
+
|
|
392
543
|
def mouse_move_event(self, event):
|
|
393
544
|
if self.empty():
|
|
394
545
|
return
|
|
@@ -424,12 +575,6 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
424
575
|
self.last_mouse_pos = position
|
|
425
576
|
self.scroll_view(master_view, delta.x(), delta.y())
|
|
426
577
|
|
|
427
|
-
def scroll_view(self, view, delta_x, delta_y):
|
|
428
|
-
view.horizontalScrollBar().setValue(
|
|
429
|
-
view.horizontalScrollBar().value() - delta_x)
|
|
430
|
-
view.verticalScrollBar().setValue(
|
|
431
|
-
view.verticalScrollBar().value() - delta_y)
|
|
432
|
-
|
|
433
578
|
def mouse_press_event(self, event):
|
|
434
579
|
if self.empty():
|
|
435
580
|
return
|
|
@@ -464,3 +609,28 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
464
609
|
elif self.dragging:
|
|
465
610
|
self.dragging = False
|
|
466
611
|
self.brush_operation_ended.emit()
|
|
612
|
+
|
|
613
|
+
def handle_pinch_gesture(self, pinch):
|
|
614
|
+
master_view = self.get_master_view()
|
|
615
|
+
if pinch.state() == Qt.GestureStarted:
|
|
616
|
+
self.pinch_start_scale = self.zoom_factor()
|
|
617
|
+
self.pinch_center_view = pinch.centerPoint()
|
|
618
|
+
self.pinch_center_scene = master_view.mapToScene(self.pinch_center_view.toPoint())
|
|
619
|
+
self.gesture_active = True
|
|
620
|
+
elif pinch.state() == Qt.GestureUpdated:
|
|
621
|
+
new_scale = self.pinch_start_scale * pinch.totalScaleFactor()
|
|
622
|
+
new_scale = max(self.min_scale(), min(new_scale, self.max_scale()))
|
|
623
|
+
if abs(new_scale - self.zoom_factor()) > 0.01:
|
|
624
|
+
self.set_zoom_factor(new_scale)
|
|
625
|
+
self.apply_zoom()
|
|
626
|
+
new_center = master_view.mapToScene(self.pinch_center_view.toPoint())
|
|
627
|
+
delta = self.pinch_center_scene - new_center
|
|
628
|
+
h_scroll = master_view.horizontalScrollBar().value() + \
|
|
629
|
+
int(delta.x() * self.zoom_factor())
|
|
630
|
+
v_scroll = master_view.verticalScrollBar().value() + \
|
|
631
|
+
int(delta.y() * self.zoom_factor())
|
|
632
|
+
self.status.set_scroll(h_scroll, v_scroll)
|
|
633
|
+
self.center_image(master_view)
|
|
634
|
+
elif pinch.state() in (Qt.GestureFinished, Qt.GestureCanceled):
|
|
635
|
+
self.gesture_active = False
|
|
636
|
+
self.update_cursor_pen_width()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -70,11 +70,11 @@ The GUI has two main working areas:
|
|
|
70
70
|
|
|
71
71
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
## Resources
|
|
74
74
|
|
|
75
75
|
🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
## Note for macOS users
|
|
78
78
|
|
|
79
79
|
**The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
|
|
80
80
|
|
|
@@ -93,17 +93,17 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
|
|
|
93
93
|
|
|
94
94
|
macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
## Credits
|
|
97
97
|
|
|
98
98
|
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.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
## Resources
|
|
101
101
|
|
|
102
102
|
* [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
|
|
103
103
|
Pyramid methods in image processing
|
|
104
104
|
* [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
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
## License
|
|
107
107
|
|
|
108
108
|
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
109
109
|
|
|
@@ -112,7 +112,7 @@ Pyramid methods in image processing
|
|
|
112
112
|
|
|
113
113
|
- **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista). Copyright © Alessandro Lista. All rights reserved. The logo is not covered by the LGPL-3.0 license of this project.
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
## Attribution request
|
|
116
116
|
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|
|
117
117
|
|
|
118
118
|
*Created with Shine Stacker – https://github.com/lucalista/shinestacker*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=oU1lLCdhmPP8CUfgzMFfhCPjCfk4eDfd8WSwOfXNBOk,21
|
|
3
3
|
shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
|
|
4
4
|
shinestacker/algorithms/align.py,sha256=mb44u-YxZI1TTSHz81nRpX_2c8awlOhnGrK0LyfTQeQ,33543
|
|
5
5
|
shinestacker/algorithms/align_auto.py,sha256=pJetw6zZEWQLouzcelkI8gD4cPiOp887ePXzVbm0E6Q,3800
|
|
@@ -22,16 +22,17 @@ shinestacker/algorithms/vignetting.py,sha256=gJOv-FN3GnTgaVn70W_6d-qbw3WmqinDiO9
|
|
|
22
22
|
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
23
23
|
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
|
|
25
|
+
shinestacker/app/args.py,sha256=TlfMR5GBd6Zz9wZNrN7CGJ1_hm1FnLZn5EoP5mkRHrk,776
|
|
25
26
|
shinestacker/app/gui_utils.py,sha256=fSpkwPXTON_l676UHdAnJNrGq7BPbSlPOiHpOF_LZaI,2519
|
|
26
27
|
shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
|
|
27
|
-
shinestacker/app/main.py,sha256=
|
|
28
|
+
shinestacker/app/main.py,sha256=dcitc5vwIIyIXeDfZwQnC7KHRCdd3FaVJWyaU8mK86c,10935
|
|
28
29
|
shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
|
|
29
|
-
shinestacker/app/project.py,sha256=
|
|
30
|
-
shinestacker/app/retouch.py,sha256=
|
|
30
|
+
shinestacker/app/project.py,sha256=X98pK_mMtE_NefTUZfebEaP1YCsVY97hcQD4bSxuNyY,2777
|
|
31
|
+
shinestacker/app/retouch.py,sha256=wlk-tHaei5YAFinGZWyzBopUhUqyxMT6jSH-4DMEwo8,2659
|
|
31
32
|
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
32
33
|
shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
|
|
33
34
|
shinestacker/config/constants.py,sha256=EEdr7pZg4JpbIjUWaP7kJQfTuBB85FN739myDNAfn8A,8301
|
|
34
|
-
shinestacker/config/gui_constants.py,sha256=
|
|
35
|
+
shinestacker/config/gui_constants.py,sha256=Aqan-AdUjtlARXpsefQvWW2uKVv1tvwc0gfKyo7xud4,2787
|
|
35
36
|
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
36
37
|
shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
|
|
37
38
|
shinestacker/core/core_utils.py,sha256=1LYj19Dfc9jZN9-4dlf1paximDH5WZYa7DXvKr7R7QY,1719
|
|
@@ -50,7 +51,7 @@ shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPu
|
|
|
50
51
|
shinestacker/gui/gui_run.py,sha256=zr7x4BVmM0n_ZRsSEaJVVKvHSWHuwhftgkUvgeg90gU,15767
|
|
51
52
|
shinestacker/gui/main_window.py,sha256=5k_9TiZT9idKCmovUFYpUTSEQQj-DMQrlyq9dAgY1MU,24800
|
|
52
53
|
shinestacker/gui/menu_manager.py,sha256=b5Cxh6uddOlio8i7fRISbGDJI-oe0ds6LIF5dWM7leI,11263
|
|
53
|
-
shinestacker/gui/new_project.py,sha256=
|
|
54
|
+
shinestacker/gui/new_project.py,sha256=z8e3EhRMB-KtoPwYQSiKLSOQ2dS0-Okm7zVw21B7zy8,16391
|
|
54
55
|
shinestacker/gui/project_controller.py,sha256=W4sbBGEPVtfF9F1rC-6Y0oKLq_y94HuFBvZRj87xNKQ,16272
|
|
55
56
|
shinestacker/gui/project_converter.py,sha256=Gmna0HwbvACcXiX74TaQYumif8ZV8sZ2APLTMM-L1mU,7436
|
|
56
57
|
shinestacker/gui/project_editor.py,sha256=lSgQ42IoaobHs-NQQWT88Qhg5l7nu5ejxAO5VgIupr8,25498
|
|
@@ -70,34 +71,35 @@ shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzd
|
|
|
70
71
|
shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
|
|
71
72
|
shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
|
|
72
73
|
shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
-
shinestacker/retouch/base_filter.py,sha256=
|
|
74
|
+
shinestacker/retouch/base_filter.py,sha256=gvRGvhOhhWS05jKOXyNdMnia3b7rscQqR8-xnySIWFc,10260
|
|
74
75
|
shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
|
|
75
76
|
shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
|
|
76
|
-
shinestacker/retouch/brush_preview.py,sha256=
|
|
77
|
+
shinestacker/retouch/brush_preview.py,sha256=cOFVMCbEsgR_alzmr_-LLghtGU_unrE-hAjLHcvrZAY,5484
|
|
77
78
|
shinestacker/retouch/brush_tool.py,sha256=8uVncTA375uC3Nhp2YM0eZjpOR-nN47i2eGjN8tJzOU,8714
|
|
78
79
|
shinestacker/retouch/denoise_filter.py,sha256=TDUHzhRKlKvCa3D5SCYCZKTpjcl81kGwmONsgSDtO1k,440
|
|
79
|
-
shinestacker/retouch/display_manager.py,sha256=
|
|
80
|
+
shinestacker/retouch/display_manager.py,sha256=wwbX4n7ulg53fM3E5QL5YEb5IFV0jN8za6yfYc74anA,8624
|
|
80
81
|
shinestacker/retouch/exif_data.py,sha256=LF-fRXW-reMq-xJ_QRE5j8DC2LVGKIlC6MR3QbC1cdg,1896
|
|
81
82
|
shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
|
|
82
83
|
shinestacker/retouch/filter_manager.py,sha256=SdYIZkZBUvuB6wDG0moGWav5sfEvIcB9ioUJR5wJFts,388
|
|
83
84
|
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
84
|
-
shinestacker/retouch/image_editor_ui.py,sha256=
|
|
85
|
-
shinestacker/retouch/image_view_status.py,sha256=
|
|
86
|
-
shinestacker/retouch/image_viewer.py,sha256=
|
|
87
|
-
shinestacker/retouch/io_gui_handler.py,sha256=
|
|
85
|
+
shinestacker/retouch/image_editor_ui.py,sha256=ifrlHafXdELwCXoVfUiSM7jcz3O6qH7z2WjNpNPEmUs,33505
|
|
86
|
+
shinestacker/retouch/image_view_status.py,sha256=bdIhsXiYXm7eyjkTGWkw5PRShzaF_by-g7daqgmhwjM,1858
|
|
87
|
+
shinestacker/retouch/image_viewer.py,sha256=D7feFlaZkR2nzJ9VcKcVg8lYPS7AMjA_uxJs9L099kQ,4412
|
|
88
|
+
shinestacker/retouch/io_gui_handler.py,sha256=BRQ5eSt1tCMDYtOqxfdYGhx2BLCrncfNrNLGuWIy5Rk,11873
|
|
88
89
|
shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
|
|
89
|
-
shinestacker/retouch/layer_collection.py,sha256=
|
|
90
|
-
shinestacker/retouch/overlaid_view.py,sha256=
|
|
91
|
-
shinestacker/retouch/shortcuts_help.py,sha256=
|
|
92
|
-
shinestacker/retouch/sidebyside_view.py,sha256=
|
|
93
|
-
shinestacker/retouch/
|
|
90
|
+
shinestacker/retouch/layer_collection.py,sha256=fZlGrkm9-Ycc7AOzFSpImhafiTieBeCZRk-UlvlFHbo,5819
|
|
91
|
+
shinestacker/retouch/overlaid_view.py,sha256=0V6Y0wVmf7vPzhB4O9BSF8UwBnw0RdBdYCt_tHD4vls,8478
|
|
92
|
+
shinestacker/retouch/shortcuts_help.py,sha256=BFWTT5QvodqMhqa_9LI25hZqjICfckgyWG4fGrGzvnM,4283
|
|
93
|
+
shinestacker/retouch/sidebyside_view.py,sha256=We4hY_ZGwINPVPHACPGYFPaiU3Khpax7Sf9Xur317FA,17358
|
|
94
|
+
shinestacker/retouch/transformation_manager.py,sha256=NSHGUF-JFv4Y81gSvizjQCTp49TLo1so7c0WoUElO08,1812
|
|
95
|
+
shinestacker/retouch/undo_manager.py,sha256=cKUkqnJtnJ-Hq-LQs5Bv49FC6qkG6XSw9oCVySJ8jS0,4312
|
|
94
96
|
shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
|
|
95
|
-
shinestacker/retouch/view_strategy.py,sha256=
|
|
97
|
+
shinestacker/retouch/view_strategy.py,sha256=5AiSYH_FkeIhaUMtQwZ3v1_8r6KckvOYPFoufEtw12Y,23877
|
|
96
98
|
shinestacker/retouch/vignetting_filter.py,sha256=MA97rQkSL0D-Nh-n2L4AiPR064RoTROkvza4tw84g9U,3658
|
|
97
99
|
shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
|
|
98
|
-
shinestacker-1.
|
|
99
|
-
shinestacker-1.
|
|
100
|
-
shinestacker-1.
|
|
101
|
-
shinestacker-1.
|
|
102
|
-
shinestacker-1.
|
|
103
|
-
shinestacker-1.
|
|
100
|
+
shinestacker-1.5.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
101
|
+
shinestacker-1.5.1.dist-info/METADATA,sha256=FrhfVRsmH4sx7-cQxDajn-zZinLt-UIhhHf2xYZNzvM,6978
|
|
102
|
+
shinestacker-1.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
103
|
+
shinestacker-1.5.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
104
|
+
shinestacker-1.5.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
105
|
+
shinestacker-1.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|