setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__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 setiastrosuitepro might be problematic. Click here for more details.

Files changed (132) hide show
  1. setiastro/images/TextureClarity.svg +56 -0
  2. setiastro/images/abeicon.svg +16 -0
  3. setiastro/images/acv_icon.png +0 -0
  4. setiastro/images/colorwheel.svg +97 -0
  5. setiastro/images/cosmic.svg +40 -0
  6. setiastro/images/cosmicsat.svg +24 -0
  7. setiastro/images/first_quarter.png +0 -0
  8. setiastro/images/full_moon.png +0 -0
  9. setiastro/images/graxpert.svg +19 -0
  10. setiastro/images/last_quarter.png +0 -0
  11. setiastro/images/linearfit.svg +32 -0
  12. setiastro/images/narrowbandnormalization.png +0 -0
  13. setiastro/images/new_moon.png +0 -0
  14. setiastro/images/pixelmath.svg +42 -0
  15. setiastro/images/planetarystacker.png +0 -0
  16. setiastro/images/waning_crescent_1.png +0 -0
  17. setiastro/images/waning_crescent_2.png +0 -0
  18. setiastro/images/waning_crescent_3.png +0 -0
  19. setiastro/images/waning_crescent_4.png +0 -0
  20. setiastro/images/waning_crescent_5.png +0 -0
  21. setiastro/images/waning_gibbous_1.png +0 -0
  22. setiastro/images/waning_gibbous_2.png +0 -0
  23. setiastro/images/waning_gibbous_3.png +0 -0
  24. setiastro/images/waning_gibbous_4.png +0 -0
  25. setiastro/images/waning_gibbous_5.png +0 -0
  26. setiastro/images/waxing_crescent_1.png +0 -0
  27. setiastro/images/waxing_crescent_2.png +0 -0
  28. setiastro/images/waxing_crescent_3.png +0 -0
  29. setiastro/images/waxing_crescent_4.png +0 -0
  30. setiastro/images/waxing_crescent_5.png +0 -0
  31. setiastro/images/waxing_gibbous_1.png +0 -0
  32. setiastro/images/waxing_gibbous_2.png +0 -0
  33. setiastro/images/waxing_gibbous_3.png +0 -0
  34. setiastro/images/waxing_gibbous_4.png +0 -0
  35. setiastro/images/waxing_gibbous_5.png +0 -0
  36. setiastro/qml/ResourceMonitor.qml +84 -82
  37. setiastro/saspro/__main__.py +20 -1
  38. setiastro/saspro/_generated/build_info.py +2 -2
  39. setiastro/saspro/abe.py +37 -4
  40. setiastro/saspro/aberration_ai.py +364 -33
  41. setiastro/saspro/aberration_ai_preset.py +29 -3
  42. setiastro/saspro/acv_exporter.py +379 -0
  43. setiastro/saspro/add_stars.py +33 -6
  44. setiastro/saspro/astrospike_python.py +45 -3
  45. setiastro/saspro/backgroundneutral.py +108 -40
  46. setiastro/saspro/blemish_blaster.py +4 -1
  47. setiastro/saspro/blink_comparator_pro.py +150 -55
  48. setiastro/saspro/clahe.py +4 -1
  49. setiastro/saspro/continuum_subtract.py +4 -1
  50. setiastro/saspro/convo.py +13 -7
  51. setiastro/saspro/cosmicclarity.py +129 -18
  52. setiastro/saspro/crop_dialog_pro.py +123 -7
  53. setiastro/saspro/curve_editor_pro.py +181 -64
  54. setiastro/saspro/curves_preset.py +249 -47
  55. setiastro/saspro/doc_manager.py +245 -15
  56. setiastro/saspro/exoplanet_detector.py +120 -28
  57. setiastro/saspro/frequency_separation.py +1158 -204
  58. setiastro/saspro/ghs_dialog_pro.py +81 -16
  59. setiastro/saspro/graxpert.py +1 -0
  60. setiastro/saspro/gui/main_window.py +706 -264
  61. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  62. setiastro/saspro/gui/mixins/file_mixin.py +35 -16
  63. setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
  64. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  65. setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
  66. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  67. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  68. setiastro/saspro/halobgon.py +4 -0
  69. setiastro/saspro/histogram.py +184 -8
  70. setiastro/saspro/image_combine.py +4 -0
  71. setiastro/saspro/image_peeker_pro.py +4 -0
  72. setiastro/saspro/imageops/narrowband_normalization.py +816 -0
  73. setiastro/saspro/imageops/serloader.py +1345 -0
  74. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  75. setiastro/saspro/imageops/stretch.py +582 -62
  76. setiastro/saspro/isophote.py +4 -0
  77. setiastro/saspro/layers.py +13 -9
  78. setiastro/saspro/layers_dock.py +183 -3
  79. setiastro/saspro/legacy/image_manager.py +154 -20
  80. setiastro/saspro/legacy/numba_utils.py +68 -48
  81. setiastro/saspro/legacy/xisf.py +240 -98
  82. setiastro/saspro/live_stacking.py +203 -82
  83. setiastro/saspro/luminancerecombine.py +228 -27
  84. setiastro/saspro/mask_creation.py +174 -15
  85. setiastro/saspro/mfdeconv.py +113 -35
  86. setiastro/saspro/mfdeconvcudnn.py +119 -70
  87. setiastro/saspro/mfdeconvsport.py +112 -35
  88. setiastro/saspro/morphology.py +4 -0
  89. setiastro/saspro/multiscale_decomp.py +81 -29
  90. setiastro/saspro/narrowband_normalization.py +1618 -0
  91. setiastro/saspro/numba_utils.py +72 -57
  92. setiastro/saspro/ops/commands.py +18 -18
  93. setiastro/saspro/ops/script_editor.py +10 -2
  94. setiastro/saspro/ops/scripts.py +122 -0
  95. setiastro/saspro/perfect_palette_picker.py +37 -3
  96. setiastro/saspro/plate_solver.py +84 -49
  97. setiastro/saspro/psf_viewer.py +119 -37
  98. setiastro/saspro/remove_green.py +1 -1
  99. setiastro/saspro/resources.py +73 -0
  100. setiastro/saspro/rgbalign.py +460 -12
  101. setiastro/saspro/selective_color.py +4 -1
  102. setiastro/saspro/ser_stack_config.py +82 -0
  103. setiastro/saspro/ser_stacker.py +2321 -0
  104. setiastro/saspro/ser_stacker_dialog.py +1838 -0
  105. setiastro/saspro/ser_tracking.py +206 -0
  106. setiastro/saspro/serviewer.py +1625 -0
  107. setiastro/saspro/sfcc.py +662 -216
  108. setiastro/saspro/shortcuts.py +171 -33
  109. setiastro/saspro/signature_insert.py +692 -33
  110. setiastro/saspro/stacking_suite.py +1347 -485
  111. setiastro/saspro/star_alignment.py +247 -123
  112. setiastro/saspro/star_spikes.py +4 -0
  113. setiastro/saspro/star_stretch.py +38 -3
  114. setiastro/saspro/stat_stretch.py +892 -129
  115. setiastro/saspro/subwindow.py +787 -363
  116. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  117. setiastro/saspro/texture_clarity.py +593 -0
  118. setiastro/saspro/wavescale_hdr.py +4 -1
  119. setiastro/saspro/wavescalede.py +4 -1
  120. setiastro/saspro/whitebalance.py +84 -12
  121. setiastro/saspro/widgets/common_utilities.py +28 -21
  122. setiastro/saspro/widgets/resource_monitor.py +209 -111
  123. setiastro/saspro/widgets/spinboxes.py +10 -13
  124. setiastro/saspro/wimi.py +27 -656
  125. setiastro/saspro/wims.py +13 -3
  126. setiastro/saspro/xisf.py +101 -11
  127. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +4 -2
  128. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
  129. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
  130. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
  131. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
  132. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
