setiastrosuitepro 1.6.2.post1__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/__init__.py +2 -0
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +945 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1346 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +624 -0
- setiastro/saspro/astrobin_exporter.py +1010 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1841 -0
- setiastro/saspro/autostretch.py +198 -0
- setiastro/saspro/backgroundneutral.py +602 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +491 -0
- setiastro/saspro/blink_comparator_pro.py +2926 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +213 -0
- setiastro/saspro/clahe.py +368 -0
- setiastro/saspro/comet_stacking.py +1442 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1400 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1589 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +973 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2562 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +673 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2664 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +748 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1349 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +663 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +637 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8810 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +389 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +488 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +756 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +417 -0
- setiastro/saspro/image_peeker_pro.py +1604 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +236 -0
- setiastro/saspro/isophote.py +1182 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1841 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +931 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3831 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +407 -0
- setiastro/saspro/multiscale_decomp.py +1293 -0
- setiastro/saspro/nbtorgb_stars.py +541 -0
- setiastro/saspro/numba_utils.py +3145 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1473 -0
- setiastro/saspro/ops/settings.py +637 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1071 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2445 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +501 -0
- setiastro/saspro/rgb_combination.py +208 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +73 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1472 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1102 -0
- setiastro/saspro/stacking_suite.py +18470 -0
- setiastro/saspro/star_alignment.py +7435 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +765 -0
- setiastro/saspro/star_stretch.py +507 -0
- setiastro/saspro/stat_stretch.py +538 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3328 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/all_source_strings.json +3654 -0
- setiastro/saspro/translations/ar_translations.py +3865 -0
- setiastro/saspro/translations/de_translations.py +3749 -0
- setiastro/saspro/translations/es_translations.py +3939 -0
- setiastro/saspro/translations/fr_translations.py +3858 -0
- setiastro/saspro/translations/hi_translations.py +3571 -0
- setiastro/saspro/translations/integrate_translations.py +270 -0
- setiastro/saspro/translations/it_translations.py +3678 -0
- setiastro/saspro/translations/ja_translations.py +3601 -0
- setiastro/saspro/translations/pt_translations.py +3869 -0
- setiastro/saspro/translations/ru_translations.py +2848 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +255 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +253 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +12520 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +12514 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +257 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +12520 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +257 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +257 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +237 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +257 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +10771 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/sw_translations.py +3671 -0
- setiastro/saspro/translations/uk_translations.py +3700 -0
- setiastro/saspro/translations/zh_translations.py +3675 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +1558 -0
- setiastro/saspro/wavescale_hdr.py +645 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +680 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +492 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/minigame/game.js +986 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/resource_monitor.py +237 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
- setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
- setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
#pro.fitsmodifier.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from astropy.io import fits
|
|
8
|
+
try:
|
|
9
|
+
from astropy.io.fits.verify import VerifyError
|
|
10
|
+
except Exception:
|
|
11
|
+
# Fallback for older Astropy – same pattern as in legacy.image_manager
|
|
12
|
+
class VerifyError(Exception):
|
|
13
|
+
pass
|
|
14
|
+
from PyQt6.QtCore import Qt
|
|
15
|
+
from PyQt6.QtWidgets import (
|
|
16
|
+
QAbstractItemView, QCheckBox, QComboBox, QDialog, QFileDialog, QHBoxLayout,
|
|
17
|
+
QLabel, QLineEdit, QMessageBox, QPushButton, QTreeWidget, QTreeWidgetItem,
|
|
18
|
+
QVBoxLayout
|
|
19
|
+
)
|
|
20
|
+
from PyQt6.QtCore import QSettings
|
|
21
|
+
from setiastro.saspro.legacy.image_manager import (
|
|
22
|
+
load_image as legacy_load_image,
|
|
23
|
+
save_image as legacy_save_image,
|
|
24
|
+
_drop_invalid_cards, # ← new
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
class FITSModifier(QDialog):
|
|
28
|
+
def __init__(self, file_path: Optional[str], header,
|
|
29
|
+
image_manager=None,
|
|
30
|
+
doc_manager=None, active_document=None, # <— rename param
|
|
31
|
+
parent=None):
|
|
32
|
+
super().__init__(parent)
|
|
33
|
+
self.setWindowTitle(self.tr("FITS Header Editor"))
|
|
34
|
+
self.resize(800, 600)
|
|
35
|
+
|
|
36
|
+
self._doc_manager = doc_manager
|
|
37
|
+
self._active_document = active_document
|
|
38
|
+
self.image_manager = None # stop using old ImageManager path
|
|
39
|
+
|
|
40
|
+
self.file_path = file_path if (file_path and os.path.isfile(file_path)) else None
|
|
41
|
+
|
|
42
|
+
self.hdul = None
|
|
43
|
+
self.current_hdu_index = 0
|
|
44
|
+
self._fallback_header = header
|
|
45
|
+
|
|
46
|
+
self._populating = False
|
|
47
|
+
self._dirty = False
|
|
48
|
+
|
|
49
|
+
# UI
|
|
50
|
+
top = QHBoxLayout()
|
|
51
|
+
self.path_label = QLabel(self.file_path or self.tr("(no file)"))
|
|
52
|
+
self.open_btn = QPushButton(self.tr("Open FITS…"))
|
|
53
|
+
self.reload_btn = QPushButton(self.tr("Reload"))
|
|
54
|
+
self.hdu_combo = QComboBox()
|
|
55
|
+
self.save_btn = QPushButton(self.tr("Save"))
|
|
56
|
+
self.saveas_btn = QPushButton(self.tr("Save a Copy As…"))
|
|
57
|
+
# self.apply_to_slot_btn = QPushButton("Apply to Slot Metadata") # optional
|
|
58
|
+
|
|
59
|
+
top.addWidget(QLabel(self.tr("File:")))
|
|
60
|
+
top.addWidget(self.path_label, 1)
|
|
61
|
+
top.addWidget(QLabel(self.tr("HDU:")))
|
|
62
|
+
top.addWidget(self.hdu_combo)
|
|
63
|
+
top.addWidget(self.open_btn)
|
|
64
|
+
top.addWidget(self.reload_btn)
|
|
65
|
+
#top.addWidget(self.save_btn)
|
|
66
|
+
top.addWidget(self.saveas_btn)
|
|
67
|
+
# top.addWidget(self.apply_to_slot_btn)
|
|
68
|
+
|
|
69
|
+
batch = QHBoxLayout()
|
|
70
|
+
self.batch_btn = QPushButton(self.tr("Batch Modify..."))
|
|
71
|
+
batch.addStretch()
|
|
72
|
+
batch.addWidget(self.batch_btn)
|
|
73
|
+
batch.addStretch()
|
|
74
|
+
|
|
75
|
+
self.tree = QTreeWidget()
|
|
76
|
+
self.tree.setColumnCount(3)
|
|
77
|
+
self.tree.setHeaderLabels([self.tr("Keyword"), self.tr("Value"), self.tr("Comment")])
|
|
78
|
+
self.tree.setAlternatingRowColors(True)
|
|
79
|
+
self.tree.setRootIsDecorated(False)
|
|
80
|
+
self.tree.setEditTriggers(QTreeWidget.EditTrigger.DoubleClicked | QTreeWidget.EditTrigger.SelectedClicked)
|
|
81
|
+
self.tree.setUniformRowHeights(True)
|
|
82
|
+
self.tree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
83
|
+
self.tree.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
84
|
+
self.tree.setStyleSheet("""
|
|
85
|
+
QTreeWidget::item:selected:active { background-color: #1E90FF; color: white; }
|
|
86
|
+
QTreeWidget::item:selected:!active { background-color: #5AA7FF; color: white; }
|
|
87
|
+
QTreeWidget::item:hover { background-color: rgba(30,144,255,0.18); }
|
|
88
|
+
QTreeWidget::item { padding: 2px 6px; }
|
|
89
|
+
""")
|
|
90
|
+
|
|
91
|
+
bottom = QHBoxLayout()
|
|
92
|
+
self.add_key_edit = QLineEdit(); self.add_key_edit.setPlaceholderText(self.tr("KEYWORD"))
|
|
93
|
+
self.add_val_edit = QLineEdit(); self.add_val_edit.setPlaceholderText(self.tr("Value"))
|
|
94
|
+
self.add_com_edit = QLineEdit(); self.add_com_edit.setPlaceholderText(self.tr("Comment (optional)"))
|
|
95
|
+
self.add_btn = QPushButton(self.tr("Add/Update"))
|
|
96
|
+
self.del_btn = QPushButton(self.tr("Delete Selected"))
|
|
97
|
+
self.all_hdus_chk = QCheckBox(self.tr("Apply add/update/delete to all HDUs"))
|
|
98
|
+
bottom.addWidget(self.add_key_edit)
|
|
99
|
+
bottom.addWidget(self.add_val_edit)
|
|
100
|
+
bottom.addWidget(self.add_com_edit)
|
|
101
|
+
bottom.addWidget(self.all_hdus_chk)
|
|
102
|
+
bottom.addWidget(self.add_btn)
|
|
103
|
+
bottom.addWidget(self.del_btn)
|
|
104
|
+
|
|
105
|
+
layout = QVBoxLayout(self)
|
|
106
|
+
layout.addLayout(top)
|
|
107
|
+
layout.addLayout(batch)
|
|
108
|
+
layout.addWidget(self.tree, 1)
|
|
109
|
+
layout.addLayout(bottom)
|
|
110
|
+
|
|
111
|
+
# Signals
|
|
112
|
+
self.open_btn.clicked.connect(self._choose_file)
|
|
113
|
+
self.reload_btn.clicked.connect(self._reload)
|
|
114
|
+
self.hdu_combo.currentIndexChanged.connect(self._on_hdu_changed)
|
|
115
|
+
self.save_btn.clicked.connect(self._save_in_place)
|
|
116
|
+
self.saveas_btn.clicked.connect(self._save_as_copy)
|
|
117
|
+
# self.apply_to_slot_btn.clicked.connect(self._apply_to_slot_metadata)
|
|
118
|
+
self.add_btn.clicked.connect(self._add_or_update_keyword)
|
|
119
|
+
self.del_btn.clicked.connect(self._delete_selected)
|
|
120
|
+
|
|
121
|
+
# Initial content
|
|
122
|
+
if self.file_path:
|
|
123
|
+
ok = self._load_file(self.file_path)
|
|
124
|
+
if not ok and header is not None:
|
|
125
|
+
self._init_from_header(header)
|
|
126
|
+
elif header is not None:
|
|
127
|
+
self._init_from_header(header)
|
|
128
|
+
else:
|
|
129
|
+
self._init_from_header(fits.Header())
|
|
130
|
+
|
|
131
|
+
self.tree.itemChanged.connect(self._on_item_changed)
|
|
132
|
+
self.tree.currentItemChanged.connect(self._on_row_selected)
|
|
133
|
+
self.batch_btn.clicked.connect(self._open_batch_modifier)
|
|
134
|
+
|
|
135
|
+
# ---- helpers ----
|
|
136
|
+
def _get_active_doc(self):
|
|
137
|
+
"""
|
|
138
|
+
Prefer the explicitly passed active_document; fall back to doc_manager.
|
|
139
|
+
This avoids accidentally selecting the last opened document when MDI focus
|
|
140
|
+
detection is flaky.
|
|
141
|
+
"""
|
|
142
|
+
# 1) if caller passed a doc, use it
|
|
143
|
+
if getattr(self, "_active_document", None) is not None:
|
|
144
|
+
return self._active_document
|
|
145
|
+
|
|
146
|
+
# 2) otherwise ask the doc manager
|
|
147
|
+
try:
|
|
148
|
+
if self._doc_manager and hasattr(self._doc_manager, "get_active_document"):
|
|
149
|
+
d = self._doc_manager.get_active_document()
|
|
150
|
+
if d is not None:
|
|
151
|
+
return d
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def _update_multi_hdu_ui(self):
|
|
158
|
+
n = len(self.hdul) if self.hdul else 0
|
|
159
|
+
self.all_hdus_chk.setVisible(n > 1)
|
|
160
|
+
|
|
161
|
+
def _on_row_selected(self, curr, prev):
|
|
162
|
+
if not curr:
|
|
163
|
+
return
|
|
164
|
+
self.add_key_edit.setText(curr.text(0))
|
|
165
|
+
self.add_val_edit.setText(curr.text(1))
|
|
166
|
+
self.add_com_edit.setText(curr.text(2))
|
|
167
|
+
|
|
168
|
+
def _selected_row_triplet(self) -> Tuple[str, str, str]:
|
|
169
|
+
it = self.tree.currentItem()
|
|
170
|
+
if not it:
|
|
171
|
+
return "", "", ""
|
|
172
|
+
return (it.text(0).strip(), it.text(1), it.text(2))
|
|
173
|
+
|
|
174
|
+
def _open_batch_modifier(self):
|
|
175
|
+
key, val, com = self._selected_row_triplet()
|
|
176
|
+
dlg = BatchFITSHeaderDialog(parent=self,
|
|
177
|
+
preset_keyword=key,
|
|
178
|
+
preset_value=val,
|
|
179
|
+
preset_comment=com)
|
|
180
|
+
dlg.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
181
|
+
dlg.show()
|
|
182
|
+
|
|
183
|
+
def _init_from_header(self, header):
|
|
184
|
+
phdu = fits.PrimaryHDU()
|
|
185
|
+
if isinstance(header, fits.Header):
|
|
186
|
+
clean = _drop_invalid_cards(header)
|
|
187
|
+
phdu.header = clean.copy()
|
|
188
|
+
elif isinstance(header, dict):
|
|
189
|
+
for k, v in header.items():
|
|
190
|
+
try:
|
|
191
|
+
phdu.header[k] = v
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
self.hdul = fits.HDUList([phdu])
|
|
195
|
+
self._refresh_hdu_combo()
|
|
196
|
+
self._populate_tree_from_header(phdu.header)
|
|
197
|
+
|
|
198
|
+
def _apply_to_slot_metadata(self):
|
|
199
|
+
try:
|
|
200
|
+
hdr = self.hdul[self.current_hdu_index].header.copy()
|
|
201
|
+
hdr = _drop_invalid_cards(hdr)
|
|
202
|
+
doc = self._get_active_doc()
|
|
203
|
+
if doc is not None and hasattr(doc, "metadata"):
|
|
204
|
+
doc.metadata["original_header"] = hdr
|
|
205
|
+
if hasattr(doc, "changed"):
|
|
206
|
+
doc.changed.emit()
|
|
207
|
+
except Exception as e:
|
|
208
|
+
print(f"[FITSModifier] _apply_to_slot_metadata error: {e}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _set_dirty(self, dirty=True):
|
|
212
|
+
self._dirty = dirty
|
|
213
|
+
self.setWindowTitle(self.tr("FITS Header Editor") + (" *" if dirty else ""))
|
|
214
|
+
|
|
215
|
+
def _sync_tree_to_header(self):
|
|
216
|
+
if not self.hdul:
|
|
217
|
+
return
|
|
218
|
+
hdr = self.hdul[self.current_hdu_index].header
|
|
219
|
+
self._collect_tree_into_header(hdr)
|
|
220
|
+
|
|
221
|
+
def _choose_file(self):
|
|
222
|
+
fn, _ = QFileDialog.getOpenFileName(self, self.tr("Open FITS"), self._last_dir(), self.tr("FITS files (*.fits *.fit *.fts *.fz)"))
|
|
223
|
+
if not fn:
|
|
224
|
+
return
|
|
225
|
+
self._load_file(fn)
|
|
226
|
+
|
|
227
|
+
def _load_file(self, path) -> bool:
|
|
228
|
+
try:
|
|
229
|
+
if self.hdul is not None:
|
|
230
|
+
self.hdul.close()
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
try:
|
|
234
|
+
self.hdul = fits.open(path, mode='update', memmap=False)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
QMessageBox.warning(self, self.tr("Invalid FITS"),
|
|
237
|
+
self.tr("This file does not appear to be a valid FITS:\n\n{0}\n\n{1}\n\n"
|
|
238
|
+
"Tip: Choose a FITS file via 'Open FITS…' or edit an in-memory header.").format(path, e))
|
|
239
|
+
self.hdul = None
|
|
240
|
+
self.file_path = None
|
|
241
|
+
self.path_label.setText(self.tr("(no file)"))
|
|
242
|
+
self.hdu_combo.clear()
|
|
243
|
+
self._update_multi_hdu_ui()
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
# Sanitize all HDU headers to drop invalid cards (e.g. bad TELESCOP)
|
|
247
|
+
for hdu in self.hdul:
|
|
248
|
+
try:
|
|
249
|
+
if isinstance(hdu.header, fits.Header):
|
|
250
|
+
hdu.header = _drop_invalid_cards(hdu.header)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
print(f"[FITSModifier] Header sanitize failed for HDU: {e}")
|
|
253
|
+
|
|
254
|
+
self.file_path = path
|
|
255
|
+
self.path_label.setText(path)
|
|
256
|
+
self._save_last_dir(os.path.dirname(path))
|
|
257
|
+
self._refresh_hdu_combo()
|
|
258
|
+
|
|
259
|
+
# Use the sanitized header for the current HDU
|
|
260
|
+
hdr = self.hdul[self.current_hdu_index].header
|
|
261
|
+
self._populate_tree_from_header(hdr)
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _reload(self):
|
|
266
|
+
if not self.hdul and not self.file_path:
|
|
267
|
+
return
|
|
268
|
+
if self.file_path:
|
|
269
|
+
self._load_file(self.file_path)
|
|
270
|
+
else:
|
|
271
|
+
self._populate_tree_from_header(self.hdul[0].header)
|
|
272
|
+
|
|
273
|
+
def _refresh_hdu_combo(self):
|
|
274
|
+
self.hdu_combo.blockSignals(True)
|
|
275
|
+
self.hdu_combo.clear()
|
|
276
|
+
for i, hdu in enumerate(self.hdul):
|
|
277
|
+
name = getattr(hdu, 'name', 'UNKNOWN')
|
|
278
|
+
self.hdu_combo.addItem(f"{i}: {name}")
|
|
279
|
+
self.hdu_combo.setCurrentIndex(0)
|
|
280
|
+
self.current_hdu_index = 0
|
|
281
|
+
self.hdu_combo.blockSignals(False)
|
|
282
|
+
self._update_multi_hdu_ui()
|
|
283
|
+
|
|
284
|
+
def _on_hdu_changed(self, idx):
|
|
285
|
+
self.current_hdu_index = int(idx)
|
|
286
|
+
hdr = self.hdul[self.current_hdu_index].header
|
|
287
|
+
hdr = _drop_invalid_cards(hdr)
|
|
288
|
+
self._populate_tree_from_header(hdr)
|
|
289
|
+
|
|
290
|
+
def _populate_tree_from_header(self, header: fits.Header):
|
|
291
|
+
self._populating = True
|
|
292
|
+
try:
|
|
293
|
+
self.tree.blockSignals(True)
|
|
294
|
+
self.tree.clear()
|
|
295
|
+
for card in header.cards:
|
|
296
|
+
key = card.keyword
|
|
297
|
+
if key in ("HISTORY", "COMMENT"):
|
|
298
|
+
val = ""
|
|
299
|
+
com = ""
|
|
300
|
+
else:
|
|
301
|
+
try:
|
|
302
|
+
val = self._val_to_str(card.value)
|
|
303
|
+
com = card.comment or ""
|
|
304
|
+
except VerifyError as e:
|
|
305
|
+
print(f"[FITSModifier] Skipping invalid card {key!r}: {e}")
|
|
306
|
+
continue # Don't add this card to the tree
|
|
307
|
+
it = QTreeWidgetItem([key, val, com])
|
|
308
|
+
it.setFlags(it.flags() | Qt.ItemFlag.ItemIsEditable)
|
|
309
|
+
self.tree.addTopLevelItem(it)
|
|
310
|
+
self.tree.resizeColumnToContents(0)
|
|
311
|
+
finally:
|
|
312
|
+
self.tree.blockSignals(False)
|
|
313
|
+
self._populating = False
|
|
314
|
+
self._set_dirty(False)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _collect_tree_into_header(self, header: fits.Header):
|
|
319
|
+
new_header = fits.Header()
|
|
320
|
+
for i in range(self.tree.topLevelItemCount()):
|
|
321
|
+
it = self.tree.topLevelItem(i)
|
|
322
|
+
key = (it.text(0) or "").strip()
|
|
323
|
+
val_txt = it.text(1)
|
|
324
|
+
com = it.text(2)
|
|
325
|
+
if not key:
|
|
326
|
+
continue
|
|
327
|
+
if key in ("HISTORY", "COMMENT"):
|
|
328
|
+
if key == "HISTORY" and val_txt:
|
|
329
|
+
new_header.add_history(val_txt)
|
|
330
|
+
elif key == "COMMENT" and val_txt:
|
|
331
|
+
new_header.add_comment(val_txt)
|
|
332
|
+
else:
|
|
333
|
+
if key == "COMMENT" and not val_txt and com:
|
|
334
|
+
new_header.add_comment(com)
|
|
335
|
+
continue
|
|
336
|
+
try:
|
|
337
|
+
val = self._parse_val(val_txt)
|
|
338
|
+
new_header[key] = (val, com if com else None)
|
|
339
|
+
except Exception:
|
|
340
|
+
new_header[key] = (val_txt, com if com else None)
|
|
341
|
+
|
|
342
|
+
header.clear()
|
|
343
|
+
header.update(new_header)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _on_item_changed(self, item, column):
|
|
349
|
+
if self._populating:
|
|
350
|
+
return
|
|
351
|
+
self._sync_tree_to_header()
|
|
352
|
+
self._set_dirty(True)
|
|
353
|
+
|
|
354
|
+
def _edited_primary_header(self) -> fits.Header:
|
|
355
|
+
"""Header we’ll write back out (primary HDU for single-image saves)."""
|
|
356
|
+
if self.hdul is not None and len(self.hdul) > 0:
|
|
357
|
+
return self.hdul[0].header.copy()
|
|
358
|
+
if isinstance(self._fallback_header, fits.Header):
|
|
359
|
+
return self._fallback_header.copy()
|
|
360
|
+
return fits.Header()
|
|
361
|
+
|
|
362
|
+
def _active_doc(self):
|
|
363
|
+
return self._doc_manager.get_active_document() if self._doc_manager else None
|
|
364
|
+
|
|
365
|
+
# 1) Change _write_to_path to optionally NOT touch the active doc
|
|
366
|
+
def _write_to_path(self, out_path: str, *, update_doc_metadata: bool = True) -> bool:
|
|
367
|
+
"""
|
|
368
|
+
Save the active document’s pixels with the edited header to `out_path`
|
|
369
|
+
using legacy.save_image.
|
|
370
|
+
|
|
371
|
+
If update_doc_metadata=False, this is a pure 'Save Copy' and we leave the
|
|
372
|
+
active document + dialog state untouched (no open, no rename).
|
|
373
|
+
"""
|
|
374
|
+
doc = self._active_doc()
|
|
375
|
+
if doc is None or doc.image is None:
|
|
376
|
+
QMessageBox.warning(self, self.tr("No Image"), self.tr("No active image/document to save."))
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
edited_hdr = self._edited_primary_header()
|
|
380
|
+
|
|
381
|
+
# pick format from the extension (fallback to doc’s current)
|
|
382
|
+
ext = os.path.splitext(out_path)[1].lower().lstrip(".")
|
|
383
|
+
if not ext:
|
|
384
|
+
ext = doc.metadata.get("original_format", "fits")
|
|
385
|
+
out_path = out_path + f".{ext}"
|
|
386
|
+
|
|
387
|
+
bit_depth = doc.metadata.get("bit_depth")
|
|
388
|
+
is_mono = doc.metadata.get("is_mono", (doc.image.ndim == 2))
|
|
389
|
+
image_meta = doc.metadata.get("image_meta")
|
|
390
|
+
file_meta = doc.metadata.get("file_meta")
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
legacy_save_image(
|
|
394
|
+
img_array=doc.image,
|
|
395
|
+
filename=out_path,
|
|
396
|
+
original_format=ext,
|
|
397
|
+
bit_depth=bit_depth,
|
|
398
|
+
original_header=edited_hdr,
|
|
399
|
+
is_mono=is_mono,
|
|
400
|
+
image_meta=image_meta,
|
|
401
|
+
file_meta=file_meta,
|
|
402
|
+
)
|
|
403
|
+
except Exception as e:
|
|
404
|
+
QMessageBox.critical(self, self.tr("Save Error"), self.tr("Could not save:\n{0}").format(e))
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
if update_doc_metadata:
|
|
408
|
+
# Only in true “Save” (not Save As copy)
|
|
409
|
+
self.file_path = out_path
|
|
410
|
+
self.path_label.setText(out_path)
|
|
411
|
+
doc.metadata["file_path"] = out_path
|
|
412
|
+
doc.metadata["original_format"] = ext
|
|
413
|
+
doc.metadata["original_header"] = edited_hdr
|
|
414
|
+
doc.changed.emit()
|
|
415
|
+
self._set_dirty(False)
|
|
416
|
+
|
|
417
|
+
return True
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _save_in_place(self):
|
|
421
|
+
# If the editor was opened on a file, use that.
|
|
422
|
+
# Else use the active document’s file_path if present,
|
|
423
|
+
# otherwise fall back to Save As…
|
|
424
|
+
target = self.file_path
|
|
425
|
+
if not target:
|
|
426
|
+
doc = self._get_active_doc()
|
|
427
|
+
target = (doc.metadata.get("file_path") if doc else None)
|
|
428
|
+
if not target:
|
|
429
|
+
return self._save_as_copy()
|
|
430
|
+
# apply any in-tree edits first
|
|
431
|
+
self._sync_tree_to_header()
|
|
432
|
+
self._write_to_path(target)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _save_as_copy(self):
|
|
436
|
+
self._sync_tree_to_header()
|
|
437
|
+
last = self._settings().value("fits_modifier/last_dir", "", type=str) or ""
|
|
438
|
+
path, _ = QFileDialog.getSaveFileName(
|
|
439
|
+
self, self.tr("Save Image As"),
|
|
440
|
+
last,
|
|
441
|
+
self.tr("FITS (*.fits *.fit);;TIFF (*.tif *.tiff);;PNG (*.png);;JPEG (*.jpg *.jpeg);;XISF (*.xisf)")
|
|
442
|
+
)
|
|
443
|
+
if not path:
|
|
444
|
+
return
|
|
445
|
+
ok = self._write_to_path(path, update_doc_metadata=False) # <<— key change
|
|
446
|
+
if ok:
|
|
447
|
+
self._save_last_dir(os.path.dirname(path))
|
|
448
|
+
# Optional: toast confirmation only
|
|
449
|
+
QMessageBox.information(self, self.tr("Saved Copy"), self.tr("Saved a copy to:\n{0}").format(path))
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _save_to_path(self, path: str) -> bool:
|
|
453
|
+
"""Write image+updated header to 'path' using legacy save if available; else fall back to astropy."""
|
|
454
|
+
try:
|
|
455
|
+
hdr = self.hdul[self.current_hdu_index].header.copy()
|
|
456
|
+
except Exception:
|
|
457
|
+
hdr = fits.Header()
|
|
458
|
+
|
|
459
|
+
img, bit_depth, is_mono, src = self._grab_image_for_save(self.file_path)
|
|
460
|
+
|
|
461
|
+
# Prefer legacy save_image if available and we have image data
|
|
462
|
+
if legacy_save_image and img is not None:
|
|
463
|
+
try:
|
|
464
|
+
if bit_depth is not None and is_mono is not None:
|
|
465
|
+
legacy_save_image(path, img, hdr, bit_depth=bit_depth, is_mono=is_mono)
|
|
466
|
+
else:
|
|
467
|
+
# permissive fallback if legacy signature is simpler
|
|
468
|
+
legacy_save_image(path, img, hdr)
|
|
469
|
+
return True
|
|
470
|
+
except TypeError:
|
|
471
|
+
# Try positional signature: (path, img, hdr, bit_depth, is_mono)
|
|
472
|
+
try:
|
|
473
|
+
legacy_save_image(path, img, hdr, bit_depth, is_mono)
|
|
474
|
+
return True
|
|
475
|
+
except Exception:
|
|
476
|
+
pass
|
|
477
|
+
except Exception:
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
# Fall back: write a FITS with astropy
|
|
481
|
+
try:
|
|
482
|
+
phdu = fits.PrimaryHDU(data=img, header=hdr)
|
|
483
|
+
hdul = fits.HDUList([phdu])
|
|
484
|
+
hdul.writeto(path, overwrite=True)
|
|
485
|
+
return True
|
|
486
|
+
except Exception:
|
|
487
|
+
return False
|
|
488
|
+
|
|
489
|
+
def _grab_image_for_save(self, fallback_path: Optional[str]):
|
|
490
|
+
doc = self._get_active_doc() # <— new
|
|
491
|
+
img = getattr(doc, "image", None) if doc is not None else None
|
|
492
|
+
bit_depth = (doc.metadata.get("bit_depth") if (doc and hasattr(doc, "metadata")) else None)
|
|
493
|
+
is_mono = (doc.metadata.get("is_mono") if (doc and hasattr(doc, "metadata")) else None)
|
|
494
|
+
src = ((doc.metadata.get("file_path") if (doc and hasattr(doc, "metadata")) else None)
|
|
495
|
+
or fallback_path)
|
|
496
|
+
|
|
497
|
+
if img is None and src and legacy_load_image:
|
|
498
|
+
try:
|
|
499
|
+
# common signature from your WIMI usage
|
|
500
|
+
img, orig_hdr, bit_depth, is_mono = legacy_load_image(src)
|
|
501
|
+
except TypeError:
|
|
502
|
+
# try alternate signatures
|
|
503
|
+
try:
|
|
504
|
+
img, orig_hdr = legacy_load_image(src)
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
# As a last resort, use data from opened HDUList if present
|
|
511
|
+
if img is None and self.hdul is not None:
|
|
512
|
+
try:
|
|
513
|
+
img = self.hdul[0].data
|
|
514
|
+
except Exception:
|
|
515
|
+
pass
|
|
516
|
+
|
|
517
|
+
return img, bit_depth, is_mono, src
|
|
518
|
+
|
|
519
|
+
def _add_or_update_keyword(self):
|
|
520
|
+
key = self.add_key_edit.text().strip()
|
|
521
|
+
if not key:
|
|
522
|
+
return
|
|
523
|
+
val = self.add_val_edit.text()
|
|
524
|
+
com = self.add_com_edit.text()
|
|
525
|
+
try:
|
|
526
|
+
parsed_val = self._parse_val(val)
|
|
527
|
+
except Exception:
|
|
528
|
+
parsed_val = val # leave as string if parsing fails
|
|
529
|
+
|
|
530
|
+
targets = range(len(self.hdul)) if self.all_hdus_chk.isChecked() and self.hdul else [self.current_hdu_index]
|
|
531
|
+
for idx in targets:
|
|
532
|
+
hdr = self.hdul[idx].header
|
|
533
|
+
hdr[key] = (parsed_val, com if com else None)
|
|
534
|
+
|
|
535
|
+
if not self.all_hdus_chk.isChecked():
|
|
536
|
+
self._populate_tree_from_header(self.hdul[self.current_hdu_index].header)
|
|
537
|
+
|
|
538
|
+
def _delete_selected(self):
|
|
539
|
+
items = self.tree.selectedItems()
|
|
540
|
+
if not items:
|
|
541
|
+
return
|
|
542
|
+
targets = range(len(self.hdul)) if self.all_hdus_chk.isChecked() and self.hdul else [self.current_hdu_index]
|
|
543
|
+
for it in items:
|
|
544
|
+
key = it.text(0).strip()
|
|
545
|
+
for idx in targets:
|
|
546
|
+
hdr = self.hdul[idx].header
|
|
547
|
+
if key in ("HISTORY", "COMMENT"):
|
|
548
|
+
rebuilt = fits.Header()
|
|
549
|
+
for c in hdr.cards:
|
|
550
|
+
if c.keyword == key:
|
|
551
|
+
if (key == "HISTORY" and c.value == it.text(1)) or \
|
|
552
|
+
(key == "COMMENT" and (c.value == it.text(1) or c.comment == it.text(2))):
|
|
553
|
+
continue
|
|
554
|
+
rebuilt.append(c)
|
|
555
|
+
hdr.clear(); hdr.update(rebuilt)
|
|
556
|
+
else:
|
|
557
|
+
if key in hdr:
|
|
558
|
+
del hdr[key]
|
|
559
|
+
self._populate_tree_from_header(self.hdul[self.current_hdu_index].header)
|
|
560
|
+
|
|
561
|
+
# ---- value parsing helpers ----
|
|
562
|
+
def _parse_val(self, s: str):
|
|
563
|
+
if s is None:
|
|
564
|
+
return ""
|
|
565
|
+
t = s.strip()
|
|
566
|
+
if t.lower() in ("true", "t"): return True
|
|
567
|
+
if t.lower() in ("false", "f"): return False
|
|
568
|
+
if t.lower() in ("nan",): return np.nan
|
|
569
|
+
try:
|
|
570
|
+
if t.startswith("0x"):
|
|
571
|
+
return int(t, 16)
|
|
572
|
+
return int(t)
|
|
573
|
+
except ValueError:
|
|
574
|
+
pass
|
|
575
|
+
try:
|
|
576
|
+
return float(t)
|
|
577
|
+
except ValueError:
|
|
578
|
+
pass
|
|
579
|
+
if (t.startswith('"') and t.endswith('"')) or (t.startswith("'") and t.endswith("'")):
|
|
580
|
+
return t[1:-1]
|
|
581
|
+
return t
|
|
582
|
+
|
|
583
|
+
def _val_to_str(self, v):
|
|
584
|
+
if isinstance(v, (float, np.floating)) and np.isnan(v):
|
|
585
|
+
return "nan"
|
|
586
|
+
return str(v)
|
|
587
|
+
|
|
588
|
+
def closeEvent(self, e):
|
|
589
|
+
try:
|
|
590
|
+
if self.hdul is not None:
|
|
591
|
+
self.hdul.close()
|
|
592
|
+
except Exception:
|
|
593
|
+
pass
|
|
594
|
+
super().closeEvent(e)
|
|
595
|
+
|
|
596
|
+
# ---- QSettings helpers ----
|
|
597
|
+
def _settings(self):
|
|
598
|
+
return self.parent().settings if (self.parent() and hasattr(self.parent(), "settings")) else QSettings()
|
|
599
|
+
def _last_dir(self):
|
|
600
|
+
return self._settings().value("fits_modifier/last_dir", "", type=str) or ""
|
|
601
|
+
def _save_last_dir(self, d):
|
|
602
|
+
self._settings().setValue("fits_modifier/last_dir", d)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class BatchFITSHeaderDialog(QDialog):
|
|
606
|
+
def __init__(self, parent=None, preset_keyword: str = "", preset_value: str = "", preset_comment: str = ""):
|
|
607
|
+
super().__init__(parent)
|
|
608
|
+
self.setWindowTitle(self.tr("Batch Modify FITS Headers"))
|
|
609
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
610
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
611
|
+
self.setModal(False)
|
|
612
|
+
self.resize(520, 220)
|
|
613
|
+
|
|
614
|
+
v = QVBoxLayout(self)
|
|
615
|
+
|
|
616
|
+
row1 = QHBoxLayout()
|
|
617
|
+
self.files_edit = QLineEdit(); self.files_edit.setPlaceholderText(self.tr("No files selected"))
|
|
618
|
+
self.pick_btn = QPushButton(self.tr("Choose FITS Files…"))
|
|
619
|
+
row1.addWidget(self.files_edit, 1); row1.addWidget(self.pick_btn)
|
|
620
|
+
|
|
621
|
+
row2 = QHBoxLayout()
|
|
622
|
+
self.key_edit = QLineEdit(); self.key_edit.setPlaceholderText(self.tr("KEYWORD"))
|
|
623
|
+
self.val_edit = QLineEdit(); self.val_edit.setPlaceholderText(self.tr("Value (leave blank for delete)"))
|
|
624
|
+
self.com_edit = QLineEdit(); self.com_edit.setPlaceholderText(self.tr("Comment (optional)"))
|
|
625
|
+
row2.addWidget(self.key_edit); row2.addWidget(self.val_edit); row2.addWidget(self.com_edit)
|
|
626
|
+
|
|
627
|
+
row3 = QHBoxLayout()
|
|
628
|
+
self.mode_combo = QComboBox()
|
|
629
|
+
self.mode_combo.addItem(self.tr("Add/Update"), "Add/Update")
|
|
630
|
+
self.mode_combo.addItem(self.tr("Delete"), "Delete")
|
|
631
|
+
self.all_hdus_chk = QCheckBox(self.tr("Apply to all HDUs"))
|
|
632
|
+
self.add_if_missing_chk = QCheckBox(self.tr("Add if missing (for Add/Update)"))
|
|
633
|
+
self.add_if_missing_chk.setChecked(True)
|
|
634
|
+
row3.addWidget(self.mode_combo)
|
|
635
|
+
row3.addWidget(self.all_hdus_chk)
|
|
636
|
+
row3.addWidget(self.add_if_missing_chk)
|
|
637
|
+
row3.addStretch()
|
|
638
|
+
|
|
639
|
+
row4 = QHBoxLayout()
|
|
640
|
+
self.run_btn = QPushButton(self.tr("Run"))
|
|
641
|
+
self.close_btn = QPushButton(self.tr("Close"))
|
|
642
|
+
row4.addStretch(); row4.addWidget(self.run_btn); row4.addWidget(self.close_btn)
|
|
643
|
+
|
|
644
|
+
v.addLayout(row1)
|
|
645
|
+
v.addLayout(row2)
|
|
646
|
+
v.addLayout(row3)
|
|
647
|
+
v.addLayout(row4)
|
|
648
|
+
|
|
649
|
+
if preset_keyword:
|
|
650
|
+
self.key_edit.setText(preset_keyword)
|
|
651
|
+
if preset_value:
|
|
652
|
+
self.val_edit.setText(preset_value)
|
|
653
|
+
if preset_comment:
|
|
654
|
+
self.com_edit.setText(preset_comment)
|
|
655
|
+
|
|
656
|
+
self.pick_btn.clicked.connect(self._pick_files)
|
|
657
|
+
self.run_btn.clicked.connect(self._run)
|
|
658
|
+
self.close_btn.clicked.connect(self.close)
|
|
659
|
+
|
|
660
|
+
self.files = []
|
|
661
|
+
|
|
662
|
+
def _settings(self):
|
|
663
|
+
return self.parent().settings if (self.parent() and hasattr(self.parent(), "settings")) else QSettings()
|
|
664
|
+
|
|
665
|
+
def _pick_files(self):
|
|
666
|
+
last = self._settings().value("fits_modifier/batch_dir", "", type=str) or ""
|
|
667
|
+
files, _ = QFileDialog.getOpenFileNames(self, self.tr("Select FITS files"), last, self.tr("FITS files (*.fits *.fit *.fts *.fz)"))
|
|
668
|
+
if not files:
|
|
669
|
+
return
|
|
670
|
+
self.files = files
|
|
671
|
+
self.files_edit.setText(self.tr("{0} files selected").format(len(files)))
|
|
672
|
+
self._settings().setValue("fits_modifier/batch_dir", os.path.dirname(files[0]))
|
|
673
|
+
|
|
674
|
+
def _parse_val(self, s: str):
|
|
675
|
+
t = (s or "").strip()
|
|
676
|
+
if t == "": return ""
|
|
677
|
+
if t.lower() in ("true", "t"): return True
|
|
678
|
+
if t.lower() in ("false", "f"): return False
|
|
679
|
+
if t.lower() in ("nan",): return np.nan
|
|
680
|
+
try:
|
|
681
|
+
if t.startswith("0x"):
|
|
682
|
+
return int(t, 16)
|
|
683
|
+
return int(t)
|
|
684
|
+
except ValueError:
|
|
685
|
+
pass
|
|
686
|
+
try:
|
|
687
|
+
return float(t)
|
|
688
|
+
except ValueError:
|
|
689
|
+
pass
|
|
690
|
+
if (t.startswith('"') and t.endswith('"')) or (t.startswith("'") and t.endswith("'")):
|
|
691
|
+
return t[1:-1]
|
|
692
|
+
return t
|
|
693
|
+
|
|
694
|
+
def _run(self):
|
|
695
|
+
if not self.files:
|
|
696
|
+
QMessageBox.warning(self, self.tr("No files"), self.tr("Please choose one or more FITS files."))
|
|
697
|
+
return
|
|
698
|
+
key = self.key_edit.text().strip()
|
|
699
|
+
if not key:
|
|
700
|
+
QMessageBox.warning(self, self.tr("Missing keyword"), self.tr("Please enter a FITS keyword."))
|
|
701
|
+
return
|
|
702
|
+
|
|
703
|
+
mode = self.mode_combo.currentData()
|
|
704
|
+
apply_all_hdus = self.all_hdus_chk.isChecked()
|
|
705
|
+
add_if_missing = self.add_if_missing_chk.isChecked()
|
|
706
|
+
com = self.com_edit.text().strip()
|
|
707
|
+
value_txt = self.val_edit.text()
|
|
708
|
+
|
|
709
|
+
n_ok, n_err = 0, 0
|
|
710
|
+
for fp in self.files:
|
|
711
|
+
try:
|
|
712
|
+
with fits.open(fp, mode='update', memmap=False) as hdul:
|
|
713
|
+
targets = range(len(hdul)) if apply_all_hdus else [0]
|
|
714
|
+
if mode == "Delete":
|
|
715
|
+
for i in targets:
|
|
716
|
+
hdr = hdul[i].header
|
|
717
|
+
if key in ("HISTORY", "COMMENT"):
|
|
718
|
+
rebuilt = fits.Header()
|
|
719
|
+
for c in hdr.cards:
|
|
720
|
+
if c.keyword == key:
|
|
721
|
+
if value_txt and str(c.value) == value_txt:
|
|
722
|
+
continue
|
|
723
|
+
if (not value_txt) and (not com):
|
|
724
|
+
continue
|
|
725
|
+
if com and (c.comment == com):
|
|
726
|
+
continue
|
|
727
|
+
rebuilt.append(c)
|
|
728
|
+
hdr.clear(); hdr.update(rebuilt)
|
|
729
|
+
else:
|
|
730
|
+
if key in hdr:
|
|
731
|
+
del hdr[key]
|
|
732
|
+
hdul.flush()
|
|
733
|
+
else:
|
|
734
|
+
try:
|
|
735
|
+
val = self._parse_val(value_txt)
|
|
736
|
+
except Exception:
|
|
737
|
+
val = value_txt
|
|
738
|
+
for i in targets:
|
|
739
|
+
hdr = hdul[i].header
|
|
740
|
+
if key in hdr or add_if_missing:
|
|
741
|
+
hdr[key] = (val, com if com else None)
|
|
742
|
+
hdul.flush()
|
|
743
|
+
n_ok += 1
|
|
744
|
+
except Exception as e:
|
|
745
|
+
print(f"[Batch FITS] Error on {fp}: {e}")
|
|
746
|
+
n_err += 1
|
|
747
|
+
|
|
748
|
+
QMessageBox.information(self, self.tr("Batch Complete"), self.tr("Updated {0} file(s); {1} error(s).").format(n_ok, n_err))
|