shinestacker 0.3.0__py3-none-any.whl → 0.3.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.

Files changed (43) hide show
  1. shinestacker/__init__.py +6 -6
  2. shinestacker/_version.py +1 -1
  3. shinestacker/algorithms/balance.py +6 -7
  4. shinestacker/algorithms/noise_detection.py +2 -0
  5. shinestacker/algorithms/utils.py +4 -0
  6. shinestacker/algorithms/white_balance.py +13 -0
  7. shinestacker/app/open_frames.py +6 -4
  8. shinestacker/config/__init__.py +2 -1
  9. shinestacker/config/config.py +1 -0
  10. shinestacker/config/constants.py +1 -0
  11. shinestacker/config/gui_constants.py +1 -0
  12. shinestacker/core/__init__.py +4 -3
  13. shinestacker/core/colors.py +1 -0
  14. shinestacker/core/core_utils.py +6 -6
  15. shinestacker/core/exceptions.py +1 -0
  16. shinestacker/core/framework.py +2 -1
  17. shinestacker/gui/action_config.py +47 -42
  18. shinestacker/gui/actions_window.py +8 -5
  19. shinestacker/gui/new_project.py +1 -0
  20. shinestacker/retouch/brush_gradient.py +20 -0
  21. shinestacker/retouch/brush_preview.py +10 -14
  22. shinestacker/retouch/brush_tool.py +164 -0
  23. shinestacker/retouch/denoise_filter.py +56 -0
  24. shinestacker/retouch/display_manager.py +177 -0
  25. shinestacker/retouch/exif_data.py +2 -1
  26. shinestacker/retouch/filter_base.py +114 -0
  27. shinestacker/retouch/filter_manager.py +14 -0
  28. shinestacker/retouch/image_editor.py +108 -543
  29. shinestacker/retouch/image_editor_ui.py +42 -75
  30. shinestacker/retouch/image_filters.py +27 -423
  31. shinestacker/retouch/image_viewer.py +31 -31
  32. shinestacker/retouch/io_gui_handler.py +208 -0
  33. shinestacker/retouch/io_manager.py +53 -0
  34. shinestacker/retouch/layer_collection.py +118 -0
  35. shinestacker/retouch/unsharp_mask_filter.py +84 -0
  36. shinestacker/retouch/white_balance_filter.py +111 -0
  37. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/METADATA +3 -2
  38. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/RECORD +42 -31
  39. shinestacker/retouch/brush_controller.py +0 -57
  40. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/WHEEL +0 -0
  41. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/entry_points.txt +0 -0
  42. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/licenses/LICENSE +0 -0
  43. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/top_level.txt +0 -0
@@ -1,463 +1,67 @@
1
1
  import numpy as np
2
- from PySide6.QtWidgets import (QHBoxLayout,
3
- QPushButton, QFrame, QVBoxLayout, QLabel, QDialog, QApplication, QSlider,
4
- QCheckBox, QDialogButtonBox)
5
- from PySide6.QtGui import QCursor
6
- from PySide6.QtCore import Qt, QTimer, QThread, Signal
7
- from .. algorithms.denoise import denoise
8
- from .. algorithms.sharpen import unsharp_mask
9
- from .. algorithms.white_balance import white_balance_from_rgb
10
2
  from .image_editor import ImageEditor
3
+ from .filter_manager import FilterManager
4
+ from .denoise_filter import DenoiseFilter
5
+ from .unsharp_mask_filter import UnsharpMaskFilter
6
+ from .white_balance_filter import WhiteBalanceFilter
11
7
 
12
8
 
13
9
  class ImageFilters(ImageEditor):
14
10
  def __init__(self):
15
11
  super().__init__()
12
+ self.filter_manager = FilterManager(self)
13
+ self.filter_manager.register_filter("denoise", DenoiseFilter)
14
+ self.filter_manager.register_filter("unsharp_mask", UnsharpMaskFilter)
15
+ self.filter_manager.register_filter("white_balance", WhiteBalanceFilter)
16
16
 
17
17
  def denoise(self):
