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.
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +364 -33
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +150 -55
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +181 -64
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +245 -15
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +706 -264
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +184 -8
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +68 -48
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +203 -82
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +81 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +73 -0
- setiastro/saspro/rgbalign.py +460 -12
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -0
- setiastro/saspro/sfcc.py +662 -216
- setiastro/saspro/shortcuts.py +171 -33
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1347 -485
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +892 -129
- setiastro/saspro/subwindow.py +787 -363
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +209 -111
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +4 -2
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -57,6 +57,120 @@ def _anchor_point(base_w: int, base_h: int, ins_w: int, ins_h: int,
|
|
|
57
57
|
}
|
|
58
58
|
return table.get(key, QPointF(right, bottom)) # default BR
|
|
59
59
|
|
|
60
|
+
# --------------------------- Header helpers (DocManager truth) ---------------------------
|
|
61
|
+
|
|
62
|
+
def _header_items(header_obj):
|
|
63
|
+
"""
|
|
64
|
+
Yield (key, value) pairs from whatever DocManager stored as the header.
|
|
65
|
+
Supports:
|
|
66
|
+
- astropy Header-like (has .cards OR .keys/.get) without importing astropy
|
|
67
|
+
- dict-like (has .items)
|
|
68
|
+
- list/tuple of (k, v) pairs
|
|
69
|
+
"""
|
|
70
|
+
if header_obj is None:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
# dict-like
|
|
74
|
+
items = getattr(header_obj, "items", None)
|
|
75
|
+
if callable(items):
|
|
76
|
+
try:
|
|
77
|
+
return list(header_obj.items())
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# astropy Header-like: prefer cards if present
|
|
82
|
+
cards = getattr(header_obj, "cards", None)
|
|
83
|
+
if cards is not None:
|
|
84
|
+
try:
|
|
85
|
+
out = []
|
|
86
|
+
for c in cards:
|
|
87
|
+
k = getattr(c, "keyword", None)
|
|
88
|
+
v = getattr(c, "value", None)
|
|
89
|
+
if k is not None:
|
|
90
|
+
out.append((k, v))
|
|
91
|
+
return out
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# Header-like: keys()+get()
|
|
96
|
+
keys = getattr(header_obj, "keys", None)
|
|
97
|
+
getv = getattr(header_obj, "get", None)
|
|
98
|
+
if callable(keys) and callable(getv):
|
|
99
|
+
try:
|
|
100
|
+
out = []
|
|
101
|
+
for k in header_obj.keys():
|
|
102
|
+
try:
|
|
103
|
+
out.append((k, header_obj.get(k)))
|
|
104
|
+
except Exception:
|
|
105
|
+
continue
|
|
106
|
+
return out
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# list of pairs
|
|
111
|
+
if isinstance(header_obj, (list, tuple)):
|
|
112
|
+
try:
|
|
113
|
+
out = []
|
|
114
|
+
for it in header_obj:
|
|
115
|
+
if isinstance(it, (list, tuple)) and len(it) >= 2:
|
|
116
|
+
out.append((it[0], it[1]))
|
|
117
|
+
return out
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _header_to_dict(header_obj) -> dict[str, str]:
|
|
125
|
+
"""
|
|
126
|
+
Normalize any header-ish object into {KEY: VALUE_STR}.
|
|
127
|
+
DOES NOT read from disk. Uses what DocManager already has.
|
|
128
|
+
"""
|
|
129
|
+
out: dict[str, str] = {}
|
|
130
|
+
for k, v in _header_items(header_obj):
|
|
131
|
+
if k is None:
|
|
132
|
+
continue
|
|
133
|
+
ks = str(k).strip()
|
|
134
|
+
if not ks:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Skip non-meaningful values
|
|
138
|
+
if v is None:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
vs = str(v).strip()
|
|
142
|
+
if not vs:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
out[ks] = vs
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _get_docmanager_header_dict(doc) -> dict[str, str]:
|
|
150
|
+
"""
|
|
151
|
+
Pull the *current* header already living in doc.metadata, as maintained by DocManager.
|
|
152
|
+
Priority should match your save logic expectations:
|
|
153
|
+
- wcs_header (if it's a real header object)
|
|
154
|
+
- fits_header
|
|
155
|
+
- original_header
|
|
156
|
+
- header
|
|
157
|
+
"""
|
|
158
|
+
meta = getattr(doc, "metadata", None) or {}
|
|
159
|
+
|
|
160
|
+
# IMPORTANT: This order matches what you WANT for display:
|
|
161
|
+
# show WCS-enhanced header if present, otherwise fall back.
|
|
162
|
+
for key in ("wcs_header", "fits_header", "original_header", "header"):
|
|
163
|
+
hdr_obj = meta.get(key)
|
|
164
|
+
if hdr_obj is None:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
d = _header_to_dict(hdr_obj)
|
|
168
|
+
if d:
|
|
169
|
+
return d
|
|
170
|
+
|
|
171
|
+
return {}
|
|
172
|
+
|
|
173
|
+
|
|
60
174
|
def apply_signature_preset_to_doc(doc, preset: dict) -> np.ndarray:
|
|
61
175
|
"""
|
|
62
176
|
Headless apply of signature/insert using a preset.
|
|
@@ -328,29 +442,137 @@ class InsertView(QGraphicsView):
|
|
|
328
442
|
self.fitInView(r, Qt.AspectRatioMode.KeepAspectRatio)
|
|
329
443
|
self.zoom_factor = 1.0 # logical reset
|
|
330
444
|
|
|
331
|
-
# --- context menu to snap inserts ---
|
|
332
445
|
def contextMenuEvent(self, e):
|
|
333
446
|
scene_pos = self.mapToScene(e.pos())
|
|
334
447
|
item = self.scene().itemAt(scene_pos, self.transform())
|
|
335
448
|
|
|
336
|
-
|
|
449
|
+
if item is None:
|
|
450
|
+
return super().contextMenuEvent(e)
|
|
451
|
+
|
|
452
|
+
# 1) If user clicked the child rect (bounding box) -> use the parent pixmap insert
|
|
337
453
|
if isinstance(item, QGraphicsRectItem) and item.parentItem() in self.owner.inserts:
|
|
338
454
|
item = item.parentItem()
|
|
339
455
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
456
|
+
# 2) If user clicked *inside* a TechCard (bg/border/text) -> walk up to TechCardItem
|
|
457
|
+
# (TechCardItem is a parent of its bg/border/text children)
|
|
458
|
+
p = item
|
|
459
|
+
while p is not None and not isinstance(p, TechCardItem):
|
|
460
|
+
p = p.parentItem()
|
|
461
|
+
if isinstance(p, TechCardItem):
|
|
462
|
+
item = p
|
|
463
|
+
|
|
464
|
+
# 3) Determine if this is a snap-eligible insert
|
|
465
|
+
is_pix_insert = isinstance(item, QGraphicsPixmapItem) and item in self.owner.inserts
|
|
466
|
+
is_text_insert = isinstance(item, QGraphicsTextItem) # includes OutlinedTextItem
|
|
467
|
+
is_tech_card = isinstance(item, TechCardItem)
|
|
468
|
+
|
|
469
|
+
if not (is_pix_insert or is_text_insert or is_tech_card):
|
|
470
|
+
return super().contextMenuEvent(e)
|
|
471
|
+
|
|
472
|
+
# 4) Build one menu
|
|
473
|
+
m = QMenu(self)
|
|
474
|
+
pos = {
|
|
475
|
+
"Top-Left": "top_left",
|
|
476
|
+
"Top-Center": "top_center",
|
|
477
|
+
"Top-Right": "top_right",
|
|
478
|
+
"Middle-Left": "middle_left",
|
|
479
|
+
"Center": "center",
|
|
480
|
+
"Middle-Right": "middle_right",
|
|
481
|
+
"Bottom-Left": "bottom_left",
|
|
482
|
+
"Bottom-Center": "bottom_center",
|
|
483
|
+
"Bottom-Right": "bottom_right",
|
|
484
|
+
}
|
|
485
|
+
for label, key in pos.items():
|
|
486
|
+
m.addAction(label, lambda k=key, it=item: self.owner.send_insert_to_position(it, k))
|
|
487
|
+
|
|
488
|
+
m.exec(e.globalPos())
|
|
489
|
+
e.accept()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class TechCardItem(QGraphicsItem):
|
|
493
|
+
def __init__(self, text_item: OutlinedTextItem):
|
|
494
|
+
super().__init__()
|
|
495
|
+
self.bg = QGraphicsRectItem(self)
|
|
496
|
+
self.border = QGraphicsRectItem(self)
|
|
497
|
+
self.text = text_item
|
|
498
|
+
self.text.setParentItem(self)
|
|
499
|
+
|
|
500
|
+
self.padding = 16
|
|
501
|
+
self.bg_color = QColor(0, 0, 0)
|
|
502
|
+
self.bg_opacity = 0.55
|
|
503
|
+
|
|
504
|
+
self.border_enabled = True
|
|
505
|
+
self.border_pen = QPen(QColor("white"), 2, Qt.PenStyle.SolidLine)
|
|
506
|
+
self.border_pen.setCosmetic(True)
|
|
507
|
+
|
|
508
|
+
# ✅ correct: QPen/QBrush objects
|
|
509
|
+
self.bg.setPen(QPen(Qt.PenStyle.NoPen))
|
|
510
|
+
self.bg.setBrush(QBrush(self.bg_color))
|
|
511
|
+
self.bg.setOpacity(self.bg_opacity)
|
|
512
|
+
|
|
513
|
+
self.border.setBrush(QBrush(Qt.BrushStyle.NoBrush))
|
|
514
|
+
self.border.setPen(self.border_pen)
|
|
515
|
+
self.border.setVisible(self.border_enabled)
|
|
516
|
+
|
|
517
|
+
self.setFlags(
|
|
518
|
+
QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
|
|
519
|
+
QGraphicsItem.GraphicsItemFlag.ItemIsSelectable |
|
|
520
|
+
QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
|
|
521
|
+
QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
|
522
|
+
)
|
|
523
|
+
self.setZValue(1)
|
|
524
|
+
self._rebuild_geometry()
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def boundingRect(self) -> QRectF:
|
|
528
|
+
# union of child rects (border covers bg; include text)
|
|
529
|
+
r = QRectF()
|
|
530
|
+
r = r.united(self.bg.rect())
|
|
531
|
+
r = r.united(self.border.rect())
|
|
532
|
+
r = r.united(self.text.mapRectToParent(self.text.boundingRect()))
|
|
533
|
+
return r
|
|
534
|
+
|
|
535
|
+
def paint(self, painter, option, widget=None):
|
|
536
|
+
# children paint themselves
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
def set_padding(self, px: int):
|
|
540
|
+
self.padding = max(0, int(px))
|
|
541
|
+
self._rebuild_geometry()
|
|
542
|
+
|
|
543
|
+
def set_background(self, c: QColor, opacity: float):
|
|
544
|
+
self.bg_color = QColor(c)
|
|
545
|
+
self.bg_opacity = max(0.0, min(1.0, float(opacity)))
|
|
546
|
+
self.bg.setBrush(QBrush(self.bg_color))
|
|
547
|
+
self.bg.setOpacity(self.bg_opacity)
|
|
548
|
+
|
|
549
|
+
def set_border(self, enabled: bool, pen: QPen):
|
|
550
|
+
self.border_enabled = bool(enabled)
|
|
551
|
+
self.border_pen = QPen(pen)
|
|
552
|
+
self.border_pen.setCosmetic(True)
|
|
553
|
+
self.border.setVisible(self.border_enabled)
|
|
554
|
+
self.border.setPen(self.border_pen)
|
|
555
|
+
|
|
556
|
+
def set_text(self, s: str):
|
|
557
|
+
self.text.setPlainText(s)
|
|
558
|
+
self._rebuild_geometry()
|
|
559
|
+
|
|
560
|
+
def _rebuild_geometry(self):
|
|
561
|
+
self.prepareGeometryChange() # <-- move to top
|
|
562
|
+
|
|
563
|
+
self.text.setPos(QPointF(self.padding, self.padding))
|
|
564
|
+
tr = self.text.mapRectToParent(self.text.boundingRect())
|
|
565
|
+
|
|
566
|
+
w = tr.width() + 2 * self.padding
|
|
567
|
+
h = tr.height() + 2 * self.padding
|
|
568
|
+
|
|
569
|
+
rect = QRectF(0, 0, w, h)
|
|
570
|
+
self.bg.setRect(rect)
|
|
571
|
+
self.border.setRect(rect)
|
|
572
|
+
|
|
573
|
+
self.setTransformOriginPoint(rect.center())
|
|
574
|
+
self.update()
|
|
575
|
+
|
|
354
576
|
|
|
355
577
|
|
|
356
578
|
# --------------------------- Main dialog ---------------------------
|
|
@@ -366,6 +588,10 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
366
588
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
367
589
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
368
590
|
self.setModal(False)
|
|
591
|
+
try:
|
|
592
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
593
|
+
except Exception:
|
|
594
|
+
pass # older PyQt6 versions
|
|
369
595
|
if icon:
|
|
370
596
|
try: self.setWindowIcon(icon)
|
|
371
597
|
except Exception as e:
|
|
@@ -380,13 +606,24 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
380
606
|
self.bounding_boxes: list[QGraphicsRectItem] = []
|
|
381
607
|
self.bounding_boxes_enabled = True
|
|
382
608
|
self.bounding_box_pen = QPen(QColor("red"), 2, Qt.PenStyle.DashLine)
|
|
609
|
+
self.bounding_box_pen.setCosmetic(True)
|
|
383
610
|
self.text_inserts: list[OutlinedTextItem] = []
|
|
384
611
|
self.scene.selectionChanged.connect(self._on_selection_changed)
|
|
385
612
|
# Handle sync timer (keeps the handle parked on the item corner)
|
|
386
613
|
self._timer = QTimer(self); self._timer.timeout.connect(self._sync_handles); self._timer.start(16)
|
|
614
|
+
self.tech_card_item: TechCardItem | None = None
|
|
615
|
+
self.tech_card_text: OutlinedTextItem | None = None
|
|
616
|
+
self.tech_fields_order: list[str] = []
|
|
617
|
+
self.tech_fields_enabled: set[str] = set()
|
|
618
|
+
self.tech_bg_color = QColor(0, 0, 0)
|
|
619
|
+
self.tech_border_color = QColor("white")
|
|
620
|
+
self.tech_text_fill = QColor("white")
|
|
621
|
+
self.tech_text_outline = QColor("black")
|
|
387
622
|
|
|
388
623
|
self._build_ui()
|
|
624
|
+
|
|
389
625
|
self._update_base_image()
|
|
626
|
+
self._tech_init_catalog()
|
|
390
627
|
self.resize(1000, 680)
|
|
391
628
|
|
|
392
629
|
# -------- UI ----------
|
|
@@ -448,6 +685,81 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
448
685
|
|
|
449
686
|
col.addWidget(txt_grp)
|
|
450
687
|
|
|
688
|
+
# --- Tech Card ---------------------------------------------------------
|
|
689
|
+
tech_grp = QGroupBox("Technical Card")
|
|
690
|
+
tc = QGridLayout(tech_grp)
|
|
691
|
+
|
|
692
|
+
self.cb_tech = QCheckBox("Enable Tech Card")
|
|
693
|
+
self.cb_tech.stateChanged.connect(self._tech_toggle)
|
|
694
|
+
|
|
695
|
+
self.btn_tech_build = QPushButton("Build / Update")
|
|
696
|
+
self.btn_tech_build.clicked.connect(self._tech_rebuild)
|
|
697
|
+
|
|
698
|
+
self.btn_tech_reset = QPushButton("Reset")
|
|
699
|
+
self.btn_tech_reset.clicked.connect(self._tech_reset_defaults)
|
|
700
|
+
|
|
701
|
+
tc.addWidget(self.cb_tech, 0, 0, 1, 2)
|
|
702
|
+
tc.addWidget(self.btn_tech_build, 0, 2)
|
|
703
|
+
tc.addWidget(self.btn_tech_reset, 0, 3)
|
|
704
|
+
|
|
705
|
+
# field list (simple: checklist combo)
|
|
706
|
+
self.cmb_tech_field = QComboBox()
|
|
707
|
+
self.btn_tech_add_field = QPushButton("Add Field")
|
|
708
|
+
self.btn_tech_remove_field = QPushButton("Remove Field")
|
|
709
|
+
self.btn_tech_up = QPushButton("Up")
|
|
710
|
+
self.btn_tech_down = QPushButton("Down")
|
|
711
|
+
|
|
712
|
+
self.btn_tech_add_field.clicked.connect(self._tech_add_field)
|
|
713
|
+
self.btn_tech_remove_field.clicked.connect(self._tech_remove_field)
|
|
714
|
+
self.btn_tech_up.clicked.connect(lambda: self._tech_move_field(-1))
|
|
715
|
+
self.btn_tech_down.clicked.connect(lambda: self._tech_move_field(+1))
|
|
716
|
+
|
|
717
|
+
tc.addWidget(QLabel("Fields"), 1, 0)
|
|
718
|
+
tc.addWidget(self.cmb_tech_field, 1, 1, 1, 2)
|
|
719
|
+
tc.addWidget(self.btn_tech_add_field, 1, 3)
|
|
720
|
+
|
|
721
|
+
self.cmb_tech_order = QComboBox() # shows current ordered selection
|
|
722
|
+
tc.addWidget(QLabel("Order"), 2, 0)
|
|
723
|
+
tc.addWidget(self.cmb_tech_order, 2, 1, 1, 2)
|
|
724
|
+
btns = QHBoxLayout()
|
|
725
|
+
btns.addWidget(self.btn_tech_remove_field)
|
|
726
|
+
btns.addWidget(self.btn_tech_up)
|
|
727
|
+
btns.addWidget(self.btn_tech_down)
|
|
728
|
+
w_btns = QWidget(); w_btns.setLayout(btns)
|
|
729
|
+
tc.addWidget(w_btns, 2, 3)
|
|
730
|
+
|
|
731
|
+
self.cb_tech_hide_empty = QCheckBox("Hide empty fields")
|
|
732
|
+
self.cb_tech_hide_empty.setChecked(True)
|
|
733
|
+
self.cb_tech_hide_empty.stateChanged.connect(lambda: self._tech_rebuild(live=True))
|
|
734
|
+
tc.addWidget(self.cb_tech_hide_empty, 3, 0, 1, 2)
|
|
735
|
+
|
|
736
|
+
# style controls
|
|
737
|
+
self.sp_tech_padding = QSpinBox(); self.sp_tech_padding.setRange(0, 200); self.sp_tech_padding.setValue(16)
|
|
738
|
+
self.sp_tech_padding.valueChanged.connect(lambda: self._tech_rebuild(live=True))
|
|
739
|
+
|
|
740
|
+
self.sl_tech_bg_opacity = QSlider(Qt.Orientation.Horizontal); self.sl_tech_bg_opacity.setRange(0, 100); self.sl_tech_bg_opacity.setValue(55)
|
|
741
|
+
self.sl_tech_bg_opacity.valueChanged.connect(lambda: self._tech_rebuild(live=True))
|
|
742
|
+
|
|
743
|
+
self.cb_tech_border = QCheckBox("Border"); self.cb_tech_border.setChecked(True)
|
|
744
|
+
self.cb_tech_border.stateChanged.connect(lambda: self._tech_rebuild(live=True))
|
|
745
|
+
|
|
746
|
+
self.sp_tech_border_w = QSpinBox(); self.sp_tech_border_w.setRange(0, 30); self.sp_tech_border_w.setValue(2)
|
|
747
|
+
self.sp_tech_border_w.valueChanged.connect(lambda: self._tech_rebuild(live=True))
|
|
748
|
+
|
|
749
|
+
self.cmb_tech_border_style = QComboBox()
|
|
750
|
+
self.cmb_tech_border_style.addItems(["Solid","Dash","Dot","DashDot","DashDotDot"])
|
|
751
|
+
self.cmb_tech_border_style.currentIndexChanged.connect(lambda: self._tech_rebuild(live=True))
|
|
752
|
+
|
|
753
|
+
self.btn_tech_bg = QPushButton("BG Color…"); self.btn_tech_bg.clicked.connect(self._tech_pick_bg)
|
|
754
|
+
self.btn_tech_border_color = QPushButton("Border Color…"); self.btn_tech_border_color.clicked.connect(self._tech_pick_border)
|
|
755
|
+
|
|
756
|
+
tc.addWidget(QLabel("Padding"), 4, 0); tc.addWidget(self.sp_tech_padding, 4, 1)
|
|
757
|
+
tc.addWidget(QLabel("BG Opacity"), 5, 0); tc.addWidget(self.sl_tech_bg_opacity, 5, 1, 1, 3)
|
|
758
|
+
tc.addWidget(self.btn_tech_bg, 4, 2); tc.addWidget(self.btn_tech_border_color, 4, 3)
|
|
759
|
+
tc.addWidget(self.cb_tech_border, 6, 0); tc.addWidget(QLabel("Width"), 6, 1); tc.addWidget(self.sp_tech_border_w, 6, 2); tc.addWidget(self.cmb_tech_border_style, 6, 3)
|
|
760
|
+
|
|
761
|
+
col.addWidget(tech_grp)
|
|
762
|
+
|
|
451
763
|
|
|
452
764
|
# Transform group
|
|
453
765
|
grp = QGroupBox("Transform")
|
|
@@ -536,6 +848,279 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
536
848
|
root.addWidget(left, 0)
|
|
537
849
|
root.addWidget(self.view, 1)
|
|
538
850
|
|
|
851
|
+
def _tech_active_doc(self):
|
|
852
|
+
dm = getattr(self, "doc_manager", None)
|
|
853
|
+
if dm is not None:
|
|
854
|
+
try:
|
|
855
|
+
d = dm.get_active_document()
|
|
856
|
+
if d is not None:
|
|
857
|
+
return d
|
|
858
|
+
except Exception:
|
|
859
|
+
pass
|
|
860
|
+
return getattr(self, "doc", None) or getattr(self, "_doc", None)
|
|
861
|
+
|
|
862
|
+
def _tech_pick_header_obj(self, doc):
|
|
863
|
+
meta = getattr(doc, "metadata", None) or {}
|
|
864
|
+
for key in ("wcs_header", "fits_header", "original_header", "header"):
|
|
865
|
+
h = meta.get(key)
|
|
866
|
+
if h is not None:
|
|
867
|
+
return h
|
|
868
|
+
return None
|
|
869
|
+
|
|
870
|
+
def _tech_header_to_dict(self, hdr_obj) -> dict[str, str]:
|
|
871
|
+
"""
|
|
872
|
+
Convert whatever header object DocManager stored into {KEY: VALUE_STR}.
|
|
873
|
+
No disk IO. No astropy import.
|
|
874
|
+
"""
|
|
875
|
+
if hdr_obj is None:
|
|
876
|
+
return {}
|
|
877
|
+
|
|
878
|
+
# dict-like
|
|
879
|
+
items = getattr(hdr_obj, "items", None)
|
|
880
|
+
if callable(items):
|
|
881
|
+
try:
|
|
882
|
+
return {str(k).strip(): str(v).strip() for k, v in hdr_obj.items()
|
|
883
|
+
if str(k).strip() and str(v).strip() and str(v).strip() != "None"}
|
|
884
|
+
except Exception:
|
|
885
|
+
pass
|
|
886
|
+
|
|
887
|
+
# astropy Header-like: cards iterable with .keyword/.value
|
|
888
|
+
cards = getattr(hdr_obj, "cards", None)
|
|
889
|
+
if cards is not None:
|
|
890
|
+
out = {}
|
|
891
|
+
try:
|
|
892
|
+
for c in cards:
|
|
893
|
+
k = getattr(c, "keyword", None)
|
|
894
|
+
v = getattr(c, "value", None)
|
|
895
|
+
if k is None or v is None:
|
|
896
|
+
continue
|
|
897
|
+
ks = str(k).strip()
|
|
898
|
+
vs = str(v).strip()
|
|
899
|
+
if ks and vs and vs != "None":
|
|
900
|
+
out[ks] = vs
|
|
901
|
+
if out:
|
|
902
|
+
return out
|
|
903
|
+
except Exception:
|
|
904
|
+
pass
|
|
905
|
+
|
|
906
|
+
# Header-like: keys()+get()
|
|
907
|
+
keys = getattr(hdr_obj, "keys", None)
|
|
908
|
+
getv = getattr(hdr_obj, "get", None)
|
|
909
|
+
if callable(keys) and callable(getv):
|
|
910
|
+
out = {}
|
|
911
|
+
try:
|
|
912
|
+
for k in hdr_obj.keys():
|
|
913
|
+
try:
|
|
914
|
+
v = hdr_obj.get(k)
|
|
915
|
+
except Exception:
|
|
916
|
+
continue
|
|
917
|
+
if v is None:
|
|
918
|
+
continue
|
|
919
|
+
ks = str(k).strip()
|
|
920
|
+
vs = str(v).strip()
|
|
921
|
+
if ks and vs and vs != "None":
|
|
922
|
+
out[ks] = vs
|
|
923
|
+
return out
|
|
924
|
+
except Exception:
|
|
925
|
+
pass
|
|
926
|
+
|
|
927
|
+
return {}
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _tech_init_catalog(self):
|
|
931
|
+
catalog = self._build_tech_field_catalog()
|
|
932
|
+
keys = list(catalog.keys())
|
|
933
|
+
self.cmb_tech_field.clear()
|
|
934
|
+
self.cmb_tech_field.addItems(keys)
|
|
935
|
+
|
|
936
|
+
# defaults: populate order + enabled if empty
|
|
937
|
+
if not self.tech_fields_order:
|
|
938
|
+
defaults = ["OBJECT","DATE-OBS","EXPTIME","FILTER","GAIN","OFFSET","INSTRUME","TELESCOP","FOCALLEN","XPIXSZ","BAYERPAT","Size","Bit Depth","Mono"]
|
|
939
|
+
self.tech_fields_order = [k for k in defaults if k in catalog]
|
|
940
|
+
self.tech_fields_enabled = set(self.tech_fields_order)
|
|
941
|
+
|
|
942
|
+
self._tech_refresh_order_combo()
|
|
943
|
+
|
|
944
|
+
def _tech_refresh_order_combo(self):
|
|
945
|
+
self.cmb_tech_order.blockSignals(True)
|
|
946
|
+
self.cmb_tech_order.clear()
|
|
947
|
+
self.cmb_tech_order.addItems(self.tech_fields_order)
|
|
948
|
+
self.cmb_tech_order.blockSignals(False)
|
|
949
|
+
|
|
950
|
+
def _tech_toggle(self, state):
|
|
951
|
+
enabled = bool(state)
|
|
952
|
+
if enabled:
|
|
953
|
+
self._tech_ensure_item()
|
|
954
|
+
self._tech_rebuild()
|
|
955
|
+
else:
|
|
956
|
+
self._tech_remove_item()
|
|
957
|
+
|
|
958
|
+
def _tech_ensure_item(self):
|
|
959
|
+
if self.tech_card_item is not None:
|
|
960
|
+
return
|
|
961
|
+
|
|
962
|
+
# Create a text item for the card (outlined)
|
|
963
|
+
f = self._current_qfont()
|
|
964
|
+
self.tech_card_text = OutlinedTextItem("", f, self.tech_text_fill, self.tech_text_outline, outline_w=float(self.outline_w.value()))
|
|
965
|
+
self.tech_card_text.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) # card text is not editable directly
|
|
966
|
+
self.tech_card_text.setZValue(1)
|
|
967
|
+
|
|
968
|
+
self.tech_card_item = TechCardItem(self.tech_card_text)
|
|
969
|
+
self.tech_card_item.setZValue(1)
|
|
970
|
+
self.scene.addItem(self.tech_card_item)
|
|
971
|
+
|
|
972
|
+
TransformHandle(self.tech_card_item, self.scene)
|
|
973
|
+
|
|
974
|
+
# drop near bottom-right by default using your snap
|
|
975
|
+
self.tech_card_item.setSelected(True)
|
|
976
|
+
self.send_insert_to_position(self.tech_card_item, "top left")
|
|
977
|
+
|
|
978
|
+
def _tech_remove_item(self):
|
|
979
|
+
if self.tech_card_item is None:
|
|
980
|
+
return
|
|
981
|
+
# Remove handle tied to tech card
|
|
982
|
+
for it in list(self.scene.items()):
|
|
983
|
+
if isinstance(it, TransformHandle) and getattr(it, "parent_item", None) is self.tech_card_item:
|
|
984
|
+
try: self.scene.removeItem(it)
|
|
985
|
+
except Exception: pass
|
|
986
|
+
|
|
987
|
+
try:
|
|
988
|
+
self.scene.removeItem(self.tech_card_item)
|
|
989
|
+
except Exception:
|
|
990
|
+
pass
|
|
991
|
+
|
|
992
|
+
self.tech_card_item = None
|
|
993
|
+
self.tech_card_text = None
|
|
994
|
+
|
|
995
|
+
def _tech_pick_bg(self):
|
|
996
|
+
c = QColorDialog.getColor(self.tech_bg_color, self, "Tech Card Background")
|
|
997
|
+
if c.isValid():
|
|
998
|
+
self.tech_bg_color = c
|
|
999
|
+
self._tech_rebuild(live=True)
|
|
1000
|
+
|
|
1001
|
+
def _tech_pick_border(self):
|
|
1002
|
+
c = QColorDialog.getColor(self.tech_border_color, self, "Tech Card Border")
|
|
1003
|
+
if c.isValid():
|
|
1004
|
+
self.tech_border_color = c
|
|
1005
|
+
self._tech_rebuild(live=True)
|
|
1006
|
+
|
|
1007
|
+
def _tech_add_field(self):
|
|
1008
|
+
k = self.cmb_tech_field.currentText().strip()
|
|
1009
|
+
if not k:
|
|
1010
|
+
return
|
|
1011
|
+
if k not in self.tech_fields_order:
|
|
1012
|
+
self.tech_fields_order.append(k)
|
|
1013
|
+
self.tech_fields_enabled.add(k)
|
|
1014
|
+
self._tech_refresh_order_combo()
|
|
1015
|
+
self._tech_rebuild(live=True)
|
|
1016
|
+
|
|
1017
|
+
def _tech_remove_field(self):
|
|
1018
|
+
k = self.cmb_tech_order.currentText().strip()
|
|
1019
|
+
if not k:
|
|
1020
|
+
return
|
|
1021
|
+
if k in self.tech_fields_order:
|
|
1022
|
+
self.tech_fields_order.remove(k)
|
|
1023
|
+
self.tech_fields_enabled.discard(k)
|
|
1024
|
+
self._tech_refresh_order_combo()
|
|
1025
|
+
self._tech_rebuild(live=True)
|
|
1026
|
+
|
|
1027
|
+
def _tech_move_field(self, delta: int):
|
|
1028
|
+
k = self.cmb_tech_order.currentText().strip()
|
|
1029
|
+
if not k or k not in self.tech_fields_order:
|
|
1030
|
+
return
|
|
1031
|
+
i = self.tech_fields_order.index(k)
|
|
1032
|
+
j = max(0, min(len(self.tech_fields_order) - 1, i + int(delta)))
|
|
1033
|
+
if i == j:
|
|
1034
|
+
return
|
|
1035
|
+
self.tech_fields_order.insert(j, self.tech_fields_order.pop(i))
|
|
1036
|
+
self._tech_refresh_order_combo()
|
|
1037
|
+
self.cmb_tech_order.setCurrentText(k)
|
|
1038
|
+
self._tech_rebuild(live=True)
|
|
1039
|
+
|
|
1040
|
+
def _tech_reset_defaults(self):
|
|
1041
|
+
self.tech_fields_order = []
|
|
1042
|
+
self.tech_fields_enabled = set()
|
|
1043
|
+
self.tech_bg_color = QColor(0, 0, 0)
|
|
1044
|
+
self.tech_border_color = QColor("white")
|
|
1045
|
+
self.tech_text_fill = QColor("white")
|
|
1046
|
+
self.tech_text_outline = QColor("black")
|
|
1047
|
+
self.sp_tech_padding.setValue(16)
|
|
1048
|
+
self.sl_tech_bg_opacity.setValue(55)
|
|
1049
|
+
self.cb_tech_border.setChecked(True)
|
|
1050
|
+
self.sp_tech_border_w.setValue(2)
|
|
1051
|
+
self.cmb_tech_border_style.setCurrentText("Solid")
|
|
1052
|
+
self.cb_tech_hide_empty.setChecked(True)
|
|
1053
|
+
self._tech_init_catalog()
|
|
1054
|
+
self._tech_rebuild()
|
|
1055
|
+
|
|
1056
|
+
def _tech_format_text(self) -> str:
|
|
1057
|
+
catalog = self._build_tech_field_catalog()
|
|
1058
|
+
hide_empty = self.cb_tech_hide_empty.isChecked()
|
|
1059
|
+
|
|
1060
|
+
lines = []
|
|
1061
|
+
for k in self.tech_fields_order:
|
|
1062
|
+
if k not in self.tech_fields_enabled:
|
|
1063
|
+
continue
|
|
1064
|
+
v = str(catalog.get(k, "")).strip()
|
|
1065
|
+
if hide_empty and not v:
|
|
1066
|
+
continue
|
|
1067
|
+
# Friendly: strip full file path by default (optional)
|
|
1068
|
+
if k == "File" and v:
|
|
1069
|
+
try:
|
|
1070
|
+
import os
|
|
1071
|
+
v = os.path.basename(v)
|
|
1072
|
+
except Exception:
|
|
1073
|
+
pass
|
|
1074
|
+
lines.append(f"{k}: {v}" if v else f"{k}:")
|
|
1075
|
+
|
|
1076
|
+
return "\n".join(lines) if lines else "No fields selected."
|
|
1077
|
+
|
|
1078
|
+
def _tech_border_pen(self) -> QPen:
|
|
1079
|
+
style_map = {
|
|
1080
|
+
"Solid": Qt.PenStyle.SolidLine,
|
|
1081
|
+
"Dash": Qt.PenStyle.DashLine,
|
|
1082
|
+
"Dot": Qt.PenStyle.DotLine,
|
|
1083
|
+
"DashDot": Qt.PenStyle.DashDotLine,
|
|
1084
|
+
"DashDotDot": Qt.PenStyle.DashDotDotLine
|
|
1085
|
+
}
|
|
1086
|
+
pen = QPen(self.tech_border_color, float(self.sp_tech_border_w.value()), style_map[self.cmb_tech_border_style.currentText()])
|
|
1087
|
+
pen.setCosmetic(True)
|
|
1088
|
+
return pen
|
|
1089
|
+
|
|
1090
|
+
def _tech_rebuild(self, live: bool = False):
|
|
1091
|
+
if not self.cb_tech.isChecked():
|
|
1092
|
+
return
|
|
1093
|
+
self._tech_ensure_item()
|
|
1094
|
+
if self.tech_card_item is None or self.tech_card_text is None:
|
|
1095
|
+
return
|
|
1096
|
+
|
|
1097
|
+
# Update text styling from the existing text controls (so UI stays consistent)
|
|
1098
|
+
f = self._current_qfont()
|
|
1099
|
+
self.tech_card_text.set_font(f)
|
|
1100
|
+
self.tech_card_text.set_fill(self.tech_text_fill)
|
|
1101
|
+
ow = float(self.outline_w.value())
|
|
1102
|
+
if ow > 0:
|
|
1103
|
+
self.tech_card_text.set_outline(self.tech_text_outline, ow)
|
|
1104
|
+
else:
|
|
1105
|
+
self.tech_card_text.set_outline(None, 0.0)
|
|
1106
|
+
|
|
1107
|
+
# Update card style
|
|
1108
|
+
pad = int(self.sp_tech_padding.value())
|
|
1109
|
+
bg_op = float(self.sl_tech_bg_opacity.value()) / 100.0
|
|
1110
|
+
self.tech_card_item.set_padding(pad)
|
|
1111
|
+
self.tech_card_item.set_background(self.tech_bg_color, bg_op)
|
|
1112
|
+
|
|
1113
|
+
border_on = self.cb_tech_border.isChecked() and self.sp_tech_border_w.value() > 0
|
|
1114
|
+
self.tech_card_item.set_border(border_on, self._tech_border_pen())
|
|
1115
|
+
|
|
1116
|
+
# Update text content
|
|
1117
|
+
txt = self._tech_format_text()
|
|
1118
|
+
self.tech_card_item.set_text(txt)
|
|
1119
|
+
|
|
1120
|
+
# Keep transform handle correct
|
|
1121
|
+
self._sync_handles()
|
|
1122
|
+
|
|
1123
|
+
|
|
539
1124
|
def _selected_text_items(self):
|
|
540
1125
|
return [it for it in self.scene.selectedItems() if isinstance(it, QGraphicsTextItem)]
|
|
541
1126
|
|
|
@@ -737,6 +1322,10 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
737
1322
|
bg = QGraphicsPixmapItem(QPixmap.fromImage(qimg))
|
|
738
1323
|
bg.setZValue(0)
|
|
739
1324
|
self.scene.addItem(bg)
|
|
1325
|
+
self._tech_init_catalog()
|
|
1326
|
+
if self.cb_tech.isChecked():
|
|
1327
|
+
self._tech_rebuild(live=True)
|
|
1328
|
+
|
|
740
1329
|
|
|
741
1330
|
def _load_from_file(self):
|
|
742
1331
|
fp, _ = QFileDialog.getOpenFileName(self, "Select Insert Image", "", "Images (*.png *.jpg *.jpeg *.tif *.tiff)")
|
|
@@ -802,6 +1391,10 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
802
1391
|
# text
|
|
803
1392
|
for ti in self._selected_text_items():
|
|
804
1393
|
self.send_insert_to_position(ti, key)
|
|
1394
|
+
# tech card
|
|
1395
|
+
if self.tech_card_item is not None and self.tech_card_item.isSelected():
|
|
1396
|
+
self.send_insert_to_position(self.tech_card_item, key)
|
|
1397
|
+
|
|
805
1398
|
|
|
806
1399
|
# -------- Commands ----------
|
|
807
1400
|
def _rotate_selected(self):
|
|
@@ -845,6 +1438,7 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
845
1438
|
}
|
|
846
1439
|
self.bounding_box_pen.setWidth(self.sl_thick.value())
|
|
847
1440
|
self.bounding_box_pen.setStyle(style_map[self.cmb_style.currentText()])
|
|
1441
|
+
self.bounding_box_pen.setCosmetic(True)
|
|
848
1442
|
self._refresh_all_boxes()
|
|
849
1443
|
|
|
850
1444
|
def _refresh_all_boxes(self):
|
|
@@ -964,6 +1558,8 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
964
1558
|
items.append(it)
|
|
965
1559
|
elif self.bounding_boxes_enabled and isinstance(it, QGraphicsRectItem):
|
|
966
1560
|
items.append(it)
|
|
1561
|
+
elif isinstance(it, TechCardItem):
|
|
1562
|
+
items.append(it)
|
|
967
1563
|
|
|
968
1564
|
# compute scene bbox
|
|
969
1565
|
bbox = QRectF()
|
|
@@ -1021,23 +1617,6 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
1021
1617
|
if had_focus:
|
|
1022
1618
|
it.setFocus()
|
|
1023
1619
|
|
|
1024
|
-
# temporarily hide non-items
|
|
1025
|
-
hidden = []
|
|
1026
|
-
for it in self.scene.items():
|
|
1027
|
-
if it not in items:
|
|
1028
|
-
it.setVisible(False); hidden.append(it)
|
|
1029
|
-
|
|
1030
|
-
# render
|
|
1031
|
-
out = QImage(w, h, QImage.Format.Format_ARGB32)
|
|
1032
|
-
out.fill(Qt.GlobalColor.transparent)
|
|
1033
|
-
p = QPainter(out)
|
|
1034
|
-
self.scene.render(p, target=QRectF(0, 0, w, h), source=QRectF(x, y, w, h))
|
|
1035
|
-
p.end()
|
|
1036
|
-
|
|
1037
|
-
# restore
|
|
1038
|
-
for it in hidden: it.setVisible(True)
|
|
1039
|
-
for r in hidden_boxes: r.setVisible(True)
|
|
1040
|
-
|
|
1041
1620
|
# drop alpha → RGB, write back to doc
|
|
1042
1621
|
arr = self._qimage_to_numpy(out)
|
|
1043
1622
|
if arr.shape[2] == 4:
|
|
@@ -1075,6 +1654,86 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
1075
1654
|
pass
|
|
1076
1655
|
self.bounding_boxes.clear()
|
|
1077
1656
|
|
|
1657
|
+
def _build_tech_field_catalog(self) -> dict[str, str]:
|
|
1658
|
+
"""
|
|
1659
|
+
Returns a merged catalog of:
|
|
1660
|
+
- header-derived fields (OBJECT/DATE-OBS/etc) from the in-memory doc header
|
|
1661
|
+
- convenience fields (File/Size/Bit Depth/Mono)
|
|
1662
|
+
"""
|
|
1663
|
+
doc = self._tech_active_doc()
|
|
1664
|
+
if doc is None:
|
|
1665
|
+
return {}
|
|
1666
|
+
|
|
1667
|
+
meta = getattr(doc, "metadata", None) or {}
|
|
1668
|
+
|
|
1669
|
+
# --- header from DocManager (in-memory) ---
|
|
1670
|
+
hdr_obj = self._tech_pick_header_obj(doc)
|
|
1671
|
+
H = self._tech_header_to_dict(hdr_obj)
|
|
1672
|
+
|
|
1673
|
+
# Case-insensitive getter for FITS keys
|
|
1674
|
+
# (FITS keys are usually uppercase, but play safe)
|
|
1675
|
+
def hget(key: str) -> str:
|
|
1676
|
+
if not key:
|
|
1677
|
+
return ""
|
|
1678
|
+
if key in H:
|
|
1679
|
+
return H.get(key, "") or ""
|
|
1680
|
+
up = key.upper()
|
|
1681
|
+
if up in H:
|
|
1682
|
+
return H.get(up, "") or ""
|
|
1683
|
+
# last resort: scan once (small headers, OK)
|
|
1684
|
+
for k, v in H.items():
|
|
1685
|
+
if str(k).upper() == up:
|
|
1686
|
+
return v or ""
|
|
1687
|
+
return ""
|
|
1688
|
+
|
|
1689
|
+
# --- computed fields ---
|
|
1690
|
+
# File: prefer actual file path from metadata
|
|
1691
|
+
file_path = meta.get("file_path") or ""
|
|
1692
|
+
# Size: prefer header if available (NAXIS1/2), fallback to image shape
|
|
1693
|
+
naxis1 = hget("NAXIS1")
|
|
1694
|
+
naxis2 = hget("NAXIS2")
|
|
1695
|
+
if naxis1 and naxis2:
|
|
1696
|
+
size_str = f"{naxis1}×{naxis2}"
|
|
1697
|
+
else:
|
|
1698
|
+
img = getattr(doc, "image", None)
|
|
1699
|
+
try:
|
|
1700
|
+
if img is not None and hasattr(img, "shape"):
|
|
1701
|
+
if img.ndim == 2:
|
|
1702
|
+
h, w = img.shape[:2]
|
|
1703
|
+
else:
|
|
1704
|
+
h, w = img.shape[:2]
|
|
1705
|
+
size_str = f"{w}×{h}"
|
|
1706
|
+
else:
|
|
1707
|
+
size_str = ""
|
|
1708
|
+
except Exception:
|
|
1709
|
+
size_str = ""
|
|
1710
|
+
|
|
1711
|
+
bit_depth = meta.get("bit_depth") or ""
|
|
1712
|
+
# Mono: metadata uses is_mono in your loader; your save uses "mono" sometimes; handle both
|
|
1713
|
+
mono_val = meta.get("is_mono", meta.get("mono", None))
|
|
1714
|
+
if mono_val is None:
|
|
1715
|
+
img = getattr(doc, "image", None)
|
|
1716
|
+
mono_val = bool(img.ndim == 2) if hasattr(img, "ndim") else False
|
|
1717
|
+
mono_str = "Yes" if bool(mono_val) else "No"
|
|
1718
|
+
|
|
1719
|
+
# --- build catalog ---
|
|
1720
|
+
catalog: dict[str, str] = {}
|
|
1721
|
+
|
|
1722
|
+
# 1) Header keys: include all FITS keys as selectable fields
|
|
1723
|
+
# (This is why your dropdown will finally show everything in that big list.)
|
|
1724
|
+
for k, v in H.items():
|
|
1725
|
+
ks = str(k).strip()
|
|
1726
|
+
if not ks:
|
|
1727
|
+
continue
|
|
1728
|
+
catalog[ks] = str(v).strip()
|
|
1729
|
+
|
|
1730
|
+
# 2) Friendly extras (these are the ones you have in defaults)
|
|
1731
|
+
catalog.setdefault("File", str(file_path))
|
|
1732
|
+
catalog["Size"] = size_str
|
|
1733
|
+
catalog["Bit Depth"] = str(bit_depth)
|
|
1734
|
+
catalog["Mono"] = mono_str
|
|
1735
|
+
|
|
1736
|
+
return catalog
|
|
1078
1737
|
|
|
1079
1738
|
# ------------------ numpy/QImage bridges ------------------
|
|
1080
1739
|
def _numpy_to_qimage(self, a: np.ndarray) -> QImage:
|