@@ -0,0 +1,379 @@
1
+ # src/setiastro/saspro/acv_exporter.py
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from PyQt6.QtCore import Qt, QCoreApplication
9
+ from PyQt6.QtWidgets import (
10
+ QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox,
11
+ QLabel, QLineEdit, QPushButton, QFileDialog, QComboBox,
12
+ QMessageBox, QWidget
13
+ )
14
+
15
+ # If you have a canonical place for "global save_as", wire it here.
16
+ # Example placeholders (change to your actual import):
17
+ # from setiastro.saspro.actions.save_as import save_as
18
+ # from setiastro.saspro.fileio.save_as import save_as
19
+
20
+
21
+ def _tr(s: str) -> str:
22
+ return QCoreApplication.translate("AstroCatalogueViewerExporter", s)
23
+
24
+
25
+ class AstroCatalogueViewerExporterDialog(QDialog):
26
+ """
27
+ Astro Catalogue Viewer Exporter
28
+ - Persist folders in QSettings
29
+ - Export current active document/view via your global save_as
30
+ - Route by name prefix: M -> Messier, NGC -> NGC, IC -> IC, C -> Caldwell, else Master
31
+ """
32
+
33
+ # QSettings keys
34
+ K_MASTER = "acv_export/master_folder"
35
+ K_M = "acv_export/messier_folder"
36
+ K_NGC = "acv_export/ngc_folder"
37
+ K_IC = "acv_export/ic_folder"
38
+ K_C = "acv_export/caldwell_folder"
39
+ K_FMT = "acv_export/format"
40
+
41
+ def __init__(self, parent, dm, doc):
42
+ super().__init__(parent)
43
+ self.dm = dm
44
+ self.doc = doc
45
+ self.setObjectName("acv_exporter_dialog")
46
+ self.setWindowTitle(_tr("Astro Catalogue Viewer Exporter"))
47
+
48
+ # Prefer not using WA_DeleteOnClose if that’s a Linux landmine for you.
49
+ # (You can still set it from caller if you want.)
50
+ self._main = parent
51
+ self._settings = getattr(parent, "settings", None) # typical SASpro pattern
52
+
53
+ root = QVBoxLayout(self)
54
+ root.setContentsMargins(12, 12, 12, 12)
55
+ root.setSpacing(10)
56
+
57
+ # -------------------------
58
+ # Folder section
59
+ # -------------------------
60
+ gb = QGroupBox(_tr("Image folders"), self)
61
+ g = QGridLayout(gb)
62
+ g.setContentsMargins(10, 10, 10, 10)
63
+ g.setHorizontalSpacing(10)
64
+ g.setVerticalSpacing(8)
65
+
66
+ self.ed_master = QLineEdit(self)
67
+ self.btn_master = QPushButton(_tr("Browse…"), self)
68
+ self.btn_master.clicked.connect(lambda: self._browse_folder(self.ed_master))
69
+
70
+ g.addWidget(QLabel(_tr("Master Image Folder")), 0, 0)
71
+ g.addWidget(self.ed_master, 0, 1)
72
+ g.addWidget(self.btn_master, 0, 2)
73
+
74
+ gb2 = QGroupBox(_tr("Image folder per catalog"), self)
75
+ gg = QGridLayout(gb2)
76
+ gg.setContentsMargins(10, 10, 10, 10)
77
+ gg.setHorizontalSpacing(10)
78
+ gg.setVerticalSpacing(8)
79
+
80
+ self.ed_m = QLineEdit(self)
81
+ self.ed_ngc = QLineEdit(self)
82
+ self.ed_ic = QLineEdit(self)
83
+ self.ed_c = QLineEdit(self)
84
+
85
+ bm = QPushButton(_tr("Browse…"), self); bm.clicked.connect(lambda: self._browse_folder(self.ed_m))
86
+ bn = QPushButton(_tr("Browse…"), self); bn.clicked.connect(lambda: self._browse_folder(self.ed_ngc))
87
+ bi = QPushButton(_tr("Browse…"), self); bi.clicked.connect(lambda: self._browse_folder(self.ed_ic))
88
+ bc = QPushButton(_tr("Browse…"), self); bc.clicked.connect(lambda: self._browse_folder(self.ed_c))
89
+
90
+ gg.addWidget(QLabel(_tr("Messier")), 0, 0); gg.addWidget(self.ed_m, 0, 1); gg.addWidget(bm, 0, 2)
91
+ gg.addWidget(QLabel(_tr("NGC")), 1, 0); gg.addWidget(self.ed_ngc, 1, 1); gg.addWidget(bn, 1, 2)
92
+ gg.addWidget(QLabel(_tr("IC")), 2, 0); gg.addWidget(self.ed_ic, 2, 1); gg.addWidget(bi, 2, 2)
93
+ gg.addWidget(QLabel(_tr("Caldwell")),3, 0); gg.addWidget(self.ed_c, 3, 1); gg.addWidget(bc, 3, 2)
94
+
95
+ root.addWidget(gb)
96
+ root.addWidget(gb2)
97
+
98
+ # -------------------------
99
+ # Export controls
100
+ # -------------------------
101
+ row = QHBoxLayout()
102
+ row.setSpacing(10)
103
+
104
+ self.ed_name = QLineEdit(self)
105
+ self.ed_name.setPlaceholderText(_tr("e.g. M31, NGC5060, M31_HaOnly…"))
106
+
107
+ self.cmb_fmt = QComboBox(self)
108
+ # keep these lower-case as “extensions”
109
+ self.cmb_fmt.addItems(["jpg", "png", "tif"])
110
+
111
+ self.btn_export = QPushButton(_tr("Export"), self)
112
+ self.btn_export.clicked.connect(self._on_export)
113
+
114
+ row.addWidget(QLabel(_tr("Image Name")), 0)
115
+ row.addWidget(self.ed_name, 2)
116
+ row.addWidget(QLabel(_tr("Type")), 0)
117
+ row.addWidget(self.cmb_fmt, 0)
118
+ row.addWidget(self.btn_export, 0)
119
+
120
+ root.addLayout(row)
121
+
122
+ # Load persisted settings
123
+ self._load_settings()
124
+
125
+ # Save on edits (lightweight)
126
+ self.ed_master.textChanged.connect(self._save_settings)
127
+ self.ed_m.textChanged.connect(self._save_settings)
128
+ self.ed_ngc.textChanged.connect(self._save_settings)
129
+ self.ed_ic.textChanged.connect(self._save_settings)
130
+ self.ed_c.textChanged.connect(self._save_settings)
131
+ self.cmb_fmt.currentTextChanged.connect(self._save_settings)
132
+
133
+ # -------------------------
134
+ # Settings
135
+ # -------------------------
136
+ def _load_settings(self):
137
+ s = self._settings
138
+ if s is None:
139
+ return
140
+ self.ed_master.setText(s.value(self.K_MASTER, "", type=str) or "")
141
+ self.ed_m.setText(s.value(self.K_M, "", type=str) or "")
142
+ self.ed_ngc.setText(s.value(self.K_NGC, "", type=str) or "")
143
+ self.ed_ic.setText(s.value(self.K_IC, "", type=str) or "")
144
+ self.ed_c.setText(s.value(self.K_C, "", type=str) or "")
145
+ fmt = (s.value(self.K_FMT, "png", type=str) or "png").lower()
146
+ idx = self.cmb_fmt.findText(fmt)
147
+ if idx >= 0:
148
+ self.cmb_fmt.setCurrentIndex(idx)
149
+
150
+ def _save_settings(self):
151
+ s = self._settings
152
+ if s is None:
153
+ return
154
+ s.setValue(self.K_MASTER, self.ed_master.text().strip())
155
+ s.setValue(self.K_M, self.ed_m.text().strip())
156
+ s.setValue(self.K_NGC, self.ed_ngc.text().strip())
157
+ s.setValue(self.K_IC, self.ed_ic.text().strip())
158
+ s.setValue(self.K_C, self.ed_c.text().strip())
159
+ s.setValue(self.K_FMT, self.cmb_fmt.currentText().strip().lower())
160
+ try:
161
+ s.sync()
162
+ except Exception:
163
+ pass
164
+
165
+ # -------------------------
166
+ # Folder helpers
167
+ # -------------------------
168
+ def _browse_folder(self, target_edit: QLineEdit):
169
+ start = target_edit.text().strip() or os.path.expanduser("~")
170
+ path = QFileDialog.getExistingDirectory(self, _tr("Select folder"), start)
171
+ if path:
172
+ target_edit.setText(path)
173
+
174
+ # -------------------------
175
+ # Active doc resolution (ROI aware)
176
+ # -------------------------
177
+ def _get_active_doc(self):
178
+ mw = self._main
179
+ mdi = getattr(mw, "mdi", None)
180
+ dm = getattr(mw, "doc_manager", None) or getattr(mw, "docman", None)
181
+
182
+ if mdi is None or not hasattr(mdi, "activeSubWindow"):
183
+ return None
184
+
185
+ sw = mdi.activeSubWindow()
186
+ if not sw:
187
+ return None
188
+
189
+ view = sw.widget()
190
+
191
+ # Prefer ROI-aware doc lookup via DocManager
192
+ if dm is not None and hasattr(dm, "get_document_for_view"):
193
+ try:
194
+ doc = dm.get_document_for_view(view)
195
+ if doc is not None:
196
+ return doc
197
+ except Exception:
198
+ pass
199
+
200
+ # Fallback: view.document
201
+ return getattr(view, "document", None)
202
+
203
+ # -------------------------
204
+ # Routing logic
205
+ # -------------------------
206
+ @staticmethod
207
+ def _pick_target_folder(name: str, master: str, m: str, ngc: str, ic: str, c: str) -> str:
208
+ raw = (name or "").strip().upper()
209
+
210
+ # routing normalization: remove spaces (optionally also "_" and "-" if you want)
211
+ u = re.sub(r"\s+", "", raw)
212
+
213
+ if re.match(r"^M\d+", u):
214
+ return m or master
215
+ if re.match(r"^NGC\d+", u): # optional tighten
216
+ return ngc or master
217
+ if re.match(r"^IC\d+", u): # optional tighten
218
+ return ic or master
219
+ if re.match(r"^C\d+", u):
220
+ return c or master
221
+ return master
222
+
223
+ def _normalize_for_routing(self, name: str) -> str:
224
+ # Uppercase + remove spaces only (routing only)
225
+ u = (name or "").strip().upper()
226
+ u = re.sub(r"\s+", "", u)
227
+ return u
228
+
229
+ def _sanitize_filename(self, s: str) -> str:
230
+ # Keep it simple + safe across OSes
231
+ s = (s or "").strip()
232
+ s = s.replace("\\", "_").replace("/", "_").replace(":", "_")
233
+ s = re.sub(r"\s+", " ", s) # keep single spaces
234
+ s = re.sub(r"[^\w\-\.\(\) ]+", "", s) # drop odd chars
235
+ return s.strip() or "export"
236
+
237
+ # -------------------------
238
+ # Export action
239
+ # -------------------------
240
+ def _on_export(self):
241
+ fmt = self.cmb_fmt.currentText().strip().lower()
242
+
243
+ # -------------------------
244
+ # Name (raw + routing-normalized)
245
+ # -------------------------
246
+ raw_name = self.ed_name.text().strip()
247
+ if not raw_name:
248
+ QMessageBox.information(
249
+ self, _tr("Export"),
250
+ _tr("Please enter an Image Name (e.g. M31, NGC5060…).")
251
+ )
252
+ return
253
+
254
+ route_name = self._normalize_for_routing(raw_name)
255
+
256
+ # -------------------------
257
+ # Active doc (passed in)
258
+ # -------------------------
259
+ doc = self.doc
260
+ if doc is None or getattr(doc, "image", None) is None:
261
+ QMessageBox.information(self, _tr("Export"), _tr("No active image. Open an image first."))
262
+ return
263
+
264
+ # -------------------------
265
+ # Folder config
266
+ # -------------------------
267
+ master_dir = self.ed_master.text().strip()
268
+ if not master_dir:
269
+ QMessageBox.information(self, _tr("Export"), _tr("Please set a Master Image Folder."))
270
+ return
271
+
272
+ messier_dir = self.ed_m.text().strip() or master_dir
273
+ ngc_dir = self.ed_ngc.text().strip() or master_dir
274
+ ic_dir = self.ed_ic.text().strip() or master_dir
275
+ caldwell_dir = self.ed_c.text().strip() or master_dir
276
+
277
+ # -------------------------
278
+ # Route by prefix (digit-guard)
279
+ # -------------------------
280
+ if route_name.startswith("M") and route_name[1:2].isdigit():
281
+ base_dir = messier_dir
282
+ elif route_name.startswith("NGC") and route_name[3:4].isdigit():
283
+ base_dir = ngc_dir
284
+ elif route_name.startswith("IC") and route_name[2:3].isdigit():
285
+ base_dir = ic_dir
286
+ elif route_name.startswith("C") and route_name[1:2].isdigit():
287
+ base_dir = caldwell_dir
288
+ else:
289
+ base_dir = master_dir
290
+
291
+ # Ensure folder exists
292
+ try:
293
+ Path(base_dir).mkdir(parents=True, exist_ok=True)
294
+ except Exception as e:
295
+ QMessageBox.critical(
296
+ self, _tr("Export"),
297
+ _tr("Could not create folder:\n{0}\n\n{1}").format(base_dir, str(e))
298
+ )
299
+ return
300
+
301
+ # Filename uses user's raw name (sanitized for filesystem)
302
+ file_stem = self._sanitize_filename(raw_name)
303
+ out_path = str(Path(base_dir) / f"{file_stem}.{fmt}")
304
+
305
+ # -------------------------
306
+ # Save
307
+ # -------------------------
308
+ ok, err = self._save_current_doc_to_path(doc, out_path, fmt)
309
+ if ok:
310
+ QMessageBox.information(self, _tr("Export"), _tr("Exported:\n{0}").format(out_path))
311
+ else:
312
+ QMessageBox.critical(self, _tr("Export"), _tr("Export failed:\n{0}").format(err or "Unknown error"))
313
+
314
+
315
+ def _save_current_doc_to_path(self, doc, out_path: str, fmt: str) -> tuple[bool, str]:
316
+ try:
317
+ import numpy as np
318
+ from astropy.io import fits
319
+ from astropy.wcs import WCS
320
+
321
+ from setiastro.saspro.legacy.image_manager import save_image as legacy_save_image
322
+
323
+ img = getattr(doc, "image", None)
324
+ if img is None:
325
+ return False, "Active document has no image data."
326
+
327
+ # Ensure numpy array
328
+ img_array = np.asarray(img)
329
+
330
+ # Mono detection (your saver uses this for RAW/XISF paths; safe to provide anyway)
331
+ is_mono = (img_array.ndim == 2) or (img_array.ndim == 3 and img_array.shape[2] == 1)
332
+
333
+ # Preserve bit depth for tif/fit if the doc has it; PNG/JPG ignore bit depth anyway.
334
+ bit_depth = (
335
+ getattr(doc, "bit_depth", None)
336
+ or getattr(doc, "bitdepth", None)
337
+ or (getattr(doc, "metadata", None) or {}).get("bit_depth")
338
+ or (getattr(doc, "metadata", None) or {}).get("bitDepth")
339
+ )
340
+
341
+ # Pull headers/WCS from metadata (matches your SASv2->SASpro WCS preference)
342
+ md = getattr(doc, "metadata", None) or {}
343
+
344
+ original_header = md.get("original_header", None)
345
+ wcs_header = None
346
+
347
+ # If wcs is stored as astropy.wcs.WCS, convert to Header
348
+ wcs_obj = md.get("wcs", None)
349
+ if wcs_obj is not None:
350
+ try:
351
+ if isinstance(wcs_obj, WCS):
352
+ wcs_header = wcs_obj.to_header(relax=True)
353
+ elif isinstance(wcs_obj, fits.Header):
354
+ wcs_header = wcs_obj
355
+ except Exception:
356
+ wcs_header = None
357
+
358
+ # Optional passthroughs (safe if missing)
359
+ image_meta = md.get("image_meta", None) or md.get("image_metadata", None)
360
+ file_meta = md.get("file_meta", None) or md.get("xisf_metadata", None)
361
+
362
+ # IMPORTANT:
363
+ # legacy_save_image expects original_format to be the desired output format,
364
+ # and will normalize extension itself.
365
+ legacy_save_image(
366
+ img_array=img_array,
367
+ filename=out_path,
368
+ original_format=fmt, # "jpg","png","tif","fit"
369
+ bit_depth=bit_depth, # keep same depth for tif/fit when possible
370
+ original_header=original_header,
371
+ is_mono=is_mono,
372
+ image_meta=image_meta,
373
+ file_meta=file_meta,
374
+ wcs_header=wcs_header, # merged into FITS header (your saver supports this)
375
+ )
376
+ return True, ""
377
+
378
+ except Exception as e:
379
+ return False, f"{type(e).__name__}: {e}"
@@ -231,7 +231,10 @@ class AddStarsDialog(QDialog):
231
231
  self.setWindowModality(Qt.WindowModality.NonModal)