18
- max_range = 500.0
19
- max_value = 5.00
20
- initial_value = 2.5
21
- self.master_layer_copy = self.master_layer.copy()
22
- dlg = QDialog(self)
23
- dlg.setWindowTitle("Denoise")
24
- dlg.setMinimumWidth(600)
25
- layout = QVBoxLayout(dlg)
26
- slider_layout = QHBoxLayout()
27
- slider = QSlider(Qt.Horizontal)
28
- slider.setRange(0, max_range)
29
- slider.setValue(initial_value / max_value * max_range)
30
- slider_layout.addWidget(slider)
31
- value_label = QLabel(f"{max_value:.2f}")
32
- slider_layout.addWidget(value_label)
33
- layout.addLayout(slider_layout)
34
- preview_check = QCheckBox("Preview")
35
- preview_check.setChecked(True)
36
- layout.addWidget(preview_check)
37
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
38
- layout.addWidget(button_box)
39
- last_preview_strength = None
40
- preview_timer = QTimer()
41
- preview_timer.setSingleShot(True)
42
- preview_timer.setInterval(200)
43
- active_worker = None
44
- last_request_id = 0
45
-
46
- class PreviewWorker(QThread):
47
- finished = Signal(np.ndarray, int)
48
-
49
- def __init__(self, image, strength, request_id):
50
- super().__init__()
51
- self.image = image
52
- self.strength = strength
53
- self.request_id = request_id
54
-
55
- def run(self):
56
- result = denoise(self.image, self.strength)
57
- self.finished.emit(result, self.request_id)
58
-
59
- def slider_changed(value):
60
- float_value = max_value * value / max_range
61
- value_label.setText(f"{float_value:.2f}")
62
- if preview_check.isChecked():
63
- nonlocal last_preview_strength
64
- last_preview_strength = float_value
65
- preview_timer.start()
66
-
67
- def do_preview():
68
- nonlocal active_worker, last_request_id
69
- if last_preview_strength is None:
70
- return
71
- if active_worker and active_worker.isRunning():
72
- active_worker.quit()
73
- active_worker.wait()
74
- last_request_id += 1
75
- current_request_id = last_request_id
76
- active_worker = PreviewWorker(
77
- self.master_layer_copy.copy(),
78
- last_preview_strength,
79
- current_request_id
80
- )
81
- active_worker.finished.connect(
82
- lambda img, rid: set_preview(img, rid, current_request_id)
83
- )
84
- active_worker.start()
85
-
86
- def set_preview(img, request_id, expected_id):
87
- if request_id != expected_id:
88
- return
89
- self.master_layer = img
90
- self.display_master_layer()
91
- dlg.activateWindow()
92
- slider.setFocus()
93
-
94
- def on_preview_toggled(checked):
95
- nonlocal last_preview_strength
96
- if checked:
97
- last_preview_strength = max_value * slider.value() / max_range
98
- do_preview()
99
- else:
100
- self.master_layer = self.master_layer_copy.copy()
101
- self.display_master_layer()
102
- dlg.activateWindow()
103
- slider.setFocus()
104
- button_box.setFocus()
105
-
106
- slider.valueChanged.connect(slider_changed)
107
- preview_timer.timeout.connect(do_preview)
108
- preview_check.stateChanged.connect(on_preview_toggled)
109
- button_box.accepted.connect(dlg.accept)
110
- button_box.rejected.connect(dlg.reject)
111
-
112
- def run_initial_preview():
113
- slider_changed(slider.value())
114
-
115
- QTimer.singleShot(0, run_initial_preview)
116
- slider.setFocus()
117
- if dlg.exec_() == QDialog.Accepted:
118
- strength = max_value * float(slider.value()) / max_range
119
- h, w = self.master_layer.shape[:2]
120
- self.undo_manager.extend_undo_area(0, 0, w, h)
121
- self.undo_manager.save_undo_state(self.master_layer_copy, 'Denoise')
122
- self.master_layer = denoise(self.master_layer_copy, strength)
123
- self.master_layer_copy = self.master_layer.copy()
124
- self.display_master_layer()
125
- self.update_master_thumbnail()
126
- self.mark_as_modified()
127
- else:
128
- self.master_layer = self.master_layer_copy.copy()
129
- self.display_master_layer()
18
+ self.filter_manager.apply("denoise")
130
19
 
131
20
  def unsharp_mask(self):
132
- max_range = 500.0
133
- max_radius = 4.0
134
- max_amount = 3.0
135
- max_threshold = 100.0
136
- initial_radius = 1.0
137
- initial_amount = 0.5
138
- initial_threshold = 0.0
139
- self.master_layer_copy = self.master_layer.copy()
140
- dlg = QDialog(self)
141
- dlg.setWindowTitle("Unsharp Mask")
142
- dlg.setMinimumWidth(600)
143
- layout = QVBoxLayout(dlg)
144
- params = {
145
- "Radius": (max_radius, initial_radius, "{:.2f}"),
146
- "Amount": (max_amount, initial_amount, "{:.2%}"),
147
- "Threshold": (max_threshold, initial_threshold, "{:.2f}")
148
- }
149
- sliders = {}
150
- value_labels = {}
151
- for name, (max_val, init_val, fmt) in params.items():
152
- param_layout = QHBoxLayout()
153
- name_label = QLabel(f"{name}:")
154
- param_layout.addWidget(name_label)
155
- slider = QSlider(Qt.Horizontal)
156
- slider.setRange(0, max_range)
157
- slider.setValue(init_val / max_val * max_range)
158
- param_layout.addWidget(slider)
159
- value_label = QLabel(fmt.format(init_val))
160
- param_layout.addWidget(value_label)
161
- layout.addLayout(param_layout)
162
- sliders[name] = slider
163
- value_labels[name] = value_label
164
- preview_check = QCheckBox("Preview")
165
- preview_check.setChecked(True)
166
- layout.addWidget(preview_check)
167
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
168
- layout.addWidget(button_box)
169
- last_preview_params = None
170
- preview_timer = QTimer()
171
- preview_timer.setSingleShot(True)
172
- preview_timer.setInterval(200)
173
- active_worker = None
174
- last_request_id = 0
175
-
176
- class UnsharpWorker(QThread):
177
- finished = Signal(np.ndarray, int)
178
-
179
- def __init__(self, image, radius, amount, threshold, request_id):
180
- super().__init__()
181
- self.image = image
182
- self.radius = radius
183
- self.amount = amount
184
- self.threshold = threshold
185
- self.request_id = request_id
21
+ self.filter_manager.apply("unsharp_mask")
186
22
 
187
- def run(self):
188
- result = unsharp_mask(self.image, max(0.01, self.radius), self.amount, self.threshold)
189
- self.finished.emit(result, self.request_id)
23
+ def white_balance(self, init_val=None):
24
+ self.filter_manager.apply("white_balance", init_val=init_val or (128, 128, 128))
190
25
 
191
- def update_param_value(name, value, max_val, fmt):
192
- float_value = max_val * value / max_range
193
- value_labels[name].setText(fmt.format(float_value))
194
- if preview_check.isChecked():
195
- nonlocal last_preview_params
196
- last_preview_params = (
197
- max_radius * sliders["Radius"].value() / max_range,
198
- max_amount * sliders["Amount"].value() / max_range,
199
- max_threshold * sliders["Threshold"].value() / max_range
200
- )
201
- preview_timer.start()
202
- sliders["Radius"].valueChanged.connect(
203
- lambda v: update_param_value("Radius", v, params["Radius"][0], params["Radius"][2]))
204
- sliders["Amount"].valueChanged.connect(
205
- lambda v: update_param_value("Amount", v, params["Amount"][0], params["Amount"][2]))
206
- sliders["Threshold"].valueChanged.connect(
207
- lambda v: update_param_value("Threshold", v, params["Threshold"][0], params["Threshold"][2]))
208
-
209
- def do_preview():
210
- nonlocal active_worker, last_request_id
211
- if last_preview_params is None:
212
- return
213
- if active_worker and active_worker.isRunning():
214
- active_worker.quit()
215
- active_worker.wait()
216
- last_request_id += 1
217
- current_request_id = last_request_id
218
- radius, amount, threshold = last_preview_params
219
- active_worker = UnsharpWorker(
220
- self.master_layer_copy.copy(),
221
- radius,
222
- amount,
223
- threshold,
224
- current_request_id
225
- )
226
- active_worker.finished.connect(lambda img, rid: set_preview(img, rid, current_request_id))
227
- active_worker.start()
228
-
229
- def set_preview(img, request_id, expected_id):
230
- if request_id != expected_id:
231
- return
232
- self.master_layer = img
233
- self.display_master_layer()
234
- dlg.activateWindow()
235
- sliders["Radius"].setFocus()
236
-
237
- def on_preview_toggled(checked):
238
- nonlocal last_preview_params
26
+ def connect_preview_toggle(self, preview_check, do_preview, restore_original):
27
+ def on_toggled(checked):
239
28
  if checked:
