shinestacker 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

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

Potentially problematic release.


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

shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.4.0'
1
+ __version__ = '0.5.0'
@@ -1,15 +1,82 @@
1
- # pylint: disable=C0114, C0116, E0611
1
+ # pylint: disable=C0114, C0116, E0611, W0718
2
+ import json
3
+ from urllib.request import urlopen, Request
4
+ from urllib.error import URLError
2
5
  from PySide6.QtWidgets import QMessageBox
3
6
  from PySide6.QtCore import Qt
4
7
  from .. import __version__
5
8
  from .. config.constants import constants
6
9
 
7
10
 
11
+ def compare_versions(current, latest):
12
+ def parse_version(v):
13
+ v = v.lstrip('v')
14
+ parts = v.split('.')
15
+ result = []
16
+ for part in parts:
17
+ try:
18
+ result.append(int(part))
19
+ except ValueError:
20
+ result.append(part)
21
+ return result
22
+ current_parts = parse_version(current)
23
+ latest_parts = parse_version(latest)
24
+ for i in range(max(len(current_parts), len(latest_parts))):
25
+ c = current_parts[i] if i < len(current_parts) else 0
26
+ l = latest_parts[i] if i < len(latest_parts) else 0 # noqa: E741
27
+ if isinstance(c, int) and isinstance(l, int):
28
+ if c < l:
29
+ return -1
30
+ if c > l:
31
+ return 1
32
+ else:
33
+ if str(c) < str(l):
34
+ return -1
35
+ if str(c) > str(l):
36
+ return 1
37
+ return 0
38
+
39
+
40
+ def get_latest_version():
41
+ try:
42
+ url = "https://api.github.com/repos/lucalista/shinestacker/releases/latest"
43
+ headers = {'User-Agent': 'ShineStacker'}
44
+ req = Request(url, headers=headers)
45
+ with urlopen(req, timeout=5) as response:
46
+ data = json.loads(response.read().decode())
47
+ return data['tag_name']
48
+ except (URLError, ValueError, KeyError, TimeoutError):
49
+ return None
50
+
51
+
8
52
  def show_about_dialog():
9
53
  version_clean = __version__.split("+", maxsplit=1)[0]
54
+ latest_version = None
55
+ try:
56
+ latest_version = get_latest_version()
57
+ except Exception:
58
+ pass
59
+ update_text = ""
60
+ # pyling: disable=XXX
61
+ if latest_version:
62
+ latest_clean = latest_version.lstrip('v')
63
+ if compare_versions(version_clean, latest_clean) < 0:
64
+ update_text = f"""
65
+ <p style="color: red; font-weight: bold;">
66
+ Update available! Latest version: {latest_version}
67
+ <br><a href="https://github.com/lucalista/shinestacker/releases/latest">Download here</a>
68
+ </p>
69
+ """ # noqa E501
70
+ else:
71
+ update_text = f"""
72
+ <p style="color: green; font-weight: bold;">
73
+ You are using the lastet version: {latest_version}.
74
+ </p>
75
+ """
10
76
  about_text = f"""
11
77
  <h3>{constants.APP_TITLE}</h3>
12
78
  <h4>version: v{version_clean}</h4>
79
+ {update_text}
13
80
  <p style='font-weight: normal;'>App and framework to combine multiple images
14
81
  into a single focused image.</p>
15
82
  <p>Author: Luca Lista<br/>
@@ -19,6 +86,7 @@ def show_about_dialog():
19
86
  <li><a href="https://github.com/lucalista/shinestacker">GitHub project repository</a></li>
20
87
  </ul>
21
88
  """
89
+ # pyling: enable=XXX
22
90
  msg = QMessageBox()
23
91
  msg.setWindowTitle(f"About {constants.APP_STRING}")
24
92
  msg.setIcon(QMessageBox.Icon.Information)
@@ -36,9 +36,10 @@ class _GuiConstants:
36
36
 
37
37
  THUMB_WIDTH = 120 # px
38
38
  THUMB_HEIGHT = 80 # px
39
- IMG_WIDTH = 100 # px
40
- IMG_HEIGHT = 80 # px
41
- LABEL_HEIGHT = 20 # px
39
+ THUMB_HI_COLOR = '#0000FF'
40
+ THUMB_LO_COLOR = '#0000FF'
41
+ THUMB_MASTER_HI_COLOR = '#0000FF'
42
+ THUMB_MASTER_LO_COLOR = 'transparent'
42
43
 
43
44
  MAX_UNDO_STEPS = 50
44
45
 
@@ -46,8 +47,9 @@ class _GuiConstants:
46
47
 
47
48
  UI_SIZES = {
48
49
  'brush_preview': (100, 80),
49
- 'thumbnail': (IMG_WIDTH, IMG_HEIGHT),
50
- 'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT)
50
+ 'thumbnail_width': 100,
51
+ 'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT),
52
+ 'label_height': 20
51
53
  }
52
54
 
53
55
  DEFAULT_BRUSH_HARDNESS = 50
@@ -30,6 +30,7 @@ class ActionsWindow(ProjectEditor):
30
30
  def mark_as_modified(self):
31
31
  self._modified_project = True
32
32
  self.project_buffer.append(self.project.clone())
33
+ self.save_actions_set_enabled(True)
33
34
  self.update_title()
34
35
 
35
36
  def close_project(self):
@@ -40,6 +41,7 @@ class ActionsWindow(ProjectEditor):
40
41
  self.job_list.clear()
41
42
  self.action_list.clear()
42
43
  self._modified_project = False
44
+ self.save_actions_set_enabled(False)
43
45
 
44
46
  def new_project(self):
45
47
  if not self._check_unsaved_changes():
@@ -51,8 +53,10 @@ class ActionsWindow(ProjectEditor):
51
53
  self.job_list.clear()
52
54
  self.action_list.clear()
53
55
  self.set_project(Project())
56
+ self.save_actions_set_enabled(False)
54
57
  dialog = NewProjectDialog(self)
55
58
  if dialog.exec() == QDialog.Accepted:
59
+ self.save_actions_set_enabled(True)
56
60
  input_folder = dialog.get_input_folder().split('/')
57
61
  working_path = '/'.join(input_folder[:-1])
58
62
  input_path = input_folder[-1]
@@ -142,6 +146,7 @@ class ActionsWindow(ProjectEditor):
142
146
  if len(self.project.jobs) > 0:
143
147
  self.job_list.setCurrentRow(0)
144
148
  self.activateWindow()
149
+ self.save_actions_set_enabled(True)
145
150
  for job in self.project.jobs:
146
151
  if 'working_path' in job.params.keys():
147
152
  working_path = job.params['working_path']
@@ -256,3 +261,6 @@ class ActionsWindow(ProjectEditor):
256
261
  if dialog.exec() == QDialog.Accepted:
257
262
  self.on_job_selected(self.job_list.currentRow())
258
263
  self.mark_as_modified()
264
+
265
+ def save_actions_set_enabled(self, enabled):
266
+ pass
@@ -189,19 +189,24 @@ class MainWindow(ActionsWindow, LogManager):
189
189
  open_action.setShortcut("Ctrl+O")
190
190
  open_action.triggered.connect(self.open_project)
191
191
  menu.addAction(open_action)
192
- save_action = QAction("&Save", self)
193
- save_action.setShortcut("Ctrl+S")
194
- save_action.triggered.connect(self.save_project)
195
- menu.addAction(save_action)
196
- save_as_action = QAction("Save &As...", self)
197
- save_as_action.setShortcut("Ctrl+Shift+S")
198
- save_as_action.triggered.connect(self.save_project_as)
199
- menu.addAction(save_as_action)
192
+ self.save_action = QAction("&Save", self)
193
+ self.save_action.setShortcut("Ctrl+S")
194
+ self.save_action.triggered.connect(self.save_project)
195
+ menu.addAction(self.save_action)
196
+ self.save_as_action = QAction("Save &As...", self)
197
+ self.save_as_action.setShortcut("Ctrl+Shift+S")
198
+ self.save_as_action.triggered.connect(self.save_project_as)
199
+ menu.addAction(self.save_as_action)
200
+ self.save_actions_set_enabled(False)
200
201
  close_action = QAction("&Close", self)
201
202
  close_action.setShortcut("Ctrl+W")
202
203
  close_action.triggered.connect(self.close_project)
203
204
  menu.addAction(close_action)
204
205
 
206
+ def save_actions_set_enabled(self, enabled):
207
+ self.save_action.setEnabled(enabled)
208
+ self.save_as_action.setEnabled(enabled)
209
+
205
210
  def add_edit_menu(self, menubar):
206
211
  menu = menubar.addMenu("&Edit")
207
212
  undo_action = QAction("&Undo", self)
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0913, R0917, R0914
2
2
  import numpy as np
3
- from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
3
+ from PySide6.QtWidgets import QApplication, QLabel
4
+ from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QFont
4
5
  from PySide6.QtCore import Qt, QPoint
5
6
  from .brush_gradient import create_default_brush_gradient
6
7
  from .. config.gui_constants import gui_constants
@@ -18,11 +19,16 @@ class BrushTool:
18
19
  self.opacity_slider = None
19
20
  self.flow_slider = None
20
21
  self._brush_mask_cache = {}
22
+ self.brush_text = None
21
23
 
22
24
  def setup_ui(self, brush, brush_preview, image_viewer, size_slider, hardness_slider,
23
25
  opacity_slider, flow_slider):
24
26
  self.brush = brush
25
27
  self.brush_preview = brush_preview
28
+ self.brush_text = QLabel(brush_preview.parent())
29
+ self.brush_text.setStyleSheet("color: navy; background: transparent;")
30
+ self.brush_text.setAlignment(Qt.AlignLeft | Qt.AlignTop)
31
+ self.brush_text.raise_()
26
32
  self.image_viewer = image_viewer
27
33
  self.size_slider = size_slider
28
34
  self.hardness_slider = hardness_slider
@@ -86,7 +92,7 @@ class BrushTool:
86
92
  pixmap = QPixmap(width, height)
87
93
  pixmap.fill(Qt.transparent)
88
94
  painter = QPainter(pixmap)
89
- painter.setRenderHint(QPainter.Antialiasing)
95
+ painter.setRenderHint(QPainter.TextAntialiasing, True)
90
96
  preview_size = min(self.brush.size, width + 30, height + 30)
91
97
  center_x, center_y = width // 2, height // 2
92
98
  radius = preview_size // 2
@@ -109,10 +115,21 @@ class BrushTool:
109
115
  painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
110
116
  if self.image_viewer.cursor_style == 'preview':
111
117
  painter.setPen(QPen(QColor(0, 0, 160)))