232
232
  self.setModal(False)
233
233
  #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
234
-
234
+ try:
235
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
236
+ except Exception:
237
+ pass # older PyQt6 versions
235
238
  self.main = main
236
239
  self.starless = None
237
240
  self.stars_only = None
@@ -575,10 +578,9 @@ class AddStarsDialog(QDialog):
575
578
 
576
579
  # Emit (target_doc, blended_image)
577
580
  self.stars_added.emit(target_doc, self.blended_image.astype(np.float32, copy=False))
578
- # Dialog stays open so user can apply to other images
579
- # Refresh combo boxes for next operation
580
- self._populate_doc_combos()
581
-
581
+ # Close UI after apply
582
+ self.accept() # or: self.close()
583
+ return
582
584
 
583
585
  # Ensure initial fit once shown
584
586
  def showEvent(self, ev):
@@ -603,7 +605,32 @@ def add_stars(main):
603
605
 
604
606
  dlg = AddStarsDialog(main, parent=main)
605
607
  dlg.stars_added.connect(lambda target, arr: _apply_to_doc(main, target, arr))
606
- dlg.exec()
608
+
609
+ # IMPORTANT: keep a strong reference (non-modal show)
610
+ if not hasattr(main, "_tool_dialogs"):
611
+ main._tool_dialogs = []
612
+ main._tool_dialogs.append(dlg)
613
+
614
+ # When the dialog closes, drop the reference
615
+ def _cleanup(_=None, d=dlg):
616
+ try:
617
+ if hasattr(main, "_tool_dialogs") and d in main._tool_dialogs:
618
+ main._tool_dialogs.remove(d)
619
+ except Exception:
620
+ pass
621
+
622
+ try:
623
+ dlg.finished.connect(_cleanup) # QDialog signal
624
+ except Exception:
625
+ pass
626
+ try:
627
+ dlg.destroyed.connect(_cleanup) # QObject signal (extra safety)
628
+ except Exception:
629
+ pass
630
+
631
+ dlg.show()
632
+ dlg.raise_()
633
+ dlg.activateWindow()
607
634
 