240
- last_preview_params = (
241
- max_radius * sliders["Radius"].value() / max_range,
242
- max_amount * sliders["Amount"].value() / max_range,
243
- max_threshold * sliders["Threshold"].value() / max_range
244
- )
245
29
  do_preview()
246
30
  else:
247
- self.master_layer = self.master_layer_copy.copy()
248
- self.display_master_layer()
249
- dlg.activateWindow()
250
- sliders["Radius"].setFocus()
251
-
252
- preview_timer.timeout.connect(do_preview)
253
- preview_check.stateChanged.connect(on_preview_toggled)
254
- button_box.accepted.connect(dlg.accept)
255
- button_box.rejected.connect(dlg.reject)
256
-
257
- def run_initial_preview():
258
- nonlocal last_preview_params
259
- last_preview_params = (
260
- initial_radius,
261
- initial_amount,
262
- initial_threshold
263
- )
264
- do_preview()
265
-
266
- QTimer.singleShot(0, run_initial_preview)
267
- sliders["Radius"].setFocus()
268
- if dlg.exec_() == QDialog.Accepted:
269
- radius = max_radius * sliders["Radius"].value() / max_range
270
- amount = max_amount * sliders["Amount"].value() / max_range
271
- threshold = max_threshold * sliders["Threshold"].value() / max_range
272
- h, w = self.master_layer.shape[:2]
273
- self.undo_manager.extend_undo_area(0, 0, w, h)
274
- self.undo_manager.save_undo_state(self.master_layer_copy, 'Unsharp Mask')
275
- self.master_layer = unsharp_mask(self.master_layer_copy, max(0.01, radius), amount, threshold)
276
- self.master_layer_copy = self.master_layer.copy()
277
- self.display_master_layer()
278
- self.update_master_thumbnail()
279
- self.mark_as_modified()
280
- else:
281
- self.master_layer = self.master_layer_copy.copy()
282
- self.display_master_layer()
283
-
284
- def white_balance(self):
285
- if hasattr(self, 'wb_dialog') and self.wb_dialog:
286
- self.wb_dialog.activateWindow()
287
- self.wb_dialog.raise_()
288
- return
289
- max_range = 255
290
- initial_val = 128
291
- initial_rgb = (initial_val, initial_val, initial_val)
292
- cursor_style = self.image_viewer.cursor_style
293
- self.image_viewer.set_cursor_style('outline')
294
- if self.image_viewer.brush_cursor:
295
- self.image_viewer.brush_cursor.hide()
296
- self.master_layer_copy = self.master_layer.copy()
297
- self.brush_preview.hide()
298
- self.wb_dialog = dlg = QDialog(self)
299
- dlg.setWindowTitle("White Balance")
300
- dlg.setMinimumWidth(600)
301
- layout = QVBoxLayout(dlg)
302
- row_layout = QHBoxLayout()
303
- color_preview = QFrame()
304
- color_preview.setFixedHeight(80)
305
- color_preview.setFixedWidth(80)
306
- color_preview.setStyleSheet("background-color: rgb(128,128,128);")
307
- row_layout.addWidget(color_preview)
308
- sliders_layout = QVBoxLayout()
309
- sliders = {}
310
- value_labels = {}
311
- rgb_layouts = {}
312
- for name, init_val in zip(("R", "G", "B"), initial_rgb):
313
- row = QHBoxLayout()
314
- label = QLabel(f"{name}:")
315
- row.addWidget(label)
316
- slider = QSlider(Qt.Horizontal)
317
- slider.setRange(0, max_range)
318
- slider.setValue(init_val)
319
- row.addWidget(slider)
320
- val_label = QLabel(str(init_val))
321
- row.addWidget(val_label)
322
- sliders_layout.addLayout(row)
323
- sliders[name] = slider
324
- value_labels[name] = val_label
325
- rgb_layouts[name] = row
326
- row_layout.addLayout(sliders_layout)
327
- layout.addLayout(row_layout)
328
- pick_button = QPushButton("Pick Color")
329
- layout.addWidget(pick_button)
330
-
331
- def update_preview_color():
332
- rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
333
- color_preview.setStyleSheet(f"background-color: rgb{rgb};")
334
-
335
- def schedule_preview():
336
- nonlocal last_preview_rgb
337
- rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
338
- for n in ("R", "G", "B"):
339
- value_labels[n].setText(str(sliders[n].value()))
340
- update_preview_color()
341
- if preview_check.isChecked() and rgb != last_preview_rgb:
342
- last_preview_rgb = rgb
343
- preview_timer.start(100)
344
-
345
- def apply_preview():
346
- rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
347
- processed = white_balance_from_rgb(self.master_layer_copy, rgb)
348
- self.master_layer = processed
349
- self.display_master_layer()
350
- dlg.activateWindow()
351
-
352
- def on_preview_toggled(checked):
353
- nonlocal last_preview_rgb
354
- if checked:
355
- last_preview_rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
356
- preview_timer.start(100)
357
- else:
358
- self.master_layer = self.master_layer_copy.copy()
359
- self.display_master_layer()
360
- dlg.activateWindow()
361
-
362
- preview_check = QCheckBox("Preview")
363
- preview_check.setChecked(True)
364
- preview_check.stateChanged.connect(on_preview_toggled)
365
- layout.addWidget(preview_check)
366
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel)
367
- layout.addWidget(button_box)
368
- last_preview_rgb = None
369
- preview_timer = QTimer()
370
- preview_timer.setSingleShot(True)
371
- preview_timer.timeout.connect(apply_preview)
372
- for slider in sliders.values():
373
- slider.valueChanged.connect(schedule_preview)
374
-
375
- def start_color_pick():
376
- dlg.hide()
377
- QApplication.setOverrideCursor(QCursor(Qt.CrossCursor))
378
- self.image_viewer.setCursor(Qt.CrossCursor)
379
- self.master_layer = self.master_layer_copy
380
- self.display_master_layer()
381
- self._original_mouse_press = self.image_viewer.mousePressEvent
382
- self.image_viewer.mousePressEvent = pick_color_from_click
383
-
384
- def pick_color_from_click(event):
385
- if event.button() == Qt.LeftButton:
386
- pos = event.pos()
387
- bgr = self.get_pixel_color_at(pos)
388
- rgb = (bgr[2], bgr[1], bgr[0])
389
- for name, val in zip(("R", "G", "B"), rgb):
390
- sliders[name].setValue(val)
391
- QApplication.restoreOverrideCursor()
392
- self.image_viewer.unsetCursor()
393
- if hasattr(self, "_original_mouse_press"):
394
- self.image_viewer.mousePressEvent = self._original_mouse_press
395
- dlg.show()
396
- dlg.activateWindow()
397
- dlg.raise_()
398
-
399
- pick_button.clicked.connect(start_color_pick)
400
- button_box.accepted.connect(dlg.accept)
401
-
402
- def cancel_changes():
403
- self.master_layer = self.master_layer_copy
404
- self.display_master_layer()
405
- dlg.reject()
406
-
407
- def reset_rgb():
408
- for k, s in sliders.items():
409
- s.setValue(initial_val)
410
-
411
- button_box.rejected.connect(cancel_changes)
412
- button_box.button(QDialogButtonBox.Reset).clicked.connect(reset_rgb)
413
-
414
- def finish_white_balance(result):
415
- if result == QDialog.Accepted:
416
- apply_preview()
417
- h, w = self.master_layer.shape[:2]
418
- self.undo_manager.extend_undo_area(0, 0, w, h)
419
- self.undo_manager.save_undo_state(self.master_layer_copy, 'White Balance')
420
- self.master_layer_copy = self.master_layer.copy()
421
- self.display_master_layer()
422
- self.update_master_thumbnail()
423
- self.mark_as_modified()
424
- self.image_viewer.set_cursor_style(cursor_style)
425
- self.wb_dialog = None
426
-
427
- dlg.finished.connect(finish_white_balance)
428
- dlg.show()
31
+ restore_original()
32
+ preview_check.toggled.connect(on_toggled)
429
33
 
430
34
  def get_pixel_color_at(self, pos, radius=None):
431
- scene_pos = self.image_viewer.mapToScene(pos)
432
- item_pos = self.image_viewer.pixmap_item.mapFromScene(scene_pos)
35
+ item_pos = self.image_viewer.position_on_image(pos)
433
36
  x = int(item_pos.x())
434
37
  y = int(item_pos.y())
435
- if (0 <= x < self.master_layer.shape[1]) and (0 <= y < self.master_layer.shape[0]):
38
+ master_layer = self.master_layer()
39
+ if (0 <= x < self.master_layer().shape[1]) and \
40
+ (0 <= y < self.master_layer().shape[0]):
436
41
  if radius is None:
437
42
  radius = int(self.brush.size)
438
43
  if radius > 0:
439
44
  y_indices, x_indices = np.ogrid[-radius:radius + 1, -radius:radius + 1]
440
45
  mask = x_indices**2 + y_indices**2 <= radius**2
441
46
  x0 = max(0, x - radius)
442
- x1 = min(self.master_layer.shape[1], x + radius + 1)
47
+ x1 = min(master_layer.shape[1], x + radius + 1)
443
48
  y0 = max(0, y - radius)
444
- y1 = min(self.master_layer.shape[0], y + radius + 1)
49
+ y1 = min(master_layer.shape[0], y + radius + 1)
445
50
  mask = mask[radius - (y - y0): radius + (y1 - y), radius - (x - x0): radius + (x1 - x)]
446
- region = self.master_layer[y0:y1, x0:x1]
51
+ region = master_layer[y0:y1, x0:x1]
447
52
  if region.size == 0:
448
- pixel = self.master_layer[y, x]
53
+ pixel = master_layer[y, x]
449
54
  else:
450
55
  if region.ndim == 3:
451
56
  pixel = [region[:, :, c][mask].mean() for c in range(region.shape[2])]
452
57
  else:
453
58
  pixel = region[mask].mean()
454
59
  else:
455
- pixel = self.master_layer[y, x]
60
+ pixel = self.master_layer()[y, x]
456
61
  if np.isscalar(pixel):
457
62
  pixel = [pixel, pixel, pixel]
458
63
  pixel = [np.float32(x) for x in pixel]
459
- if self.master_layer.dtype == np.uint16:
64
+ if master_layer.dtype == np.uint16:
460
65
  pixel = [x / 256.0 for x in pixel]
461
66
  return tuple(int(v) for v in pixel)
462
- else:
463
- return (0, 0, 0)
67
+ return (0, 0, 0)
@@ -1,34 +1,23 @@
1
1
  import math
2
2
  from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
3
- from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence, QRadialGradient
4
- from PySide6.QtCore import Qt, QRectF, QTime, QPoint, Signal
3
+ from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence
4
+ from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal
5
5
  from .. config.gui_constants import gui_constants
6
6
  from .brush_preview import BrushPreviewItem
7
-
8
-
9
- def create_brush_gradient(center_x, center_y, radius, hardness, inner_color=None, outer_color=None, opacity=100):
10
- gradient = QRadialGradient(center_x, center_y, float(radius))
11
- inner = inner_color if inner_color is not None else QColor(*gui_constants.BRUSH_COLORS['inner'])
12
- outer = outer_color if outer_color is not None else QColor(*gui_constants.BRUSH_COLORS['gradient_end'])
13
- inner_with_opacity = QColor(inner)
14
- inner_with_opacity.setAlpha(int(float(inner.alpha()) * float(opacity) / 100.0))
15
- if hardness < 100:
16
- hardness_normalized = float(hardness) / 100.0
17
- gradient.setColorAt(0.0, inner_with_opacity)
18
- gradient.setColorAt(hardness_normalized, inner_with_opacity)
19
- gradient.setColorAt(1.0, outer)
20
- else:
21
- gradient.setColorAt(0.0, inner_with_opacity)
22
- gradient.setColorAt(1.0, inner_with_opacity)
23
- return gradient
7
+ from .brush_gradient import create_brush_gradient
24
8
 
25
9
 
26
10
  class ImageViewer(QGraphicsView):
27
11
  temp_view_requested = Signal(bool)
12
+ brush_operation_started = Signal(QPoint)
13
+ brush_operation_continued = Signal(QPoint)
14
+ brush_operation_ended = Signal()
15
+ brush_size_change_requested = Signal(int) # +1 or -1
28
16
 
29
- def __init__(self, parent=None):
17
+ def __init__(self, layer_collection, parent=None):
30
18
  super().__init__(parent)
31
- self.image_editor = None
19
+ self.display_manager = None
20
+ self.layer_collection = layer_collection
32
21
  self.brush = None
33
22
  self.cursor_style = gui_constants.DEFAULT_CURSOR_STYLE
34
23
  self.scene = QGraphicsScene(self)
@@ -55,8 +44,10 @@ class ImageViewer(QGraphicsView):
55
44
  self.dragging = False
56
45
  self.last_update_time = QTime.currentTime()
57
46
  self.brush_preview = BrushPreviewItem()
47
+ self.layer_collection.add_to(self.brush_preview)
58
48
  self.scene.addItem(self.brush_preview)
