shinestacker 1.9.0__py3-none-any.whl → 1.9.2__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/algorithms/align.py +18 -1
- shinestacker/algorithms/align_parallel.py +9 -5
- shinestacker/algorithms/exif.py +498 -255
- shinestacker/algorithms/stack.py +8 -4
- shinestacker/app/main.py +1 -1
- shinestacker/gui/action_config_dialog.py +0 -4
- shinestacker/gui/config_dialog.py +6 -5
- shinestacker/gui/main_window.py +6 -0
- shinestacker/gui/menu_manager.py +8 -2
- shinestacker/retouch/brush_tool.py +20 -0
- shinestacker/retouch/exif_data.py +57 -37
- shinestacker/retouch/image_editor_ui.py +79 -3
- shinestacker/retouch/image_viewer.py +6 -1
- shinestacker/retouch/io_gui_handler.py +9 -12
- shinestacker/retouch/shortcuts_help.py +15 -8
- shinestacker/retouch/view_strategy.py +12 -2
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/METADATA +1 -1
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/RECORD +23 -23
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/WHEEL +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/top_level.txt +0 -0
shinestacker/algorithms/stack.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, R0913, R0917
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, R0913, R0917, W0718
|
|
2
2
|
import os
|
|
3
3
|
import traceback
|
|
4
4
|
import logging
|
|
@@ -82,6 +82,10 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def get_bunches(collection, n_frames, n_overlap):
|
|
85
|
+
if n_frames == n_overlap:
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
f"Can't get bunch collection, total number of frames ({n_frames}) "
|
|
88
|
+
"is equal to the number of overlapping grames")
|
|
85
89
|
bunches = [collection[x:x + n_frames]
|
|
86
90
|
for x in range(0, len(collection) - n_overlap, n_frames - n_overlap)]
|
|
87
91
|
return bunches
|
|
@@ -109,7 +113,7 @@ class FocusStackBunch(SequentialTask, FocusStackBase):
|
|
|
109
113
|
|
|
110
114
|
def begin(self):
|
|
111
115
|
SequentialTask.begin(self)
|
|
112
|
-
self._chunks = get_bunches(self.input_filepaths(), self.frames, self.overlap)
|
|
116
|
+
self._chunks = get_bunches(sorted(self.input_filepaths()), self.frames, self.overlap)
|
|
113
117
|
self.set_counts(len(self._chunks))
|
|
114
118
|
|
|
115
119
|
def end(self):
|
|
@@ -122,9 +126,9 @@ class FocusStackBunch(SequentialTask, FocusStackBase):
|
|
|
122
126
|
self.print_message(
|
|
123
127
|
color_str(f"fusing bunch: {action_count + 1}/{self.total_action_counts}",
|
|
124
128
|
constants.LOG_COLOR_LEVEL_2))
|
|
125
|
-
img_files = self._chunks[action_count
|
|
129
|
+
img_files = self._chunks[action_count]
|
|
126
130
|
self.stack_algo.init(img_files)
|
|
127
|
-
self.focus_stack(self._chunks[action_count
|
|
131
|
+
self.focus_stack(self._chunks[action_count])
|
|
128
132
|
return True
|
|
129
133
|
|
|
130
134
|
|
shinestacker/app/main.py
CHANGED
|
@@ -199,7 +199,7 @@ class MainApp(QMainWindow):
|
|
|
199
199
|
class Application(QApplication):
|
|
200
200
|
def event(self, event):
|
|
201
201
|
if event.type() == QEvent.Quit and event.spontaneous():
|
|
202
|
-
if not self.quit():
|
|
202
|
+
if not self.main_app.quit():
|
|
203
203
|
return True
|
|
204
204
|
return super().event(event)
|
|
205
205
|
|
|
@@ -372,10 +372,6 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
|
|
|
372
372
|
self.add_field_to_layout(
|
|
373
373
|
self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
|
|
374
374
|
default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
|
|
375
|
-
self.add_field_to_layout(
|
|
376
|
-
self.general_tab_layout, 'scratch_output_dir', FIELD_BOOL,
|
|
377
|
-
'Scratch output folder before run',
|
|
378
|
-
required=False, default=True)
|
|
379
375
|
self.add_field_to_layout(
|
|
380
376
|
self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
|
|
381
377
|
'Delete output at end of job',
|
|
@@ -12,6 +12,7 @@ class ConfigDialog(QDialog):
|
|
|
12
12
|
self.form_layout = create_form_layout(self)
|
|
13
13
|
scroll_area = QScrollArea()
|
|
14
14
|
scroll_area.setWidgetResizable(True)
|
|
15
|
+
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
15
16
|
container_widget = QWidget()
|
|
16
17
|
self.container_layout = QFormLayout(container_widget)
|
|
17
18
|
self.container_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
@@ -19,19 +20,19 @@ class ConfigDialog(QDialog):
|
|
|
19
20
|
self.container_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
20
21
|
self.container_layout.setLabelAlignment(Qt.AlignLeft)
|
|
21
22
|
scroll_area.setWidget(container_widget)
|
|
22
|
-
button_box = QHBoxLayout()
|
|
23
|
+
self.button_box = QHBoxLayout()
|
|
23
24
|
self.ok_button = QPushButton("OK")
|
|
24
25
|
self.ok_button.setFocus()
|
|
25
26
|
self.cancel_button = QPushButton("Cancel")
|
|
26
27
|
self.reset_button = QPushButton("Reset")
|
|
27
|
-
button_box.addWidget(self.ok_button)
|
|
28
|
-
button_box.addWidget(self.cancel_button)
|
|
29
|
-
button_box.addWidget(self.reset_button)
|
|
28
|
+
self.button_box.addWidget(self.ok_button)
|
|
29
|
+
self.button_box.addWidget(self.cancel_button)
|
|
30
|
+
self.button_box.addWidget(self.reset_button)
|
|
30
31
|
self.reset_button.clicked.connect(self.reset_to_defaults)
|
|
31
32
|
self.ok_button.clicked.connect(self.accept)
|
|
32
33
|
self.cancel_button.clicked.connect(self.reject)
|
|
33
34
|
self.form_layout.addRow(scroll_area)
|
|
34
|
-
self.form_layout.addRow(button_box)
|
|
35
|
+
self.form_layout.addRow(self.button_box)
|
|
35
36
|
QTimer.singleShot(0, self.adjust_dialog_size)
|
|
36
37
|
self.create_form_content()
|
|
37
38
|
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -318,6 +318,12 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
318
318
|
edit_config_action.triggered.connect(self.edit_current_action)
|
|
319
319
|
menu.addAction(edit_config_action)
|
|
320
320
|
menu.addSeparator()
|
|
321
|
+
menu.addAction(self.menu_manager.cut_action)
|
|
322
|
+
menu.addAction(self.menu_manager.copy_action)
|
|
323
|
+
menu.addAction(self.menu_manager.paste_action)
|
|
324
|
+
menu.addAction(self.menu_manager.duplicate_action)
|
|
325
|
+
menu.addAction(self.menu_manager.delete_element_action)
|
|
326
|
+
menu.addSeparator()
|
|
321
327
|
menu.addAction(self.menu_manager.run_job_action)
|
|
322
328
|
menu.addAction(self.menu_manager.run_all_jobs_action)
|
|
323
329
|
menu.addSeparator()
|
shinestacker/gui/menu_manager.py
CHANGED
|
@@ -123,8 +123,14 @@ class MenuManager(QObject):
|
|
|
123
123
|
self.undo_action = self.action("&Undo")
|
|
124
124
|
self.undo_action.setEnabled(False)
|
|
125
125
|
menu.addAction(self.undo_action)
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
self.cut_action = self.action("&Cut", requires_file=True)
|
|
127
|
+
menu.addAction(self.cut_action)
|
|
128
|
+
self.copy_action = self.action("Cop&y", requires_file=True)
|
|
129
|
+
menu.addAction(self.copy_action)
|
|
130
|
+
self.paste_action = self.action("&Paste", requires_file=True)
|
|
131
|
+
menu.addAction(self.paste_action)
|
|
132
|
+
self.duplicate_action = self.action("Duplicate", requires_file=True)
|
|
133
|
+
menu.addAction(self.duplicate_action)
|
|
128
134
|
self.delete_element_action = self.action("Delete", requires_file=True)
|
|
129
135
|
self.delete_element_action.setEnabled(False)
|
|
130
136
|
menu.addAction(self.delete_element_action)
|
|
@@ -75,6 +75,26 @@ class BrushTool:
|
|
|
75
75
|
self.hardness_slider.setValue(val)
|
|
76
76
|
self.update_brush_hardness(val)
|
|
77
77
|
|
|
78
|
+
def increase_brush_opacity(self, amount=2):
|
|
79
|
+
val = min(self.opacity_slider.value() + amount, self.opacity_slider.maximum())
|
|
80
|
+
self.opacity_slider.setValue(val)
|
|
81
|
+
self.update_brush_opacity(val)
|
|
82
|
+
|
|
83
|
+
def decrease_brush_opacity(self, amount=2):
|
|
84
|
+
val = max(self.opacity_slider.value() - amount, self.opacity_slider.minimum())
|
|
85
|
+
self.opacity_slider.setValue(val)
|
|
86
|
+
self.update_brush_opacity(val)
|
|
87
|
+
|
|
88
|
+
def increase_brush_flow(self, amount=2):
|
|
89
|
+
val = min(self.flow_slider.value() + amount, self.flow_slider.maximum())
|
|
90
|
+
self.flow_slider.setValue(val)
|
|
91
|
+
self.update_brush_flow(val)
|
|
92
|
+
|
|
93
|
+
def decrease_brush_flow(self, amount=2):
|
|
94
|
+
val = max(self.flow_slider.value() - amount, self.flow_slider.minimum())
|
|
95
|
+
self.flow_slider.setValue(val)
|
|
96
|
+
self.update_brush_flow(val)
|
|
97
|
+
|
|
78
98
|
def update_brush_hardness(self, hardness):
|
|
79
99
|
self.brush.hardness = hardness
|
|
80
100
|
self.update_brush_thumb()
|
|
@@ -1,55 +1,75 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718
|
|
2
|
+
from xml.dom import minidom
|
|
2
3
|
from PIL.TiffImagePlugin import IFDRational
|
|
3
|
-
from PySide6.QtWidgets import
|
|
4
|
+
from PySide6.QtWidgets import QLabel, QTextEdit
|
|
4
5
|
from PySide6.QtCore import Qt
|
|
6
|
+
from PySide6.QtGui import QFontDatabase
|
|
5
7
|
from .. algorithms.exif import exif_dict
|
|
6
|
-
from .
|
|
7
|
-
from .. gui.base_form_dialog import BaseFormDialog
|
|
8
|
+
from .. gui.config_dialog import ConfigDialog
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class ExifData(
|
|
11
|
-
def __init__(self, exif, parent=None):
|
|
12
|
-
super().__init__("EXIF data", parent=parent)
|
|
11
|
+
class ExifData(ConfigDialog):
|
|
12
|
+
def __init__(self, exif, title="EXIF Data", parent=None, show_buttons=True):
|
|
13
13
|
self.exif = exif
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
ok_button.setFocus()
|
|
21
|
-
button_layout.addWidget(ok_button)
|
|
22
|
-
self.add_row_to_layout(button_container)
|
|
23
|
-
ok_button.clicked.connect(self.accept)
|
|
14
|
+
super().__init__(title, parent)
|
|
15
|
+
self.reset_button.setVisible(False)
|
|
16
|
+
self.cancel_button.setVisible(show_buttons)
|
|
17
|
+
if not show_buttons:
|
|
18
|
+
self.ok_button.setFixedWidth(100)
|
|
19
|
+
self.button_box.setAlignment(Qt.AlignCenter)
|
|
24
20
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
def is_likely_xml(self, text):
|
|
22
|
+
if not isinstance(text, str):
|
|
23
|
+
return False
|
|
24
|
+
text = text.strip()
|
|
25
|
+
return (text.startswith('<?xml') or
|
|
26
|
+
text.startswith('<x:xmpmeta') or
|
|
27
|
+
text.startswith('<rdf:RDF') or
|
|
28
|
+
text.startswith('<?xpacket') or
|
|
29
|
+
(text.startswith('<') and text.endswith('>') and
|
|
30
|
+
any(tag in text for tag in ['<rdf:', '<xmp:', '<dc:', '<tiff:'])))
|
|
29
31
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
+
def prettify_xml(self, xml_string):
|
|
33
|
+
try:
|
|
34
|
+
parsed = minidom.parseString(xml_string)
|
|
35
|
+
pretty_xml = parsed.toprettyxml(indent=" ")
|
|
36
|
+
lines = [line for line in pretty_xml.split('\n') if line.strip()]
|
|
37
|
+
if lines and lines[0].startswith('<?xml version="1.0" ?>'):
|
|
38
|
+
lines = lines[1:]
|
|
39
|
+
return '\n'.join(lines)
|
|
40
|
+
except Exception:
|
|
41
|
+
return xml_string
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
spacer.setFixedHeight(10)
|
|
35
|
-
self.form_layout.addRow(spacer)
|
|
36
|
-
self.add_bold_label("EXIF data")
|
|
37
|
-
shortcuts = {}
|
|
43
|
+
def create_form_content(self):
|
|
38
44
|
if self.exif is None:
|
|
39
|
-
shortcuts['Warning:'] = 'no EXIF data found'
|
|
40
45
|
data = {}
|
|
41
46
|
else:
|
|
42
47
|
data = exif_dict(self.exif)
|
|
43
48
|
if len(data) > 0:
|
|
44
49
|
for k, (_, d) in data.items():
|
|
45
|
-
print(k, type(d))
|
|
46
50
|
if isinstance(d, IFDRational):
|
|
47
51
|
d = f"{d.numerator}/{d.denominator}"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
d_str = str(d)
|
|
53
|
+
if "<<<" not in d_str and k != 'IPTCNAA':
|
|
54
|
+
if len(d_str) <= 40:
|
|
55
|
+
self.container_layout.addRow(f"<b>{k}:</b>", QLabel(d_str))
|
|
56
|
+
else:
|
|
57
|
+
if self.is_likely_xml(d_str):
|
|
58
|
+
d_str = self.prettify_xml(d_str)
|
|
59
|
+
text_edit = QTextEdit()
|
|
60
|
+
text_edit.setPlainText(d_str)
|
|
61
|
+
text_edit.setReadOnly(True)
|
|
62
|
+
text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
63
|
+
text_edit.setLineWrapMode(QTextEdit.WidgetWidth)
|
|
64
|
+
text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
65
|
+
text_edit.setFixedWidth(400)
|
|
66
|
+
font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
|
|
67
|
+
font.setPointSize(10)
|
|
68
|
+
text_edit.setFont(font)
|
|
69
|
+
font.setPointSize(11)
|
|
70
|
+
text_edit.setFont(font)
|
|
71
|
+
text_edit.setFixedHeight(200)
|
|
72
|
+
text_edit.setFixedHeight(100)
|
|
73
|
+
self.container_layout.addRow(f"<b>{k}:</b>", text_edit)
|
|
54
74
|
else:
|
|
55
|
-
self.
|
|
75
|
+
self.container_layout.addRow("No EXIF Data", QLabel(''))
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904, W0108
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904, W0108, R0911
|
|
2
2
|
from functools import partial
|
|
3
3
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QMenu,
|
|
4
|
-
QListWidget, QSlider, QMainWindow, QMessageBox
|
|
4
|
+
QFileDialog, QListWidget, QSlider, QMainWindow, QMessageBox,
|
|
5
|
+
QDialog)
|
|
5
6
|
from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
|
|
6
7
|
from PySide6.QtCore import Qt
|
|
7
8
|
from PySide6.QtGui import QGuiApplication
|
|
@@ -9,6 +10,7 @@ from .. config.constants import constants
|
|
|
9
10
|
from .. config.app_config import AppConfig
|
|
10
11
|
from .. config.gui_constants import gui_constants
|
|
11
12
|
from .. gui.recent_file_manager import RecentFileManager
|
|
13
|
+
from .. algorithms.exif import get_exif
|
|
12
14
|
from .image_viewer import ImageViewer
|
|
13
15
|
from .shortcuts_help import ShortcutsHelp
|
|
14
16
|
from .brush import Brush
|
|
@@ -26,6 +28,7 @@ from .white_balance_filter import WhiteBalanceFilter
|
|
|
26
28
|
from .vignetting_filter import VignettingFilter
|
|
27
29
|
from .adjustments import LumiContrastFilter, SaturationVibranceFilter
|
|
28
30
|
from .transformation_manager import TransfromationManager
|
|
31
|
+
from .exif_data import ExifData
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
@@ -57,6 +60,9 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
57
60
|
self.handle_temp_view,
|
|
58
61
|
self.end_copy_brush_area,
|
|
59
62
|
self.handle_brush_size_change,
|
|
63
|
+
self.handle_brush_hardness_change,
|
|
64
|
+
self.handle_brush_opacity_change,
|
|
65
|
+
self.handle_brush_flow_change,
|
|
60
66
|
self.handle_needs_update)
|
|
61
67
|
side_panel = QWidget()
|
|
62
68
|
side_layout = QVBoxLayout(side_panel)
|
|
@@ -183,6 +189,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
183
189
|
self.thumbnail_list.setFixedWidth(gui_constants.THUMB_WIDTH)
|
|
184
190
|
self.thumbnail_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
185
191
|
self.thumbnail_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
192
|
+
self.exif_dialog = None
|
|
186
193
|
|
|
187
194
|
def change_layer_item(item):
|
|
188
195
|
layer_idx = self.thumbnail_list.row(item)
|
|
@@ -266,8 +273,17 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
266
273
|
|
|
267
274
|
file_menu.addAction("&Close", self.close_file, "Ctrl+W")
|
|
268
275
|
file_menu.addSeparator()
|
|
276
|
+
show_exif_action = QAction("Show EXIF Data", self)
|
|
277
|
+
show_exif_action.triggered.connect(self.show_exif_data)
|
|
278
|
+
show_exif_action.setProperty("requires_file", True)
|
|
279
|
+
file_menu.addAction(show_exif_action)
|
|
280
|
+
delete_exif_action = QAction("Delete EXIF Data", self)
|
|
281
|
+
delete_exif_action.triggered.connect(self.delete_exif_data)
|
|
282
|
+
delete_exif_action.setProperty("requires_file", True)
|
|
283
|
+
file_menu.addAction(delete_exif_action)
|
|
284
|
+
file_menu.addSeparator()
|
|
269
285
|
file_menu.addAction("&Import Frames", self.io_gui_handler.import_frames)
|
|
270
|
-
file_menu.addAction("Import &EXIF Data", self.
|
|
286
|
+
file_menu.addAction("Import &EXIF Data", self.select_exif_path)
|
|
271
287
|
|
|
272
288
|
edit_menu = menubar.addMenu("&Edit")
|
|
273
289
|
self.undo_action = QAction("Undo", self)
|
|
@@ -595,6 +611,18 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
595
611
|
if event.text() == '}':
|
|
596
612
|
self.brush_tool.increase_brush_hardness()
|
|
597
613
|
return
|
|
614
|
+
if event.text() == ',':
|
|
615
|
+
self.brush_tool.decrease_brush_opacity()
|
|
616
|
+
return
|
|
617
|
+
if event.text() == '.':
|
|
618
|
+
self.brush_tool.increase_brush_opacity()
|
|
619
|
+
return
|
|
620
|
+
if event.text() == ';':
|
|
621
|
+
self.brush_tool.decrease_brush_flow()
|
|
622
|
+
return
|
|
623
|
+
if event.text() == ':':
|
|
624
|
+
self.brush_tool.increase_brush_flow()
|
|
625
|
+
return
|
|
598
626
|
super().keyPressEvent(event)
|
|
599
627
|
# pylint: enable=C0103
|
|
600
628
|
|
|
@@ -676,6 +704,36 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
676
704
|
self.redo_action.setText("Redo")
|
|
677
705
|
self.redo_action.setEnabled(False)
|
|
678
706
|
|
|
707
|
+
def select_exif_path(self):
|
|
708
|
+
path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
|
|
709
|
+
if path:
|
|
710
|
+
temp_exif_data = get_exif(path)
|
|
711
|
+
self.exif_dialog = ExifData(temp_exif_data, "Import Selected EXIF Data",
|
|
712
|
+
self.parent(), show_buttons=True)
|
|
713
|
+
result = self.exif_dialog.exec()
|
|
714
|
+
if result == QDialog.Accepted:
|
|
715
|
+
self.io_gui_handler.set_exif_data(temp_exif_data, path)
|
|
716
|
+
self.show_status_message(f"EXIF data loaded from {path}.")
|
|
717
|
+
else:
|
|
718
|
+
self.show_status_message("EXIF data loading cancelled.")
|
|
719
|
+
|
|
720
|
+
def show_exif_data(self):
|
|
721
|
+
self.exif_dialog = ExifData(self.io_gui_handler.exif_data, "EXIF Data",
|
|
722
|
+
self.parent(), show_buttons=False)
|
|
723
|
+
self.exif_dialog.exec()
|
|
724
|
+
|
|
725
|
+
def delete_exif_data(self):
|
|
726
|
+
reply = QMessageBox.question(
|
|
727
|
+
self,
|
|
728
|
+
"Confirm Delete",
|
|
729
|
+
"Warning: the current EXIF data will be erased.\n\nDo you want to continue?",
|
|
730
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
731
|
+
QMessageBox.No
|
|
732
|
+
)
|
|
733
|
+
if reply == QMessageBox.Yes:
|
|
734
|
+
self.io_gui_handler.exif_data = None
|
|
735
|
+
self.io_gui_handler.exif_path = ''
|
|
736
|
+
|
|
679
737
|
def luminosity_filter(self):
|
|
680
738
|
self.filter_manager.apply("Luminosity, Contrast")
|
|
681
739
|
|
|
@@ -763,5 +821,23 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
763
821
|
else:
|
|
764
822
|
self.brush_tool.decrease_brush_size()
|
|
765
823
|
|
|
824
|
+
def handle_brush_hardness_change(self, delta):
|
|
825
|
+
if delta > 0:
|
|
826
|
+
self.brush_tool.increase_brush_hardness()
|
|
827
|
+
else:
|
|
828
|
+
self.brush_tool.decrease_brush_hardness()
|
|
829
|
+
|
|
830
|
+
def handle_brush_opacity_change(self, delta):
|
|
831
|
+
if delta > 0:
|
|
832
|
+
self.brush_tool.increase_brush_opacity()
|
|
833
|
+
else:
|
|
834
|
+
self.brush_tool.decrease_brush_opacity()
|
|
835
|
+
|
|
836
|
+
def handle_brush_flow_change(self, delta):
|
|
837
|
+
if delta > 0:
|
|
838
|
+
self.brush_tool.increase_brush_flow()
|
|
839
|
+
else:
|
|
840
|
+
self.brush_tool.decrease_brush_flow()
|
|
841
|
+
|
|
766
842
|
def handle_set_zoom_factor(self, zoom_factor):
|
|
767
843
|
self.zoom_factor_label.setText(f"zoom: {zoom_factor:.1%}")
|
|
@@ -132,10 +132,15 @@ class ImageViewer(QWidget):
|
|
|
132
132
|
|
|
133
133
|
def connect_signals(
|
|
134
134
|
self, handle_temp_view, end_copy_brush_area,
|
|
135
|
-
handle_brush_size_change,
|
|
135
|
+
handle_brush_size_change, handle_brush_hardness_change,
|
|
136
|
+
handle_brush_opacity_change, handle_brush_flow_change,
|
|
137
|
+
handle_needs_update):
|
|
136
138
|
for st in self._strategies.values():
|
|
137
139
|
st.temp_view_requested.connect(handle_temp_view)
|
|
138
140
|
st.end_copy_brush_area_requested.connect(end_copy_brush_area)
|
|
139
141
|
st.brush_size_change_requested.connect(handle_brush_size_change)
|
|
142
|
+
st.brush_hardness_change_requested.connect(handle_brush_hardness_change)
|
|
143
|
+
st.brush_opacity_change_requested.connect(handle_brush_opacity_change)
|
|
144
|
+
st.brush_flow_change_requested.connect(handle_brush_flow_change)
|
|
140
145
|
st.needs_update_requested.connect(handle_needs_update)
|
|
141
146
|
st.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -10,7 +10,6 @@ from PySide6.QtCore import Qt, QObject, QTimer, Signal
|
|
|
10
10
|
from .. algorithms.utils import EXTENSIONS_GUI_STR, EXTENSIONS_GUI_SAVE_STR
|
|
11
11
|
from .. algorithms.exif import get_exif, write_image_with_exif_data
|
|
12
12
|
from .file_loader import FileLoader
|
|
13
|
-
from .exif_data import ExifData
|
|
14
13
|
from .io_threads import FileMultilayerSaver, FrameImporter
|
|
15
14
|
from .layer_collection import LayerCollectionHandler
|
|
16
15
|
|
|
@@ -33,7 +32,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
33
32
|
self.image_viewer = None
|
|
34
33
|
self.loading_dialog = None
|
|
35
34
|
self.loading_timer = None
|
|
36
|
-
self.exif_dialog = None
|
|
37
35
|
self.saver_thread = None
|
|
38
36
|
self.saving_dialog = None
|
|
39
37
|
self.saving_timer = None
|
|
@@ -47,6 +45,10 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
47
45
|
self.exif_data = None
|
|
48
46
|
self.exif_path = ''
|
|
49
47
|
|
|
48
|
+
def set_exif_data(self, data, path):
|
|
49
|
+
self.exif_data = data
|
|
50
|
+
self.exif_path = path
|
|
51
|
+
|
|
50
52
|
def current_file_path(self):
|
|
51
53
|
return self.current_file_path_master if self.save_master_only.isChecked() \
|
|
52
54
|
else self.current_file_path_multi
|
|
@@ -164,6 +166,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
164
166
|
self.loader_thread.finished.connect(self.on_file_loaded)
|
|
165
167
|
self.loader_thread.error.connect(self.on_file_error)
|
|
166
168
|
self.loader_thread.start()
|
|
169
|
+
self.exif_path = self.current_file_path_master
|
|
170
|
+
self.exif_data = get_exif(self.exif_path)
|
|
167
171
|
|
|
168
172
|
def import_frames(self):
|
|
169
173
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
@@ -196,6 +200,9 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
196
200
|
self.frame_importer_thread.error.connect(self.on_frames_import_error)
|
|
197
201
|
self.frame_importer_thread.progress.connect(self.update_import_progress)
|
|
198
202
|
self.frame_importer_thread.start()
|
|
203
|
+
if self.exif_data is None:
|
|
204
|
+
self.exif_path = file_paths[0]
|
|
205
|
+
self.exif_data = get_exif(self.exif_path)
|
|
199
206
|
|
|
200
207
|
def update_import_progress(self, percent, filename):
|
|
201
208
|
if hasattr(self, 'progress_bar'):
|
|
@@ -296,7 +303,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
296
303
|
img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
|
|
297
304
|
write_image_with_exif_data(self.exif_data, img, path)
|
|
298
305
|
self.current_file_path_master = os.path.abspath(path)
|
|
299
|
-
# self.mark_as_modified_requested.emit(False)
|
|
300
306
|
self.update_title_requested.emit()
|
|
301
307
|
self.add_recent_file_requested.emit(self.current_file_path_master)
|
|
302
308
|
self.status_message_requested.emit(f"Saved master layer to: {path}")
|
|
@@ -304,15 +310,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
304
310
|
traceback.print_tb(e.__traceback__)
|
|
305
311
|
QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
|
|
306
312
|
|
|
307
|
-
def select_exif_path(self):
|
|
308
|
-
path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
|
|
309
|
-
if path:
|
|
310
|
-
self.exif_path = path
|
|
311
|
-
self.exif_data = get_exif(path)
|
|
312
|
-
self.status_message_requested.emit(f"EXIF data extracted from {path}.")
|
|
313
|
-
self.exif_dialog = ExifData(self.exif_data, self.parent())
|
|
314
|
-
self.exif_dialog.exec()
|
|
315
|
-
|
|
316
313
|
def close_file(self):
|
|
317
314
|
self.mark_as_modified_requested.emit(False)
|
|
318
315
|
self.layer_collection.reset()
|
|
@@ -61,7 +61,10 @@ class ShortcutsHelp(QDialog):
|
|
|
61
61
|
"Ctrl + +": "Zoom in",
|
|
62
62
|
"Ctrl + -": "Zoom out",
|
|
63
63
|
"Ctrl + 0": "Fit to screen",
|
|
64
|
-
"Ctrl + R": "Actual size"
|
|
64
|
+
"Ctrl + R": "Actual size",
|
|
65
|
+
"Ctrl + 1": "View: overlaid",
|
|
66
|
+
"Ctrl + 2": "View: side by side",
|
|
67
|
+
"Ctrl + 3": "View: top-bottom",
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
self.add_bold_label(left_layout, "Keyboard Shortcuts")
|
|
@@ -69,13 +72,14 @@ class ShortcutsHelp(QDialog):
|
|
|
69
72
|
left_layout.addRow(f"<b>{k}</b>", QLabel(v))
|
|
70
73
|
|
|
71
74
|
shortcuts = {
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
75
|
+
"[": "Decrease brush size",
|
|
76
|
+
"]": "Increase brush size",
|
|
77
|
+
"{": "Decrease brush hardness",
|
|
78
|
+
"}": "Increase brush hardness",
|
|
79
|
+
",": "Decrease brush opacity",
|
|
80
|
+
".": "Increase brush opacity",
|
|
81
|
+
";": "Decrease brush flow",
|
|
82
|
+
":": "Increase brush flow"
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
self.add_bold_label(right_layout, "Keyboard Shortcuts")
|
|
@@ -86,6 +90,9 @@ class ShortcutsHelp(QDialog):
|
|
|
86
90
|
"Space + Drag": "Move",
|
|
87
91
|
"Wheel": "Zoom in/out",
|
|
88
92
|
"Ctrl + Wheel": "Adjust brush size",
|
|
93
|
+
"Shift + Wheel": "Adjust brush hardness",
|
|
94
|
+
"Alt + Wheel": "Adjust brush opacity",
|
|
95
|
+
"Ctrl + Shift + Wheel": "Adjust brush flow",
|
|
89
96
|
"Left Click": "Use brush to copy from selected layer to master",
|
|
90
97
|
}
|
|
91
98
|
|
|
@@ -6,7 +6,7 @@ import numpy as np
|
|
|
6
6
|
from PySide6.QtCore import Qt, QPointF, QTime, QPoint, Signal, QRectF
|
|
7
7
|
from PySide6.QtGui import QImage, QPainter, QColor, QBrush, QPen, QCursor, QPixmap, QPainterPath
|
|
8
8
|
from PySide6.QtWidgets import (
|
|
9
|
-
QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
|
|
9
|
+
QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QApplication,
|
|
10
10
|
QGraphicsItemGroup, QGraphicsPathItem)
|
|
11
11
|
from .. config.gui_constants import gui_constants
|
|
12
12
|
from .. config.app_config import AppConfig
|
|
@@ -81,6 +81,9 @@ class ViewSignals:
|
|
|
81
81
|
temp_view_requested = Signal(bool)
|
|
82
82
|
end_copy_brush_area_requested = Signal()
|
|
83
83
|
brush_size_change_requested = Signal(int) # +1 or -1
|
|
84
|
+
brush_hardness_change_requested = Signal(int)
|
|
85
|
+
brush_opacity_change_requested = Signal(int)
|
|
86
|
+
brush_flow_change_requested = Signal(int)
|
|
84
87
|
needs_update_requested = Signal()
|
|
85
88
|
|
|
86
89
|
|
|
@@ -444,8 +447,15 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
444
447
|
if self.empty() or self.gesture_active:
|
|
445
448
|
return
|
|
446
449
|
if event.source() == Qt.MouseEventNotSynthesized: # Physical mouse
|
|
447
|
-
|
|
450
|
+
modifiers = QApplication.keyboardModifiers()
|
|
451
|
+
if modifiers & Qt.ControlModifier and modifiers & Qt.ShiftModifier:
|
|
452
|
+
self.brush_flow_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
|
|
453
|
+
elif modifiers & Qt.ControlModifier:
|
|
448
454
|
self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
|
|
455
|
+
elif modifiers & Qt.ShiftModifier:
|
|
456
|
+
self.brush_hardness_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
|
|
457
|
+
elif modifiers & Qt.AltModifier:
|
|
458
|
+
self.brush_opacity_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
|
|
449
459
|
else:
|
|
450
460
|
self.handle_zoom_wheel(self.get_view_with_mouse(event), event)
|
|
451
461
|
self.update_brush_cursor()
|