608
635
 
609
636
  def _apply_to_doc(main, doc, arr: np.ndarray):
@@ -685,9 +685,51 @@ def render_spikes(output: np.ndarray, stars: List[Star], config: SpikeConfig, ct
685
685
 
686
686
  # Main spikes
687
687
  if config.intensity > 0:
688
- for i in range(qty):
689
- theta = main_angle_rad + (i * (math.pi * 2) / float(qty))
690
- ...
688
+ rainbow_str = config.rainbow_spike_intensity if (config.enable_rainbow and config.rainbow_spikes) else 0
689
+ for i in range(int(config.quantity)):
690
+ theta = main_angle_rad + (i * (math.pi * 2) / float(config.quantity))
691
+ cos_t = math.cos(theta)
692
+ sin_t = math.sin(theta)
693
+
694
+ start_x = star.x + cos_t * 0.5
695
+ start_y = star.y + sin_t * 0.5
696
+ end_x = star.x + cos_t * base_length
697
+ end_y = star.y + sin_t * base_length
698
+
699
+ # Standard Spike
700
+ # Base star color, fading to zero alpha
701
+ c_end = (star.color.r/255.0, star.color.g/255.0, star.color.b/255.0, 0.0)
702
+
703
+ # If rainbow enabled, standard spike is dimmed (matches preview logic)
704
+ opacity_mult = 0.4 if rainbow_str > 0 else 1.0
705
+ c_start = (color[0], color[1], color[2], color[3] * opacity_mult)
706
+
707
+ draw_line_gradient(output, start_x, start_y, end_x, end_y,
708
+ c_start, c_end, thickness, config.sharpness)
709
+
710
+ # Rainbow Overlay
711
+ if rainbow_str > 0:
712
+ stops = 10
713
+ for s in range(stops):
714
+ p1 = s / stops
715
+ p2 = (s + 1) / stops
716
+ if p1 > config.rainbow_spike_length:
717
+ break
718
+
719
+ hue = (p1 * 360.0 * config.rainbow_spike_frequency) % 360.0
720
+ a_rainbow = min(1.0, config.intensity * rainbow_str * 2.0) * (1.0 - p1)
721
+ r_seg, g_seg, b_seg = hsl_to_rgb(hue / 360.0, 0.8, 0.6)
722
+ c_seg = (r_seg, g_seg, b_seg, a_rainbow)
723
+
724
+ # Calculate segment positions
725
+ sx = start_x + (end_x - start_x) * p1
726
+ sy = start_y + (end_y - start_y) * p1
727
+ ex = start_x + (end_x - start_x) * p2
728
+ ey = start_y + (end_y - start_y) * p2
729
+
730
+ # Draw rainbow segment with constant color
731
+ draw_line_gradient(output, sx, sy, ex, ey,
732
+ c_seg, c_seg, thickness, 1.0)
691
733
 
692
734
  # Secondary spikes
693
735
  if config.secondary_intensity > 0: