setiastrosuitepro 1.6.7__py3-none-any.whl → 1.6.10__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/abeicon.svg +16 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/add_stars.py +29 -5
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/cosmicclarity.py +125 -18
- setiastro/saspro/crop_dialog_pro.py +96 -2
- setiastro/saspro/curve_editor_pro.py +60 -39
- setiastro/saspro/frequency_separation.py +1159 -208
- setiastro/saspro/gui/main_window.py +131 -31
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/update_mixin.py +121 -33
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/live_stacking.py +158 -70
- setiastro/saspro/multiscale_decomp.py +47 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/shortcuts.py +122 -12
- setiastro/saspro/signature_insert.py +688 -33
- setiastro/saspro/stacking_suite.py +523 -316
- setiastro/saspro/stat_stretch.py +688 -130
- setiastro/saspro/subwindow.py +302 -71
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +7 -7
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +37 -31
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.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 ---------------------------
|
|
@@ -384,13 +606,24 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
384
606
|
self.bounding_boxes: list[QGraphicsRectItem] = []
|
|
385
607
|
self.bounding_boxes_enabled = True
|
|
386
608
|
self.bounding_box_pen = QPen(QColor("red"), 2, Qt.PenStyle.DashLine)
|
|
609
|
+
self.bounding_box_pen.setCosmetic(True)
|
|
387
610
|
self.text_inserts: list[OutlinedTextItem] = []
|
|
388
611
|
self.scene.selectionChanged.connect(self._on_selection_changed)
|
|
389
612
|
# Handle sync timer (keeps the handle parked on the item corner)
|
|
390
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")
|
|
391
622
|
|
|
392
623
|
self._build_ui()
|
|
624
|
+
|
|
393
625
|
self._update_base_image()
|
|
626
|
+
self._tech_init_catalog()
|
|
394
627
|
self.resize(1000, 680)
|
|
395
628
|
|
|
396
629
|
# -------- UI ----------
|
|
@@ -452,6 +685,81 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
452
685
|
|
|
453
686
|
col.addWidget(txt_grp)
|
|
454
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
|
+
|
|
455
763
|
|
|
456
764
|
# Transform group
|
|
457
765
|
grp = QGroupBox("Transform")
|
|
@@ -540,6 +848,279 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
540
848
|
root.addWidget(left, 0)
|
|
541
849
|
root.addWidget(self.view, 1)
|
|
542
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
|
+
|
|
543
1124
|
def _selected_text_items(self):
|
|
544
1125
|
return [it for it in self.scene.selectedItems() if isinstance(it, QGraphicsTextItem)]
|
|
545
1126
|
|
|
@@ -741,6 +1322,10 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
741
1322
|
bg = QGraphicsPixmapItem(QPixmap.fromImage(qimg))
|
|
742
1323
|
bg.setZValue(0)
|
|
743
1324
|
self.scene.addItem(bg)
|
|
1325
|
+
self._tech_init_catalog()
|
|
1326
|
+
if self.cb_tech.isChecked():
|
|
1327
|
+
self._tech_rebuild(live=True)
|
|
1328
|
+
|
|
744
1329
|
|
|
745
1330
|
def _load_from_file(self):
|
|
746
1331
|
fp, _ = QFileDialog.getOpenFileName(self, "Select Insert Image", "", "Images (*.png *.jpg *.jpeg *.tif *.tiff)")
|
|
@@ -806,6 +1391,10 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
806
1391
|
# text
|
|
807
1392
|
for ti in self._selected_text_items():
|
|
808
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
|
+
|
|
809
1398
|
|
|
810
1399
|
# -------- Commands ----------
|
|
811
1400
|
def _rotate_selected(self):
|
|
@@ -849,6 +1438,7 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
849
1438
|
}
|
|
850
1439
|
self.bounding_box_pen.setWidth(self.sl_thick.value())
|
|
851
1440
|
self.bounding_box_pen.setStyle(style_map[self.cmb_style.currentText()])
|
|
1441
|
+
self.bounding_box_pen.setCosmetic(True)
|
|
852
1442
|
self._refresh_all_boxes()
|
|
853
1443
|
|
|
854
1444
|
def _refresh_all_boxes(self):
|
|
@@ -968,6 +1558,8 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
968
1558
|
items.append(it)
|
|
969
1559
|
elif self.bounding_boxes_enabled and isinstance(it, QGraphicsRectItem):
|
|
970
1560
|
items.append(it)
|
|
1561
|
+
elif isinstance(it, TechCardItem):
|
|
1562
|
+
items.append(it)
|
|
971
1563
|
|
|
972
1564
|
# compute scene bbox
|
|
973
1565
|
bbox = QRectF()
|
|
@@ -1025,23 +1617,6 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
1025
1617
|
if had_focus:
|
|
1026
1618
|
it.setFocus()
|
|
1027
1619
|
|
|
1028
|
-
# temporarily hide non-items
|
|
1029
|
-
hidden = []
|
|
1030
|
-
for it in self.scene.items():
|
|
1031
|
-
if it not in items:
|
|
1032
|
-
it.setVisible(False); hidden.append(it)
|
|
1033
|
-
|
|
1034
|
-
# render
|
|
1035
|
-
out = QImage(w, h, QImage.Format.Format_ARGB32)
|
|
1036
|
-
out.fill(Qt.GlobalColor.transparent)
|
|
1037
|
-
p = QPainter(out)
|
|
1038
|
-
self.scene.render(p, target=QRectF(0, 0, w, h), source=QRectF(x, y, w, h))
|
|
1039
|
-
p.end()
|
|
1040
|
-
|
|
1041
|
-
# restore
|
|
1042
|
-
for it in hidden: it.setVisible(True)
|
|
1043
|
-
for r in hidden_boxes: r.setVisible(True)
|
|
1044
|
-
|
|
1045
1620
|
# drop alpha → RGB, write back to doc
|
|
1046
1621
|
arr = self._qimage_to_numpy(out)
|
|
1047
1622
|
if arr.shape[2] == 4:
|
|
@@ -1079,6 +1654,86 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
1079
1654
|
pass
|
|
1080
1655
|
self.bounding_boxes.clear()
|
|
1081
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
|
|
1082
1737
|
|
|
1083
1738
|
# ------------------ numpy/QImage bridges ------------------
|
|
1084
1739
|
def _numpy_to_qimage(self, a: np.ndarray) -> QImage:
|