59
49
  self.empty = True
50
+ self.allow_cursor_preview = True
60
51
 
61
52
  def set_image(self, qimage):
62
53
  pixmap = QPixmap.fromImage(qimage)
@@ -74,6 +65,7 @@ class ImageViewer(QGraphicsView):
74
65
  self.empty = False
75
66
  self.setFocus()
76
67
  self.activateWindow()
68
+ self.brush_preview.brush = self.brush
77
69
 
78
70
  def clear_image(self):
79
71
  self.scene.clear()
@@ -123,7 +115,7 @@ class ImageViewer(QGraphicsView):
123
115
  def mousePressEvent(self, event):
124
116
  if self.empty:
125
117
  return
126
- if event.button() == Qt.LeftButton and self.image_editor.master_layer is not None:
118
+ if event.button() == Qt.LeftButton and self.layer_collection.has_master_layer():
127
119
  if self.space_pressed:
128
120
  self.scrolling = True
129
121
  self.last_mouse_pos = event.position()
@@ -132,7 +124,7 @@ class ImageViewer(QGraphicsView):
132
124
  self.brush_cursor.hide()
133
125
  else:
134
126
  self.last_brush_pos = event.position()
135
- self.image_editor.begin_copy_brush_area(event.position().toPoint())
127
+ self.brush_operation_started.emit(event.position().toPoint())
136
128
  self.dragging = True
137
129
  if self.brush_cursor:
138
130
  self.brush_cursor.show()
@@ -159,7 +151,7 @@ class ImageViewer(QGraphicsView):
159
151
  for i in range(0, n_steps + 1):
160
152
  pos = QPoint(self.last_brush_pos.x() + i * delta_x,
161
153
  self.last_brush_pos.y() + i * delta_y)
162
- self.image_editor.continue_copy_brush_area(pos)
154
+ self.brush_operation_continued.emit(pos)
163
155
  self.last_brush_pos = position
164
156
  self.last_update_time = current_time
165
157
  if self.scrolling and event.buttons() & Qt.LeftButton:
@@ -191,17 +183,14 @@ class ImageViewer(QGraphicsView):
191
183
  self.last_mouse_pos = None
192
184
  elif hasattr(self, 'dragging') and self.dragging:
193
185
  self.dragging = False
194
- self.image_editor.end_copy_brush_area()
186
+ self.brush_operation_ended.emit()
195
187
  super().mouseReleaseEvent(event)
196
188
 
197
189
  def wheelEvent(self, event):
198
190
  if self.empty:
199
191
  return
200
192
  if self.control_pressed:
201
- if event.angleDelta().y() > 0:
202
- self.image_editor.decrease_brush_size()
203
- else:
204
- self.image_editor.increase_brush_size()
193
+ self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
205
194
  else:
206
195
  zoom_in_factor = 1.10
207
196
  zoom_out_factor = 1 / zoom_in_factor
@@ -241,11 +230,17 @@ class ImageViewer(QGraphicsView):
241
230
  center_y = scene_pos.y()
242
231
  radius = size / 2
243
232
  self.brush_cursor.setRect(center_x - radius, center_y - radius, size, size)
244
- allow_cursor_preview = self.image_editor.allow_cursor_preview()
233
+ allow_cursor_preview = self.display_manager.allow_cursor_preview()
245
234
  if self.cursor_style == 'preview' and allow_cursor_preview:
246
235
  self._setup_outline_style()
247
236
  self.brush_cursor.hide()
248
- self.brush_preview.update(self.image_editor, QCursor.pos(), int(size))
237
+ pos = QCursor.pos()
238
+ if isinstance(pos, QPointF):
239
+ scene_pos = pos
240
+ else:
241
+ cursor_pos = self.mapFromGlobal(pos)
242
+ scene_pos = self.mapToScene(cursor_pos)
243
+ self.brush_preview.update(scene_pos, int(size))
249
244
  else:
250
245
  self.brush_preview.hide()
251
246
  if self.cursor_style == 'outline' or not allow_cursor_preview:
@@ -354,3 +349,8 @@ class ImageViewer(QGraphicsView):
354
349
  self.cursor_style = style
355
350
  if self.brush_cursor:
356
351
  self.update_brush_cursor()
352
+
353
+ def position_on_image(self, pos):
354
+ scene_pos = self.mapToScene(pos)
355
+ item_pos = self.pixmap_item.mapFromScene(scene_pos)
356
+ return item_pos