112
- painter.drawText(0, 10, f"Size: {int(self.brush.size)}px")
113
- painter.drawText(0, 25, f"Hardness: {self.brush.hardness}%")
114
- painter.drawText(0, 40, f"Opacity: {self.brush.opacity}%")
115
- painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
118
+ font = QApplication.font()
119
+ painter.setFont(font)
120
+ font.setHintingPreference(QFont.PreferFullHinting)
121
+ painter.setFont(font)
122
+ self.brush_text.setText(
123
+ f"Size: {int(self.brush.size)}px\n"
124
+ f"Hardness: {self.brush.hardness}%\n"
125
+ f"Opacity: {self.brush.opacity}%\n"
126
+ f"Flow: {self.brush.flow}%"
127
+ )
128
+ self.brush_text.adjustSize()
129
+ self.brush_text.move(10, self.brush_preview.height() // 2 + 125)
130
+ self.brush_text.show()
131
+ else:
132
+ self.brush_text.hide()
116
133
  painter.end()
117
134
  self.brush_preview.setPixmap(pixmap)
118
135
  self.image_viewer.update_brush_cursor()
@@ -1,6 +1,7 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902
2
2
  import numpy as np
3
- from PySide6.QtWidgets import QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog
3
+ from PySide6.QtWidgets import (QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog,
4
+ QAbstractItemView)
4
5
  from PySide6.QtGui import QPixmap, QImage
5
6
  from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
6
7
  from .. config.gui_constants import gui_constants
@@ -39,6 +40,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
39
40
  self.update_timer = QTimer()
40
41
  self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
41
42
  self.update_timer.timeout.connect(self.process_pending_updates)
43
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
42
44
 
43
45
  def process_pending_updates(self):
44
46
  if self.needs_update:
@@ -64,15 +66,15 @@ class DisplayManager(QObject, LayerCollectionHandler):
64
66
  self.display_master_layer()
65
67
 
66
68
  def create_thumbnail(self, layer):
67
- if layer.dtype == np.uint16:
68
- layer = (layer // 256).astype(np.uint8)
69
- height, width = layer.shape[:2]
70
- if layer.ndim == 3 and layer.shape[-1] == 3:
71
- qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
69
+ source_layer = (layer // 256).astype(np.uint8) if layer.dtype == np.uint16 else layer
70
+ height, width = source_layer.shape[:2]
71
+ if layer.ndim == 3 and source_layer.shape[-1] == 3:
72
+ qimg = QImage(source_layer.data, width, height, 3 * width, QImage.Format_RGB888)
72
73
  else:
73
- qimg = QImage(layer.data, width, height, width, QImage.Format_Grayscale8)
74
+ qimg = QImage(source_layer.data, width, height, width, QImage.Format_Grayscale8)
74
75
  return QPixmap.fromImage(
75
- qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
76
+ qimg.scaledToWidth(
77
+ gui_constants.UI_SIZES['thumbnail_width'], Qt.SmoothTransformation))
76
78
 
77
79
  def update_thumbnails(self):
78
80
  self.update_master_thumbnail()
@@ -103,17 +105,22 @@ class DisplayManager(QObject, LayerCollectionHandler):
103
105
  self.master_thumbnail_label.setPixmap(pixmap)
104
106
 
105
107
  def add_thumbnail_item(self, thumbnail, label, i, is_current):
106
- item_widget = QWidget()
107
- layout = QVBoxLayout(item_widget)
108
- layout.setContentsMargins(0, 0, 0, 0)
109
- layout.setSpacing(0)
110
-
108
+ container = QWidget()
109
+ container.setFixedWidth(gui_constants.UI_SIZES['thumbnail_width'] + 4)
110
+ container.setObjectName("thumbnailContainer")
111
+ container_layout = QVBoxLayout(container)
112
+ container_layout.setContentsMargins(2, 2, 2, 2)
113
+ container_layout.setSpacing(0)
114
+ content_widget = QWidget()
115
+ content_layout = QVBoxLayout(content_widget)
116
+ content_layout.setContentsMargins(0, 0, 0, 0)
117
+ content_layout.setSpacing(0)
111
118
  thumbnail_label = QLabel()
112
119
  thumbnail_label.setPixmap(thumbnail)
113
120
  thumbnail_label.setAlignment(Qt.AlignCenter)
114
- layout.addWidget(thumbnail_label)
115
-
121
+ content_layout.addWidget(thumbnail_label)
116
122
  label_widget = ClickableLabel(label)
123
+ label_widget.setFixedHeight(gui_constants.UI_SIZES['label_height'])
117
124
  label_widget.setAlignment(Qt.AlignCenter)
118
125
 
119
126
  def rename_label(label_widget, old_label, i):
@@ -124,21 +131,45 @@ class DisplayManager(QObject, LayerCollectionHandler):
124
131
  self.set_layer_labels(i, new_label)
125
132
 
126
133
  label_widget.double_clicked.connect(lambda: rename_label(label_widget, label, i))
127
- layout.addWidget(label_widget)
134
+ content_layout.addWidget(label_widget)
135
+ container_layout.addWidget(content_widget)
136
+ if is_current:
137
+ container.setStyleSheet(
138
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
139
+ else:
140
+ container.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
128
141
  item = QListWidgetItem()
129
- item.setSizeHint(QSize(gui_constants.IMG_WIDTH, gui_constants.IMG_HEIGHT))
142
+ item.setSizeHint(QSize(gui_constants.UI_SIZES['thumbnail_width'] + 4,
143
+ thumbnail.height() + label_widget.height() + 4))
130
144
  self.thumbnail_list.addItem(item)
131
- self.thumbnail_list.setItemWidget(item, item_widget)
132
-
145
+ self.thumbnail_list.setItemWidget(item, container)
133
146
  if is_current:
134
147
  self.thumbnail_list.setCurrentItem(item)
135
148
 
149
+ def highlight_thumbnail(self, index):
150
+ for i in range(self.thumbnail_list.count()):
151
+ item = self.thumbnail_list.item(i)
152
+ widget = self.thumbnail_list.itemWidget(item)
153
+ if widget:
154
+ widget.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
155
+ current_item = self.thumbnail_list.item(index)
156
+ if current_item:
157
+ widget = self.thumbnail_list.itemWidget(current_item)
158
+ if widget:
159
+ widget.setStyleSheet(
160
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
161
+ self.thumbnail_list.setCurrentRow(index)
162
+ self.thumbnail_list.scrollToItem(
163
+ self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
164
+
136
165
  def set_view_master(self):
137
166
  if self.has_no_master_layer():
138
167
  return
139
168
  self.view_mode = 'master'
140
169
  self.temp_view_individual = False
141
170
  self.display_master_layer()
171
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
172
+ self.highlight_thumbnail(self.current_layer_idx())
142
173
  self.status_message_requested.emit("View mode: Master")
143
174
  self.cursor_preview_state_changed.emit(True) # True = allow preview
144
175
 
@@ -148,6 +179,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
148
179
  self.view_mode = 'individual'
149
180
  self.temp_view_individual = False
150
181
  self.display_current_layer()
182
+ self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
183
+ self.highlight_thumbnail(self.current_layer_idx())
151
184
  self.status_message_requested.emit("View mode: Individual layers")
152
185
  self.cursor_preview_state_changed.emit(False) # False = no preview
153
186
 
@@ -155,6 +188,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
155
188
  if not self.temp_view_individual and self.view_mode == 'master':
156
189
  self.temp_view_individual = True
157
190
  self.image_viewer.update_brush_cursor()
191
+ self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
192
+ self.highlight_thumbnail(self.current_layer_idx())
158
193
  self.display_current_layer()
159
194
  self.status_message_requested.emit("Temporary view: Individual layer (hold X)")
160
195
 
@@ -162,6 +197,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
162
197
  if self.temp_view_individual:
163
198
  self.temp_view_individual = False
164
199
  self.image_viewer.update_brush_cursor()
200
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
201
+ self.highlight_thumbnail(self.current_layer_idx())
165
202
  self.display_master_layer()
166
203
  self.status_message_requested.emit("View mode: Master")
167
204
  self.cursor_preview_state_changed.emit(True) # Restore preview
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902
2
- from PySide6.QtWidgets import QMainWindow, QMessageBox, QAbstractItemView
2
+ from PySide6.QtWidgets import QMainWindow, QMessageBox
3
3
  from .. config.constants import constants
4
4
  from .undo_manager import UndoManager
5
5
  from .layer_collection import LayerCollection
@@ -87,7 +87,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
87
87
  def update_title(self):
88
88
  title = constants.APP_TITLE
89
89
  if self.io_gui_handler is not None:
90
- path = self.io_gui_handler.current_file_path
90
+ path = self.io_gui_handler.current_file_path()
91
91
  if path != '':
92
92
  title += f" - {path.split('/')[-1]}"
93
93
  if self.modified:
@@ -96,6 +96,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
96
96
 
97
97
  def mark_as_modified(self):
98
98
  self.modified = True
99
+ self.save_actions_set_enabled(True)
99
100
  self.update_title()
100
101
 
101
102
  def change_layer(self, layer_idx):
@@ -114,19 +115,14 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
114
115
  new_idx = max(0, self.current_layer_idx() - 1)
115
116
  if new_idx != self.current_layer_idx():
116
117
  self.change_layer(new_idx)
117
- self.highlight_thumbnail(new_idx)
118
+ self.display_manager.highlight_thumbnail(new_idx)
118
119
 
119
120
  def next_layer(self):
120
121
  if self.layer_stack() is not None:
121
122
  new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
122
123
  if new_idx != self.current_layer_idx():
123
124
  self.change_layer(new_idx)
124
- self.highlight_thumbnail(new_idx)
125
-
126
- def highlight_thumbnail(self, index):
127
- self.thumbnail_list.setCurrentRow(index)
128
- self.thumbnail_list.scrollToItem(
129
- self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
125
+ self.display_manager.highlight_thumbnail(new_idx)
130
126
 
131
127
  def copy_layer_to_master(self):
132
128
  if self.layer_stack() is None or self.master_layer() is None:
@@ -23,6 +23,7 @@ def brush_size_to_slider(size):
23
23
 
24
24
  class ImageEditorUI(ImageFilters):
25
25
  def __init__(self):
26
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
26
27
  super().__init__()
27
28
  self.brush = Brush()
28
29
  self.setup_ui()
@@ -125,16 +126,19 @@ class ImageEditorUI(ImageFilters):
125
126
  }
126
127
  """)
127
128
  master_label.setAlignment(Qt.AlignCenter)
128
- master_label.setFixedHeight(gui_constants.LABEL_HEIGHT)
129
+ master_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
129
130
  side_layout.addWidget(master_label)
130
131
  self.master_thumbnail_frame = QFrame()
132
+ self.master_thumbnail_frame.setObjectName("thumbnailContainer")
133
+ self.master_thumbnail_frame.setStyleSheet(
134
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
131
135
  self.master_thumbnail_frame.setFrameShape(QFrame.StyledPanel)
132
136
  master_thumbnail_layout = QVBoxLayout(self.master_thumbnail_frame)
133
- master_thumbnail_layout.setContentsMargins(2, 2, 2, 2)
137
+ master_thumbnail_layout.setContentsMargins(8, 8, 8, 8)
134
138
  self.master_thumbnail_label = QLabel()
135
139
  self.master_thumbnail_label.setAlignment(Qt.AlignCenter)
136
- self.master_thumbnail_label.setFixedSize(
137
- gui_constants.THUMB_WIDTH, gui_constants.THUMB_HEIGHT)
140
+ self.master_thumbnail_label.setFixedWidth(
141
+ gui_constants.UI_SIZES['thumbnail_width'])
138
142
  self.master_thumbnail_label.mousePressEvent = \
139
143
  lambda e: self.display_manager.set_view_master()
140
144
  master_thumbnail_layout.addWidget(self.master_thumbnail_label)
@@ -152,7 +156,7 @@ class ImageEditorUI(ImageFilters):
152
156
  }
153
157
  """)
154
158
  layers_label.setAlignment(Qt.AlignCenter)
155
- layers_label.setFixedHeight(gui_constants.LABEL_HEIGHT)
159
+ layers_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
156
160
  side_layout.addWidget(layers_label)
157
161
  self.thumbnail_list = QListWidget()
158
162
  self.thumbnail_list.setFocusPolicy(Qt.StrongFocus)
@@ -204,18 +208,29 @@ class ImageEditorUI(ImageFilters):
204
208
  layout.setSpacing(2)
205
209
  super().setup_ui()
206
210
 
211
+ def highlight_master_thumbnail(self):
212
+ self.master_thumbnail_frame.setStyleSheet(
213
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
214
+
207
215
  def setup_menu(self):
208
216
  menubar = self.menuBar()
209
217
  file_menu = menubar.addMenu("&File")
210
218
  file_menu.addAction("&Open...", self.io_gui_handler.open_file, "Ctrl+O")
211
- file_menu.addAction("&Save", self.io_gui_handler.save_file, "Ctrl+S")
212
- file_menu.addAction("Save &As...", self.io_gui_handler.save_file_as, "Ctrl+Shift+S")
213
- self.save_master_only = QAction("Save Master &Only", self)
214
- self.save_master_only.setCheckable(True)
215
- self.save_master_only.setChecked(True)
216
- file_menu.addAction(self.save_master_only)
217
-
218
- file_menu.addAction("&Close", self.io_gui_handler.close_file, "Ctrl+W")
219
+ self.save_action = QAction("&Save", self)
220
+ self.save_action.setShortcut("Ctrl+S")
221
+ self.save_action.triggered.connect(self.io_gui_handler.save_file)
222
+ file_menu.addAction(self.save_action)
223
+ self.save_as_action = QAction("Save &As...", self)
224
+ self.save_as_action.setShortcut("Ctrl+Shift+S")
225
+ self.save_as_action.triggered.connect(self.io_gui_handler.save_file_as)
226
+ file_menu.addAction(self.save_as_action)
227
+ self.io_gui_handler.save_master_only = QAction("Save Master &Only", self)
228
+ self.io_gui_handler.save_master_only.setCheckable(True)
229
+ self.io_gui_handler.save_master_only.setChecked(True)
230
+ file_menu.addAction(self.io_gui_handler.save_master_only)
231
+ self.save_actions_set_enabled(False)
232
+
233
+ file_menu.addAction("&Close", self.close_file, "Ctrl+W")
219
234
  file_menu.addSeparator()
220
235
  file_menu.addAction("&Import frames", self.io_gui_handler.import_frames)
221
236
  file_menu.addAction("Import &EXIF data", self.io_gui_handler.select_exif_path)
@@ -271,12 +286,12 @@ class ImageEditorUI(ImageFilters):
271
286
 
272
287
  view_master_action = QAction("View Master", self)
273
288
  view_master_action.setShortcut("M")
274
- view_master_action.triggered.connect(self.display_manager.set_view_master)
289
+ view_master_action.triggered.connect(self.set_view_master)
275
290
  view_menu.addAction(view_master_action)
276
291
 
277
292
  view_individual_action = QAction("View Individual", self)
278
293
  view_individual_action.setShortcut("L")
279
- view_individual_action.triggered.connect(self.display_manager.set_view_individual)
294
+ view_individual_action.triggered.connect(self.set_view_individual)
280
295
  view_menu.addAction(view_individual_action)
281
296
  view_menu.addSeparator()
282
297
 
@@ -334,6 +349,25 @@ class ImageEditorUI(ImageFilters):
334
349
  shortcuts_help_action.triggered.connect(self.shortcuts_help)
335
350
  help_menu.addAction(shortcuts_help_action)
336
351
 
352
+ def save_actions_set_enabled(self, enabled):
353
+ self.save_action.setEnabled(enabled)
354
+ self.save_as_action.setEnabled(enabled)
355
+ self.io_gui_handler.save_master_only.setEnabled(enabled)
356
+
357
+ def close_file(self):
358
+ self.io_gui_handler.close_file()
359
+ self.save_actions_set_enabled(False)
360
+
361
+ def set_view_master(self):
362
+ self.display_manager.set_view_master()
363
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
364
+ self.highlight_master_thumbnail()
365
+
366
+ def set_view_individual(self):
367
+ self.display_manager.set_view_individual()
368
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
369
+ self.highlight_master_thumbnail()
370
+
337
371
  def shortcuts_help(self):
338
372
  self._dialog = ShortcutsHelp(self)
339
373
  self._dialog.exec()
@@ -365,8 +399,12 @@ class ImageEditorUI(ImageFilters):
365
399
  def handle_temp_view(self, start):
366
400
  if start:
367
401
  self.display_manager.start_temp_view()
402
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
403
+ self.highlight_master_thumbnail()
368
404
  else:
369
405
  self.display_manager.end_temp_view()
406
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
407
+ self.highlight_master_thumbnail()
370
408
 
371
409
  def handle_brush_size_change(self, delta):
372
410
  if delta > 0:
@@ -7,7 +7,7 @@ from PySide6.QtGui import QGuiApplication, QCursor
7
7
  from PySide6.QtCore import Qt, QObject, QTimer, Signal
8
8
  from .file_loader import FileLoader
9
9
  from .exif_data import ExifData
10
- from .io_manager import IOManager
10
+ from .io_manager import IOManager, FileMultilayerSaver
11
11
  from .layer_collection import LayerCollectionHandler
12
12
 
13
13
 
@@ -28,7 +28,15 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
28
28
  self.loading_dialog = None
29
29
  self.loading_timer = None
30
30
  self.exif_dialog = None
31
- self.current_file_path = ''
31
+ self.saver_thread = None
32
+ self.saving_dialog = None
33
+ self.saving_timer = None
34
+ self.current_file_path_master = ''
35
+ self.current_file_path_multi = ''
36
+
37
+ def current_file_path(self):
38
+ return self.current_file_path_master if self.save_master_only.isChecked() \
39
+ else self.current_file_path_multi
32
40
 
33
41
  def setup_ui(self, display_manager, image_viewer):
34
42
  self.display_manager = display_manager
@@ -44,16 +52,18 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
44
52
  else:
45
53
  self.set_layer_labels(labels)
46
54
  self.set_master_layer(master_layer)
47
- self.parent().modified = False
48
55
  self.undo_manager.reset()
49
56
  self.blank_layer = np.zeros(master_layer.shape[:2])
50
57
  self.display_manager.update_thumbnails()
51
58
  self.image_viewer.setup_brush_cursor()
52
- self.parent().change_layer(0)
53
59
  self.image_viewer.reset_zoom()
54
- self.status_message_requested.emit(f"Loaded: {self.current_file_path}")
55
- self.parent().thumbnail_list.setFocus()
60
+ self.status_message_requested.emit(f"Loaded: {self.current_file_path()}")
56
61
  self.update_title_requested.emit()
62
+ self.current_file_path_master = ''
63
+ self.current_file_path_multi = ''
64
+ self.parent().mark_as_modified()
65
+ self.parent().change_layer(0)
66
+ self.parent().thumbnail_list.setFocus()
57
67
 
58
68
  def on_file_error(self, error_msg):
59
69
  QApplication.restoreOverrideCursor()
@@ -61,7 +71,23 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
61
71
  self.loading_dialog.accept()
62
72
  self.loading_dialog.deleteLater()
63
73
  QMessageBox.critical(self.parent(), "Error", error_msg)
64
- self.status_message_requested.emit(f"Error loading: {self.current_file_path}")
74
+ self.status_message_requested.emit(f"Error loading: {self.current_file_path()}")
75
+
76
+ def on_multilayer_save_success(self):
77
+ QApplication.restoreOverrideCursor()
78
+ self.saving_timer.stop()
79
+ self.saving_dialog.hide()
80
+ self.saving_dialog.deleteLater()
81
+ self.parent().modified = False
82
+ self.update_title_requested.emit()
83
+ self.status_message_requested.emit(f"Saved multilayer to: {self.current_file_path_multi}")
84
+
85
+ def on_multilayer_save_error(self, error_msg):
86
+ QApplication.restoreOverrideCursor()
87
+ self.saving_timer.stop()
88
+ self.saving_dialog.hide()
89
+ self.saving_dialog.deleteLater()
90
+ QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {error_msg}")
65
91
 
66
92
  def open_file(self, file_paths=None):
67
93
  if file_paths is None:
@@ -77,7 +103,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
77
103
  self.import_frames_from_files(file_paths)
78
104
  return
79
105
  path = file_paths[0] if isinstance(file_paths, list) else file_paths
80
- self.current_file_path = os.path.abspath(path)
106
+ self.current_file_path_master = os.path.abspath(path)
107
+ self.current_file_path_multi = os.path.abspath(path)
81
108
  QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
82
109
  self.loading_dialog = QDialog(self.parent())
83
110
  self.loading_dialog.setWindowTitle("Loading")
@@ -129,13 +156,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
129
156
  self.display_manager.update_thumbnails()
130
157
 
131
158
  def save_file(self):
132
- if self.parent().save_master_only.isChecked():
159
+ if self.save_master_only.isChecked():
133
160
  self.save_master()
134
161
  else:
135
162
  self.save_multilayer()
136
163
 
137
164
  def save_file_as(self):
138
- if self.parent().save_master_only.isChecked():
165
+ if self.save_master_only.isChecked():
139
166
  self.save_master_as()
140
167
  else:
141
168
  self.save_multilayer_as()
@@ -143,11 +170,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
143
170
  def save_multilayer(self):
144
171
  if self.layer_stack() is None:
145
172
  return
146
- if self.current_file_path != '':
147
- extension = self.current_file_path.split('.')[-1]
173
+ if self.current_file_path_multi != '':
174
+ extension = self.current_file_path_multi.split('.')[-1]
148
175
  if extension in ['tif', 'tiff']:
149
- self.save_multilayer_to_path(self.current_file_path)
176
+ self.save_multilayer_to_path(self.current_file_path_multi)
150
177
  return
178
+ else:
179
+ self.save_multilayer_as()
151
180
 
152
181
  def save_multilayer_as(self):
153
182
  if self.layer_stack() is None:
@@ -161,11 +190,30 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
161
190
 
162
191
  def save_multilayer_to_path(self, path):
163
192
  try:
164
- self.io_manager.save_multilayer(path)
165
- self.current_file_path = os.path.abspath(path)
166
- self.parent().modified = False
167
- self.update_title_requested.emit()
168
- self.status_message_requested.emit(f"Saved multilayer to: {path}")
193
+ master_layer = {'Master': self.master_layer().copy()}
194
+ individual_layers = dict(zip(
195
+ self.layer_labels(),
196
+ [layer.copy() for layer in self.layer_stack()]
197
+ ))
198
+ images_dict = {**master_layer, **individual_layers}
199
+ self.saver_thread = FileMultilayerSaver(
200
+ images_dict, path, exif_path=self.io_manager.exif_path)
201
+ self.saver_thread.finished.connect(self.on_multilayer_save_success)
202
+ self.saver_thread.error.connect(self.on_multilayer_save_error)
203
+ QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
204
+ self.saving_dialog = QDialog(self.parent())
205
+ self.saving_dialog.setWindowTitle("Saving")
206
+ self.saving_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
207
+ self.saving_dialog.setModal(True)
208
+ layout = QVBoxLayout()
209
+ layout.addWidget(QLabel("Saving file..."))
210
+ self.saving_dialog.setLayout(layout)
211
+ self.saving_timer = QTimer()
212
+ self.saving_timer.setSingleShot(True)
213
+ self.saving_timer.timeout.connect(self.saving_dialog.show)
214
+ self.saving_timer.start(100)
215
+ self.saver_thread.start()
216
+
169
217
  except Exception as e:
170
218
  traceback.print_tb(e.__traceback__)
171
219
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
@@ -173,8 +221,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
173
221
  def save_master(self):
174
222
  if self.master_layer() is None:
175
223
  return
176
- if self.current_file_path != '':
177
- self.save_master_to_path(self.current_file_path)
224
+ if self.current_file_path_master != '':
225
+ self.save_master_to_path(self.current_file_path_master)
178
226
  return
179
227
  self.save_master_as()
180
228
 
@@ -190,7 +238,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
190
238
  def save_master_to_path(self, path):
191
239
  try:
192
240
  self.io_manager.save_master(path)
193
- self.current_file_path = os.path.abspath(path)
241
+ self.current_file_path_master = os.path.abspath(path)
194
242
  self.parent().modified = False
195
243
  self.update_title_requested.emit()
196
244
  self.status_message_requested.emit(f"Saved master layer to: {path}")
@@ -211,7 +259,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
211
259
  self.set_master_layer(None)
212
260
  self.blank_layer = None
213
261
  self.layer_collection.reset()
214
- self.current_file_path = ''
262
+ self.current_file_path_master = ''
263
+ self.current_file_path_multi = ''
215
264
  self.parent().modified = False
216
265
  self.undo_manager.reset()
217
266
  self.image_viewer.clear_image()
@@ -1,11 +1,33 @@
1
- # pylint: disable=E1101, C0114, C0115, C0116
1
+ # pylint: disable=E1101, C0114, C0115, C0116, E0611, W0718, R0903
2
+ import traceback
2
3
  import cv2
4
+ from PySide6.QtCore import QThread, Signal
3
5
  from .. algorithms.utils import read_img, validate_image, get_img_metadata
4
6
  from .. algorithms.exif import get_exif, write_image_with_exif_data
5
7
  from .. algorithms.multilayer import write_multilayer_tiff_from_images
6
8
  from .layer_collection import LayerCollectionHandler
7
9
 
8
10
 
11
+ class FileMultilayerSaver(QThread):
12
+ finished = Signal()
13
+ error = Signal(str)
14
+
15
+ def __init__(self, images_dict, path, exif_path=None):
16
+ super().__init__()
17
+ self.images_dict = images_dict
18
+ self.path = path
19
+ self.exif_path = exif_path
20
+
21
+ def run(self):
22
+ try:
23
+ write_multilayer_tiff_from_images(
24
+ self.images_dict, self.path, exif_path=self.exif_path)
25
+ self.finished.emit()
26
+ except Exception as e:
27
+ traceback.print_tb(e.__traceback__)
28
+ self.error.emit(str(e))
29
+
30
+
9
31
  class IOManager(LayerCollectionHandler):
10
32
  def __init__(self, layer_collection):
11
33
  super().__init__(layer_collection)
@@ -38,12 +60,6 @@ class IOManager(LayerCollectionHandler):
38
60
  raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
39
61
  return stack, labels, master
40
62
 
41
- def save_multilayer(self, path):
42
- master_layer = {'Master': self.master_layer()}
43
- individual_layers = dict(zip(self.layer_labels(), self.layer_stack()))
44
- write_multilayer_tiff_from_images({**master_layer, **individual_layers},
45
- path, exif_path=self.exif_path)
46
-
47
63
  def save_master(self, path):
48
64
  img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
49
65
  write_image_with_exif_data(self.exif_data, img, path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -1,5 +1,5 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=DObMj8zITWgJRRICOQXNFEgLDtZ9uQZUVwbNAU-P3oc,21
2
+ shinestacker/_version.py,sha256=LyVsN6QRbZCjxbel-HtG6unyJHf29KfzURL0WnqwB_I,21
3
3
  shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
4
4
  shinestacker/algorithms/align.py,sha256=FKGcDrp20jubY-LWA1OBqO-V781AoP8jCLH5xZm9nhk,17902
5
5
  shinestacker/algorithms/balance.py,sha256=y68L5h8yuIuGZI3g5Zhj-jLXXOPsxHVGEhNTbCc2tlI,16518
@@ -17,7 +17,7 @@ shinestacker/algorithms/utils.py,sha256=VLm6eZmcAk2QPvomT4d1q56laJSYfbCQmiwI2Rmu
17
17
  shinestacker/algorithms/vignetting.py,sha256=wFwi20ob1O3Memav1XQrtrOHgOtKRiK1RV4E-ex69r8,7470
18
18
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
19
19
  shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- shinestacker/app/about_dialog.py,sha256=QzZgTcLvkSP3_FhmPOUnwQ_YSxwJdeFrU2IAVYKDgeg,1050
20
+ shinestacker/app/about_dialog.py,sha256=Y_gt28U3OPkKSkDdlA4kN1GCM6zkB-QJ5-c2qdeyG1Y,3321
21
21
  shinestacker/app/app_config.py,sha256=eTIRxp0t7Wic46jMTe_oY3kz7ktZbdM43C3bjshVDKg,494
22
22
  shinestacker/app/gui_utils.py,sha256=ptbUKjv5atbx5vW912_j8BVmDZpovAqZDEC48d0R2vA,2331
23
23
  shinestacker/app/help_menu.py,sha256=UOlabEY_EKV2Q1BoiU2JAM1udSSBAwXlL7d58bqxKe0,516
@@ -28,7 +28,7 @@ shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2
28
28
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
29
29
  shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
30
30
  shinestacker/config/constants.py,sha256=MeZ15b7xIYJoN6EeiuR_OKi4sP-7_E7OtzrETzIowZI,5976
31
- shinestacker/config/gui_constants.py,sha256=002r96jtxV4Acel7q5NgECrcsDJzW-kOStEHqam-5Gg,2492
31
+ shinestacker/config/gui_constants.py,sha256=XnjA65a1anlFXLm1zMP01Z8nZRBWX0D35FVxKnW2pJg,2568
32
32
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
33
33
  shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
34
34
  shinestacker/core/core_utils.py,sha256=ulJhzen5McAb5n6wWNA_KB4U_PdTEr-H2TCQkVKUaOw,1421
@@ -37,12 +37,12 @@ shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7K
37
37
  shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
38
38
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  shinestacker/gui/action_config.py,sha256=zWzTVkySEYfODJ620wQk6B2dr1C-YSxtDicJ0XXrU_M,48955
40
- shinestacker/gui/actions_window.py,sha256=-ehMkGshsH22HSnn33ThAMXy7tR_cqWr14mEnXDTfXk,12025
40
+ shinestacker/gui/actions_window.py,sha256=wJ_s_acXlftwOgnttT5TWVr3dq451TLL-98dmmPYAJ4,12326
41
41
  shinestacker/gui/colors.py,sha256=zgLRcC3fAzklx7zzyjLEsMX2i64YTxGUmQM2woYBZuw,1344
42
42
  shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
43
43
  shinestacker/gui/gui_logging.py,sha256=ciuwZU-_5TicPpjC83iZmcwuDWiBO17onYJRGyF0FaY,8227
44
44
  shinestacker/gui/gui_run.py,sha256=n0OaPZn9C4SVpopMNKUpksMwV27xT0TGFn1inhmCFIk,16230
45
- shinestacker/gui/main_window.py,sha256=Na3TVSWwzIJ5lpCAf1vjkCIo8tKfhYzFO_nxB8ekVJI,28589
45
+ shinestacker/gui/main_window.py,sha256=0CDi_uHHGiz1_htLNZ_iApywEaEYkNXnTcGfvOcgk_s,28817
46
46
  shinestacker/gui/new_project.py,sha256=GSqYfonv-jdOEb4veXU6LxiDsAevr1TulzT0vsVazAk,8342
47
47
  shinestacker/gui/project_converter.py,sha256=zZfXZg2h-PHh2Prr450B1UFADbZPzMBVkYhcpZkqPuk,7370
48
48
  shinestacker/gui/project_editor.py,sha256=zwmj7PFs7X06GY4tkoDBcOL4Tl0IGo4Mf13n2qGwaJY,22245
@@ -61,27 +61,27 @@ shinestacker/retouch/base_filter.py,sha256=74GmzLjpPtn0Um0YOS8qLC1ZvhbvQX4L_AEDk
61
61
  shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
62
62
  shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
63
63
  shinestacker/retouch/brush_preview.py,sha256=KlUOqA1uvLZRsz2peJ9NgsukyzsppJUw3XXr0NFCuhQ,5181
64
- shinestacker/retouch/brush_tool.py,sha256=kwX58_gC_Fep6zowDqOs3nG2wCIc8wrJdokDADGm6K0,8016
64
+ shinestacker/retouch/brush_tool.py,sha256=m3qxyGipJth-lhB8syGSKmBPtXTQaIyaeJnpuCY3mSA,8694
65
65
  shinestacker/retouch/denoise_filter.py,sha256=eO0Cxo9xwsuiE6-JiWCFB5jf6U1kf2N3ftsDAEQ5sek,1982
66
- shinestacker/retouch/display_manager.py,sha256=54NRdlyVj_7GzJ7hmKf8Hnf3lJJx2jVSSpWedj-5pIc,7298
66
+ shinestacker/retouch/display_manager.py,sha256=XPbOBmoYc_jNA791WkWkOSaFHb0ztCZechl2p2KSlwQ,9597
67
67
  shinestacker/retouch/exif_data.py,sha256=uA9ck9skp8ztSUdX1SFrApgtqmxrHtfWW3vsry82H94,2026
68
68
  shinestacker/retouch/file_loader.py,sha256=723A_2w3cjn4rhvAzCq-__SWFelDRsMhkazgnb2h7Ig,4810
69
69
  shinestacker/retouch/filter_manager.py,sha256=SkioWTr6iFFpugUgZLg0a3m5b9EHdZAeyNFy39qk0z8,453
70
70
  shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
71
- shinestacker/retouch/image_editor.py,sha256=PxVLyXRoZVT9GmVIbiYDdfSUKqtQossmoMj7bYv97FE,8492
72
- shinestacker/retouch/image_editor_ui.py,sha256=eacZAnU7Gh4Mri0DCLzBll6kCO4hkl89xysuMyndOQ0,15742
71
+ shinestacker/retouch/image_editor.py,sha256=ob2h-g9yIlFBMyEkVAkRRqiDpwykaUAMjpzAxKAtCRE,8336
72
+ shinestacker/retouch/image_editor_ui.py,sha256=fjHAs7x_4jbL5WVBVEzyW3TQMcrITU3ZmglxAwen51o,17520
73
73
  shinestacker/retouch/image_filters.py,sha256=JF2a7VATO3CGQr5_OOIPi2k7b9HvHzrhhWS73x32t-A,2883
74
74
  shinestacker/retouch/image_viewer.py,sha256=4kovjI8s5MWWA98oqNiQfe4xZvNRL_-UwnmSVK9r-gE,18639
75
- shinestacker/retouch/io_gui_handler.py,sha256=hAgjCiYU1lopmzrIXHQggowj1D5yGncNG2EMdLroORc,9072
76
- shinestacker/retouch/io_manager.py,sha256=QdxR6fbfx_J7uM-Yoptdp17jlmk45R30wmDE9ACRm_8,2112
75
+ shinestacker/retouch/io_gui_handler.py,sha256=iEnyZ0Q1VJCXM5xf0rBFhBn4hFmGxOlIYjH8u1zDnT4,11348
76
+ shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
77
77
  shinestacker/retouch/layer_collection.py,sha256=cvAW6nbG-KdhbN6XI4SrhGwvqTYGPyrZBLWz-uZkIJ0,5672
78
78
  shinestacker/retouch/shortcuts_help.py,sha256=dlt7OSAr9thYuoEPlirTU_YRzv5xP9vy2-9mZO7GVAA,3308
79
79
  shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
80
80
  shinestacker/retouch/unsharp_mask_filter.py,sha256=hNJlqXYjf9Nd8KlVy09fd4TxrHa9Ofef0ZLSMHjLL6I,3481
81
81
  shinestacker/retouch/white_balance_filter.py,sha256=2krwdz0X6qLWuCIEQcPtSQA_txfAsl7QUzfdsOLBrBU,4878
82
- shinestacker-0.4.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
83
- shinestacker-0.4.0.dist-info/METADATA,sha256=SV1_0dnrc1f9gXLgqyE7AqTMLnxGDsodLfU-u9cbmak,5202
84
- shinestacker-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
- shinestacker-0.4.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
86
- shinestacker-0.4.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
87
- shinestacker-0.4.0.dist-info/RECORD,,
82
+ shinestacker-0.5.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
83
+ shinestacker-0.5.0.dist-info/METADATA,sha256=m4TX-tMSojm4PPLWN_MuOixF0daNqkVC1_igTn-tjDA,5202
84
+ shinestacker-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ shinestacker-0.5.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
86
+ shinestacker-0.5.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
87
+ shinestacker-0.5.0.dist-info/RECORD,,