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.

Files changed (367) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/data/SASP_data.fits +0 -0
  3. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  4. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  5. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  6. setiastro/data/catalogs/cali2.csv +63 -0
  7. setiastro/data/catalogs/cali2color.csv +65 -0
  8. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  9. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  10. setiastro/data/catalogs/detected_stars.csv +24784 -0
  11. setiastro/data/catalogs/fits_header_data.csv +46 -0
  12. setiastro/data/catalogs/test.csv +8 -0
  13. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  14. setiastro/images/Astro_Spikes.png +0 -0
  15. setiastro/images/Background_startup.jpg +0 -0
  16. setiastro/images/HRDiagram.png +0 -0
  17. setiastro/images/LExtract.png +0 -0
  18. setiastro/images/LInsert.png +0 -0
  19. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  20. setiastro/images/RGB080604.png +0 -0
  21. setiastro/images/abeicon.png +0 -0
  22. setiastro/images/aberration.png +0 -0
  23. setiastro/images/andromedatry.png +0 -0
  24. setiastro/images/andromedatry_satellited.png +0 -0
  25. setiastro/images/annotated.png +0 -0
  26. setiastro/images/aperture.png +0 -0
  27. setiastro/images/astrosuite.ico +0 -0
  28. setiastro/images/astrosuite.png +0 -0
  29. setiastro/images/astrosuitepro.icns +0 -0
  30. setiastro/images/astrosuitepro.ico +0 -0
  31. setiastro/images/astrosuitepro.png +0 -0
  32. setiastro/images/background.png +0 -0
  33. setiastro/images/background2.png +0 -0
  34. setiastro/images/benchmark.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  37. setiastro/images/blaster.png +0 -0
  38. setiastro/images/blink.png +0 -0
  39. setiastro/images/clahe.png +0 -0
  40. setiastro/images/collage.png +0 -0
  41. setiastro/images/colorwheel.png +0 -0
  42. setiastro/images/contsub.png +0 -0
  43. setiastro/images/convo.png +0 -0
  44. setiastro/images/copyslot.png +0 -0
  45. setiastro/images/cosmic.png +0 -0
  46. setiastro/images/cosmicsat.png +0 -0
  47. setiastro/images/crop1.png +0 -0
  48. setiastro/images/cropicon.png +0 -0
  49. setiastro/images/curves.png +0 -0
  50. setiastro/images/cvs.png +0 -0
  51. setiastro/images/debayer.png +0 -0
  52. setiastro/images/denoise_cnn_custom.png +0 -0
  53. setiastro/images/denoise_cnn_graph.png +0 -0
  54. setiastro/images/disk.png +0 -0
  55. setiastro/images/dse.png +0 -0
  56. setiastro/images/exoicon.png +0 -0
  57. setiastro/images/eye.png +0 -0
  58. setiastro/images/fliphorizontal.png +0 -0
  59. setiastro/images/flipvertical.png +0 -0
  60. setiastro/images/font.png +0 -0
  61. setiastro/images/freqsep.png +0 -0
  62. setiastro/images/functionbundle.png +0 -0
  63. setiastro/images/graxpert.png +0 -0
  64. setiastro/images/green.png +0 -0
  65. setiastro/images/gridicon.png +0 -0
  66. setiastro/images/halo.png +0 -0
  67. setiastro/images/hdr.png +0 -0
  68. setiastro/images/histogram.png +0 -0
  69. setiastro/images/hubble.png +0 -0
  70. setiastro/images/imagecombine.png +0 -0
  71. setiastro/images/invert.png +0 -0
  72. setiastro/images/isophote.png +0 -0
  73. setiastro/images/isophote_demo_figure.png +0 -0
  74. setiastro/images/isophote_demo_image.png +0 -0
  75. setiastro/images/isophote_demo_model.png +0 -0
  76. setiastro/images/isophote_demo_residual.png +0 -0
  77. setiastro/images/jwstpupil.png +0 -0
  78. setiastro/images/linearfit.png +0 -0
  79. setiastro/images/livestacking.png +0 -0
  80. setiastro/images/mask.png +0 -0
  81. setiastro/images/maskapply.png +0 -0
  82. setiastro/images/maskcreate.png +0 -0
  83. setiastro/images/maskremove.png +0 -0
  84. setiastro/images/morpho.png +0 -0
  85. setiastro/images/mosaic.png +0 -0
  86. setiastro/images/multiscale_decomp.png +0 -0
  87. setiastro/images/nbtorgb.png +0 -0
  88. setiastro/images/neutral.png +0 -0
  89. setiastro/images/nuke.png +0 -0
  90. setiastro/images/openfile.png +0 -0
  91. setiastro/images/pedestal.png +0 -0
  92. setiastro/images/pen.png +0 -0
  93. setiastro/images/pixelmath.png +0 -0
  94. setiastro/images/platesolve.png +0 -0
  95. setiastro/images/ppp.png +0 -0
  96. setiastro/images/pro.png +0 -0
  97. setiastro/images/project.png +0 -0
  98. setiastro/images/psf.png +0 -0
  99. setiastro/images/redo.png +0 -0
  100. setiastro/images/redoicon.png +0 -0
  101. setiastro/images/rescale.png +0 -0
  102. setiastro/images/rgbalign.png +0 -0
  103. setiastro/images/rgbcombo.png +0 -0
  104. setiastro/images/rgbextract.png +0 -0
  105. setiastro/images/rotate180.png +0 -0
  106. setiastro/images/rotateclockwise.png +0 -0
  107. setiastro/images/rotatecounterclockwise.png +0 -0
  108. setiastro/images/satellite.png +0 -0
  109. setiastro/images/script.png +0 -0
  110. setiastro/images/selectivecolor.png +0 -0
  111. setiastro/images/simbad.png +0 -0
  112. setiastro/images/slot0.png +0 -0
  113. setiastro/images/slot1.png +0 -0
  114. setiastro/images/slot2.png +0 -0
  115. setiastro/images/slot3.png +0 -0
  116. setiastro/images/slot4.png +0 -0
  117. setiastro/images/slot5.png +0 -0
  118. setiastro/images/slot6.png +0 -0
  119. setiastro/images/slot7.png +0 -0
  120. setiastro/images/slot8.png +0 -0
  121. setiastro/images/slot9.png +0 -0
  122. setiastro/images/spcc.png +0 -0
  123. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  124. setiastro/images/spinner.gif +0 -0
  125. setiastro/images/stacking.png +0 -0
  126. setiastro/images/staradd.png +0 -0
  127. setiastro/images/staralign.png +0 -0
  128. setiastro/images/starnet.png +0 -0
  129. setiastro/images/starregistration.png +0 -0
  130. setiastro/images/starspike.png +0 -0
  131. setiastro/images/starstretch.png +0 -0
  132. setiastro/images/statstretch.png +0 -0
  133. setiastro/images/supernova.png +0 -0
  134. setiastro/images/uhs.png +0 -0
  135. setiastro/images/undoicon.png +0 -0
  136. setiastro/images/upscale.png +0 -0
  137. setiastro/images/viewbundle.png +0 -0
  138. setiastro/images/whitebalance.png +0 -0
  139. setiastro/images/wimi_icon_256x256.png +0 -0
  140. setiastro/images/wimilogo.png +0 -0
  141. setiastro/images/wims.png +0 -0
  142. setiastro/images/wrench_icon.png +0 -0
  143. setiastro/images/xisfliberator.png +0 -0
  144. setiastro/qml/ResourceMonitor.qml +126 -0
  145. setiastro/saspro/__init__.py +20 -0
  146. setiastro/saspro/__main__.py +945 -0
  147. setiastro/saspro/_generated/__init__.py +7 -0
  148. setiastro/saspro/_generated/build_info.py +3 -0
  149. setiastro/saspro/abe.py +1346 -0
  150. setiastro/saspro/abe_preset.py +196 -0
  151. setiastro/saspro/aberration_ai.py +694 -0
  152. setiastro/saspro/aberration_ai_preset.py +224 -0
  153. setiastro/saspro/accel_installer.py +218 -0
  154. setiastro/saspro/accel_workers.py +30 -0
  155. setiastro/saspro/add_stars.py +624 -0
  156. setiastro/saspro/astrobin_exporter.py +1010 -0
  157. setiastro/saspro/astrospike.py +153 -0
  158. setiastro/saspro/astrospike_python.py +1841 -0
  159. setiastro/saspro/autostretch.py +198 -0
  160. setiastro/saspro/backgroundneutral.py +602 -0
  161. setiastro/saspro/batch_convert.py +328 -0
  162. setiastro/saspro/batch_renamer.py +522 -0
  163. setiastro/saspro/blemish_blaster.py +491 -0
  164. setiastro/saspro/blink_comparator_pro.py +2926 -0
  165. setiastro/saspro/bundles.py +61 -0
  166. setiastro/saspro/bundles_dock.py +114 -0
  167. setiastro/saspro/cheat_sheet.py +213 -0
  168. setiastro/saspro/clahe.py +368 -0
  169. setiastro/saspro/comet_stacking.py +1442 -0
  170. setiastro/saspro/common_tr.py +107 -0
  171. setiastro/saspro/config.py +38 -0
  172. setiastro/saspro/config_bootstrap.py +40 -0
  173. setiastro/saspro/config_manager.py +316 -0
  174. setiastro/saspro/continuum_subtract.py +1617 -0
  175. setiastro/saspro/convo.py +1400 -0
  176. setiastro/saspro/convo_preset.py +414 -0
  177. setiastro/saspro/copyastro.py +190 -0
  178. setiastro/saspro/cosmicclarity.py +1589 -0
  179. setiastro/saspro/cosmicclarity_preset.py +407 -0
  180. setiastro/saspro/crop_dialog_pro.py +973 -0
  181. setiastro/saspro/crop_preset.py +189 -0
  182. setiastro/saspro/curve_editor_pro.py +2562 -0
  183. setiastro/saspro/curves_preset.py +375 -0
  184. setiastro/saspro/debayer.py +673 -0
  185. setiastro/saspro/debug_utils.py +29 -0
  186. setiastro/saspro/dnd_mime.py +35 -0
  187. setiastro/saspro/doc_manager.py +2664 -0
  188. setiastro/saspro/exoplanet_detector.py +2166 -0
  189. setiastro/saspro/file_utils.py +284 -0
  190. setiastro/saspro/fitsmodifier.py +748 -0
  191. setiastro/saspro/fix_bom.py +32 -0
  192. setiastro/saspro/free_torch_memory.py +48 -0
  193. setiastro/saspro/frequency_separation.py +1349 -0
  194. setiastro/saspro/function_bundle.py +1596 -0
  195. setiastro/saspro/generate_translations.py +3092 -0
  196. setiastro/saspro/ghs_dialog_pro.py +663 -0
  197. setiastro/saspro/ghs_preset.py +284 -0
  198. setiastro/saspro/graxpert.py +637 -0
  199. setiastro/saspro/graxpert_preset.py +287 -0
  200. setiastro/saspro/gui/__init__.py +0 -0
  201. setiastro/saspro/gui/main_window.py +8810 -0
  202. setiastro/saspro/gui/mixins/__init__.py +33 -0
  203. setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
  204. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  205. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  206. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  207. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  208. setiastro/saspro/gui/mixins/menu_mixin.py +389 -0
  209. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  210. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  211. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  212. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  213. setiastro/saspro/gui/statistics_dialog.py +47 -0
  214. setiastro/saspro/halobgon.py +488 -0
  215. setiastro/saspro/header_viewer.py +448 -0
  216. setiastro/saspro/headless_utils.py +88 -0
  217. setiastro/saspro/histogram.py +756 -0
  218. setiastro/saspro/history_explorer.py +941 -0
  219. setiastro/saspro/i18n.py +168 -0
  220. setiastro/saspro/image_combine.py +417 -0
  221. setiastro/saspro/image_peeker_pro.py +1604 -0
  222. setiastro/saspro/imageops/__init__.py +37 -0
  223. setiastro/saspro/imageops/mdi_snap.py +292 -0
  224. setiastro/saspro/imageops/scnr.py +36 -0
  225. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  226. setiastro/saspro/imageops/stretch.py +236 -0
  227. setiastro/saspro/isophote.py +1182 -0
  228. setiastro/saspro/layers.py +208 -0
  229. setiastro/saspro/layers_dock.py +714 -0
  230. setiastro/saspro/lazy_imports.py +193 -0
  231. setiastro/saspro/legacy/__init__.py +2 -0
  232. setiastro/saspro/legacy/image_manager.py +2226 -0
  233. setiastro/saspro/legacy/numba_utils.py +3676 -0
  234. setiastro/saspro/legacy/xisf.py +1071 -0
  235. setiastro/saspro/linear_fit.py +537 -0
  236. setiastro/saspro/live_stacking.py +1841 -0
  237. setiastro/saspro/log_bus.py +5 -0
  238. setiastro/saspro/logging_config.py +460 -0
  239. setiastro/saspro/luminancerecombine.py +309 -0
  240. setiastro/saspro/main_helpers.py +201 -0
  241. setiastro/saspro/mask_creation.py +931 -0
  242. setiastro/saspro/masks_core.py +56 -0
  243. setiastro/saspro/mdi_widgets.py +353 -0
  244. setiastro/saspro/memory_utils.py +666 -0
  245. setiastro/saspro/metadata_patcher.py +75 -0
  246. setiastro/saspro/mfdeconv.py +3831 -0
  247. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  248. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  249. setiastro/saspro/mfdeconvsport.py +2382 -0
  250. setiastro/saspro/minorbodycatalog.py +567 -0
  251. setiastro/saspro/morphology.py +407 -0
  252. setiastro/saspro/multiscale_decomp.py +1293 -0
  253. setiastro/saspro/nbtorgb_stars.py +541 -0
  254. setiastro/saspro/numba_utils.py +3145 -0
  255. setiastro/saspro/numba_warmup.py +141 -0
  256. setiastro/saspro/ops/__init__.py +9 -0
  257. setiastro/saspro/ops/command_help_dialog.py +623 -0
  258. setiastro/saspro/ops/command_runner.py +217 -0
  259. setiastro/saspro/ops/commands.py +1594 -0
  260. setiastro/saspro/ops/script_editor.py +1102 -0
  261. setiastro/saspro/ops/scripts.py +1473 -0
  262. setiastro/saspro/ops/settings.py +637 -0
  263. setiastro/saspro/parallel_utils.py +554 -0
  264. setiastro/saspro/pedestal.py +121 -0
  265. setiastro/saspro/perfect_palette_picker.py +1071 -0
  266. setiastro/saspro/pipeline.py +110 -0
  267. setiastro/saspro/pixelmath.py +1604 -0
  268. setiastro/saspro/plate_solver.py +2445 -0
  269. setiastro/saspro/project_io.py +797 -0
  270. setiastro/saspro/psf_utils.py +136 -0
  271. setiastro/saspro/psf_viewer.py +549 -0
  272. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  273. setiastro/saspro/remove_green.py +331 -0
  274. setiastro/saspro/remove_stars.py +1599 -0
  275. setiastro/saspro/remove_stars_preset.py +404 -0
  276. setiastro/saspro/resources.py +501 -0
  277. setiastro/saspro/rgb_combination.py +208 -0
  278. setiastro/saspro/rgb_extract.py +19 -0
  279. setiastro/saspro/rgbalign.py +723 -0
  280. setiastro/saspro/runtime_imports.py +7 -0
  281. setiastro/saspro/runtime_torch.py +754 -0
  282. setiastro/saspro/save_options.py +73 -0
  283. setiastro/saspro/selective_color.py +1552 -0
  284. setiastro/saspro/sfcc.py +1472 -0
  285. setiastro/saspro/shortcuts.py +3043 -0
  286. setiastro/saspro/signature_insert.py +1102 -0
  287. setiastro/saspro/stacking_suite.py +18470 -0
  288. setiastro/saspro/star_alignment.py +7435 -0
  289. setiastro/saspro/star_alignment_preset.py +329 -0
  290. setiastro/saspro/star_metrics.py +49 -0
  291. setiastro/saspro/star_spikes.py +765 -0
  292. setiastro/saspro/star_stretch.py +507 -0
  293. setiastro/saspro/stat_stretch.py +538 -0
  294. setiastro/saspro/status_log_dock.py +78 -0
  295. setiastro/saspro/subwindow.py +3328 -0
  296. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  297. setiastro/saspro/swap_manager.py +99 -0
  298. setiastro/saspro/torch_backend.py +89 -0
  299. setiastro/saspro/torch_rejection.py +434 -0
  300. setiastro/saspro/translations/all_source_strings.json +3654 -0
  301. setiastro/saspro/translations/ar_translations.py +3865 -0
  302. setiastro/saspro/translations/de_translations.py +3749 -0
  303. setiastro/saspro/translations/es_translations.py +3939 -0
  304. setiastro/saspro/translations/fr_translations.py +3858 -0
  305. setiastro/saspro/translations/hi_translations.py +3571 -0
  306. setiastro/saspro/translations/integrate_translations.py +270 -0
  307. setiastro/saspro/translations/it_translations.py +3678 -0
  308. setiastro/saspro/translations/ja_translations.py +3601 -0
  309. setiastro/saspro/translations/pt_translations.py +3869 -0
  310. setiastro/saspro/translations/ru_translations.py +2848 -0
  311. setiastro/saspro/translations/saspro_ar.qm +0 -0
  312. setiastro/saspro/translations/saspro_ar.ts +255 -0
  313. setiastro/saspro/translations/saspro_de.qm +0 -0
  314. setiastro/saspro/translations/saspro_de.ts +253 -0
  315. setiastro/saspro/translations/saspro_es.qm +0 -0
  316. setiastro/saspro/translations/saspro_es.ts +12520 -0
  317. setiastro/saspro/translations/saspro_fr.qm +0 -0
  318. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  319. setiastro/saspro/translations/saspro_hi.qm +0 -0
  320. setiastro/saspro/translations/saspro_hi.ts +257 -0
  321. setiastro/saspro/translations/saspro_it.qm +0 -0
  322. setiastro/saspro/translations/saspro_it.ts +12520 -0
  323. setiastro/saspro/translations/saspro_ja.qm +0 -0
  324. setiastro/saspro/translations/saspro_ja.ts +257 -0
  325. setiastro/saspro/translations/saspro_pt.qm +0 -0
  326. setiastro/saspro/translations/saspro_pt.ts +257 -0
  327. setiastro/saspro/translations/saspro_ru.qm +0 -0
  328. setiastro/saspro/translations/saspro_ru.ts +237 -0
  329. setiastro/saspro/translations/saspro_sw.qm +0 -0
  330. setiastro/saspro/translations/saspro_sw.ts +257 -0
  331. setiastro/saspro/translations/saspro_uk.qm +0 -0
  332. setiastro/saspro/translations/saspro_uk.ts +10771 -0
  333. setiastro/saspro/translations/saspro_zh.qm +0 -0
  334. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  335. setiastro/saspro/translations/sw_translations.py +3671 -0
  336. setiastro/saspro/translations/uk_translations.py +3700 -0
  337. setiastro/saspro/translations/zh_translations.py +3675 -0
  338. setiastro/saspro/versioning.py +77 -0
  339. setiastro/saspro/view_bundle.py +1558 -0
  340. setiastro/saspro/wavescale_hdr.py +645 -0
  341. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  342. setiastro/saspro/wavescalede.py +680 -0
  343. setiastro/saspro/wavescalede_preset.py +230 -0
  344. setiastro/saspro/wcs_update.py +374 -0
  345. setiastro/saspro/whitebalance.py +492 -0
  346. setiastro/saspro/widgets/__init__.py +48 -0
  347. setiastro/saspro/widgets/common_utilities.py +306 -0
  348. setiastro/saspro/widgets/graphics_views.py +122 -0
  349. setiastro/saspro/widgets/image_utils.py +518 -0
  350. setiastro/saspro/widgets/minigame/game.js +986 -0
  351. setiastro/saspro/widgets/minigame/index.html +53 -0
  352. setiastro/saspro/widgets/minigame/style.css +241 -0
  353. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  354. setiastro/saspro/widgets/resource_monitor.py +237 -0
  355. setiastro/saspro/widgets/spinboxes.py +275 -0
  356. setiastro/saspro/widgets/themed_buttons.py +13 -0
  357. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  358. setiastro/saspro/wimi.py +7996 -0
  359. setiastro/saspro/wims.py +578 -0
  360. setiastro/saspro/window_shelf.py +185 -0
  361. setiastro/saspro/xisf.py +1123 -0
  362. setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
  363. setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
  364. setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
  365. setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
  366. setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
  367. setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,448 @@
1
+ # pro/header_viewer.py
2
+ from __future__ import annotations
3
+ import os
4
+ import csv
5
+ from typing import Optional, Dict, Any
6
+
7
+ from PyQt6.QtWidgets import (
8
+ QDockWidget, QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
9
+ QPushButton, QFileDialog, QMessageBox
10
+ )
11
+ from PyQt6.QtCore import Qt
12
+
13
+ from astropy.io import fits
14
+ try:
15
+ from astropy.io.fits.verify import VerifyError
16
+ except Exception:
17
+ class VerifyError(Exception):
18
+ pass
19
+
20
+ from setiastro.saspro.xisf import XISF
21
+
22
+ # we’ll reuse your loader helper for FITS headers
23
+ from setiastro.saspro.legacy.image_manager import get_valid_header, _drop_invalid_cards
24
+ from setiastro.saspro.doc_manager import ImageDocument
25
+
26
+
27
+ class HeaderViewerDock(QDockWidget):
28
+ """
29
+ Dock that shows metadata for the currently active ImageDocument.
30
+ Supports FITS headers and XISF file & image metadata.
31
+ """
32
+ def __init__(self, parent=None):
33
+ super().__init__(self.tr("Header Viewer"), parent)
34
+ self._doc: Optional[ImageDocument] = None
35
+ self._doc_conn = False
36
+
37
+ self._tree = QTreeWidget()
38
+ self._tree.setHeaderLabels([self.tr("Key"), self.tr("Value")])
39
+ self._tree.setColumnWidth(0, 220)
40
+
41
+ self._save_btn = QPushButton(self.tr("Save Metadata…"))
42
+ self._save_btn.clicked.connect(self._save_metadata)
43
+ self._dm = None # <-- NEW: DocManager to query "active"
44
+ self._follow_hover = False # <-- optional toggle if you ever want hover-follow
45
+ w = QWidget(self)
46
+ lay = QVBoxLayout(w)
47
+ lay.setContentsMargins(6, 6, 6, 6)
48
+ lay.addWidget(self._tree)
49
+ lay.addWidget(self._save_btn)
50
+ self.setWidget(w)
51
+
52
+ def _same_base(self, a, b) -> bool:
53
+ return self._unwrap_base_doc(a) is self._unwrap_base_doc(b)
54
+
55
+ def attach_doc_manager(self, dm):
56
+ self._dm = dm
57
+ try:
58
+ # When docs are added/removed, re-evaluate the focused base
59
+ dm.documentAdded.connect(lambda _doc: self._maybe_refresh_for_active())
60
+ dm.documentRemoved.connect(lambda _doc: self._maybe_refresh_for_active())
61
+
62
+ # DO NOT use imageRegionUpdated to retarget; it can fire from hover-driven previews.
63
+ # If you want to repaint the same doc on region changes, _on_doc_changed handles that.
64
+
65
+ mdi = getattr(dm, "_mdi", None)
66
+ if mdi and hasattr(mdi, "subWindowActivated"):
67
+ mdi.subWindowActivated.connect(lambda _sw: self._maybe_refresh_for_active())
68
+
69
+ # NEW: snap to truly active base (sticky, click-activated only)
70
+ if hasattr(dm, "activeBaseChanged"):
71
+ dm.activeBaseChanged.connect(lambda _doc: self._maybe_refresh_for_active())
72
+ except Exception:
73
+ pass
74
+
75
+ self._maybe_refresh_for_active()
76
+
77
+
78
+ def set_follow_hover(self, enabled: bool):
79
+ self._follow_hover = bool(enabled)
80
+
81
+ # Prefer base (true) doc over transient wrappers/proxies
82
+ def _unwrap_base_doc(self, d):
83
+ if d is None:
84
+ return None
85
+ # ROI preview wrapper → parent
86
+ p = getattr(d, "_parent_doc", None)
87
+ if isinstance(p, ImageDocument):
88
+ return p
89
+ # LiveViewDocument proxy → base
90
+ b = getattr(d, "_base", None)
91
+ if isinstance(b, ImageDocument):
92
+ return b
93
+ return d
94
+
95
+ def _active_base_doc(self):
96
+ if not self._dm:
97
+ return None
98
+ # Prefer DocManager’s sticky focused base if available
99
+ if hasattr(self._dm, "get_focused_base_document"):
100
+ try:
101
+ return self._dm.get_focused_base_document()
102
+ except Exception:
103
+ pass
104
+ # Fallback: unwrap whatever get_active_document returns
105
+ try:
106
+ cur = self._dm.get_active_document()
107
+ return self._unwrap_base_doc(cur)
108
+ except Exception:
109
+ return None
110
+
111
+
112
+ def _maybe_refresh_for_active(self):
113
+ """Rebuild only if our bound document == current active base document."""
114
+ active_base = self._active_base_doc()
115
+ if active_base is None:
116
+ return
117
+ # If we already show the same base doc, just ignore
118
+ if self._unwrap_base_doc(self._doc) is active_base:
119
+ return
120
+ # Else bind to the active base doc
121
+ self.set_document(active_base)
122
+
123
+ # ---- public API ----
124
+ def set_document(self, doc: Optional[ImageDocument]):
125
+ """
126
+ Hard-lock behavior:
127
+ - If attached to a DocManager AND hover-follow is OFF, ignore the caller's 'doc'
128
+ and always bind to the DocManager's *active base* doc.
129
+ - Otherwise, behave like a normal setter.
130
+ """
131
+ if self._dm and not self._follow_hover:
132
+ # Caller cannot hijack focus: resolve from DM every time
133
+ doc = self._active_base_doc()
134
+
135
+ # Always resolve to base (true) document for internal storage
136
+ base_doc = self._unwrap_base_doc(doc)
137
+
138
+ # No-op if unchanged
139
+ if self._same_base(self._doc, base_doc):
140
+ return
141
+
142
+ # Disconnect old
143
+ if self._doc and hasattr(self._doc, "changed"):
144
+ try:
145
+ self._doc.changed.disconnect(self._on_doc_changed)
146
+ except Exception:
147
+ pass
148
+
149
+ self._doc = base_doc
150
+
151
+ # Listen for internal changes on the *bound* doc
152
+ if self._doc and hasattr(self._doc, "changed"):
153
+ try:
154
+ self._doc.changed.connect(self._on_doc_changed)
155
+ except Exception:
156
+ pass
157
+
158
+ self._rebuild()
159
+
160
+
161
+ def _on_doc_changed(self):
162
+ """
163
+ Only rebuild if our bound doc is STILL the active base doc.
164
+ Prevents spurious rebuilds when focus changed between signal emit and slot run.
165
+ """
166
+ if self._dm and not self._follow_hover:
167
+ active_base = self._active_base_doc()
168
+ if not self._same_base(self._doc, active_base):
169
+ # We got a change from an old/hover doc — ignore and snap to active.
170
+ self._maybe_refresh_for_active()
171
+ return
172
+ self._rebuild()
173
+
174
+
175
+ # --- helpers ---------------------------------------------------------
176
+ def _populate_header_dict(self, d: dict, title="Header (dict)"):
177
+ # We translate the default title if it's the default, but often title is passed in.
178
+ # If title is passed in English from other methods, we should translate it at the call site or here if possible.
179
+ # Since title is variable, we'll leave it as is, but ensure call sites pass translated strings.
180
+ root = QTreeWidgetItem([title])
181
+ self._tree.addTopLevelItem(root)
182
+ for k, v in d.items():
183
+ root.addChild(QTreeWidgetItem([str(k), str(v)]))
184
+
185
+ def _populate_header_snapshot(self, snap: dict):
186
+ fmt = (snap or {}).get("format", "")
187
+ if fmt == "fits-cards":
188
+ cards = snap.get("cards") or []
189
+ hdr = fits.Header()
190
+ for k, v, c in cards:
191
+ try:
192
+ hdr[str(k)] = (v, c)
193
+ except Exception:
194
+ # extremely defensive: skip bad card entries
195
+ pass
196
+ try:
197
+ hdr = _drop_invalid_cards(hdr)
198
+ except Exception:
199
+ pass
200
+ self._populate_fits_header(hdr)
201
+ elif fmt == "dict":
202
+ self._populate_header_dict(snap.get("items") or {}, self.tr("Header (snapshot)"))
203
+ else:
204
+ # generic repr fallback
205
+ txt = (snap or {}).get("text", "")
206
+ node = QTreeWidgetItem([self.tr("Header (snapshot)")])
207
+ self._tree.addTopLevelItem(node)
208
+ node.addChild(QTreeWidgetItem(["repr", str(txt)]))
209
+
210
+
211
+ def _try_populate_from_doc(self, meta: dict) -> bool:
212
+ """Return True if we showed any header from the document metadata."""
213
+ # 1) direct astropy header
214
+ hdr = meta.get("original_header") or meta.get("fits_header") or meta.get("header")
215
+ if isinstance(hdr, fits.Header):
216
+ try:
217
+ hdr = _drop_invalid_cards(hdr.copy())
218
+ except Exception:
219
+ pass
220
+ self._populate_fits_header(hdr)
221
+ return True
222
+
223
+ # 2) dict-style header (e.g., XISF-style properties captured as dict)
224
+ if isinstance(hdr, dict):
225
+ self._populate_header_dict(hdr, self.tr("Header (dict from document)"))
226
+ return True
227
+
228
+ # 3) JSON-safe snapshot captured by DocManager
229
+ snap = meta.get("__header_snapshot__")
230
+ if isinstance(snap, dict):
231
+ self._populate_header_snapshot(snap)
232
+ return True
233
+
234
+ # 4) XISF properties stored in metadata (common keys)
235
+ for k in ("xisf_header", "xisf_properties"):
236
+ if isinstance(meta.get(k), dict):
237
+ self._populate_header_dict(meta[k], self.tr("XISF Properties (document)"))
238
+ return True
239
+
240
+ return False
241
+
242
+ def _try_populate_from_file(self, path: str, meta: dict) -> bool:
243
+ """Return True if we read & showed header from the backing file."""
244
+ if not path:
245
+ return False
246
+ p = path.lower()
247
+
248
+ # FITS (and MEF and .fz) via legacy helper
249
+ if p.endswith((".fits", ".fit", ".fz", ".fits.fz", ".fit.fz")):
250
+ # prefer the on-disk header if not already in meta
251
+ file_hdr = meta.get("original_header")
252
+ if isinstance(file_hdr, fits.Header):
253
+ try:
254
+ file_hdr = _drop_invalid_cards(file_hdr.copy())
255
+ except Exception:
256
+ pass
257
+ else:
258
+ file_hdr, _ = get_valid_header(path)
259
+
260
+ if isinstance(file_hdr, fits.Header):
261
+ self._populate_fits_header(file_hdr)
262
+ return True
263
+
264
+ # XISF: try to open and show basic properties if available
265
+ if p.endswith(".xisf"):
266
+ try:
267
+ xisf = XISF(path)
268
+ props = getattr(xisf, "properties", None)
269
+ if isinstance(props, dict):
270
+ self._populate_header_dict(props, self.tr("XISF Properties"))
271
+ return True
272
+ except Exception:
273
+ pass
274
+
275
+ return False
276
+
277
+
278
+ # --- main ------------------------------------------------------------
279
+ def _rebuild(self):
280
+ self._tree.clear()
281
+ base_doc = self._unwrap_base_doc(self._doc)
282
+ if not base_doc:
283
+ self.setWindowTitle(self.tr("Header Viewer"))
284
+ return
285
+ self._doc = base_doc
286
+
287
+ meta = self._doc.metadata or {}
288
+ path = (meta.get("file_path") or "") if isinstance(meta.get("file_path"), str) else ""
289
+ base = os.path.basename(path) if path else (meta.get("display_name") or self.tr("Untitled"))
290
+ self.setWindowTitle(self.tr("Header: {0}").format(base))
291
+
292
+ try:
293
+ # 1) Prefer header data already stored with the document
294
+ shown_any = self._try_populate_from_doc(meta)
295
+
296
+ # 2) If we didn't render anything yet, fall back to the file on disk
297
+ if not shown_any:
298
+ shown_any = self._try_populate_from_file(path, meta)
299
+
300
+ # 3) If there is a real astropy.wcs.WCS object, render it as key/value rows
301
+ try:
302
+ from astropy.wcs import WCS as _WCS
303
+ wcs_obj = meta.get("wcs")
304
+ if isinstance(wcs_obj, _WCS):
305
+ self._populate_wcs(wcs_obj)
306
+ except Exception:
307
+ pass
308
+
309
+ # 4) Always show remaining lightweight metadata (skip heavy blobs we already rendered)
310
+ info_root = QTreeWidgetItem([self.tr("Metadata")])
311
+ self._tree.addTopLevelItem(info_root)
312
+ for k, v in meta.items():
313
+ if k in ("original_header", "fits_header", "header", "wcs", "__header_snapshot__", "xisf_header", "xisf_properties"):
314
+ continue
315
+ info_root.addChild(QTreeWidgetItem([str(k), str(v)]))
316
+
317
+ self._tree.expandAll()
318
+
319
+ except Exception:
320
+ # per request: fail silently on final exception
321
+ pass
322
+
323
+
324
+ # ---- population helpers ----
325
+ def _populate_fits_header(self, header: Any):
326
+ root = QTreeWidgetItem([self.tr("FITS Header")])
327
+ self._tree.addTopLevelItem(root)
328
+
329
+ # FITS Header: sanitize and iterate cards defensively
330
+ if isinstance(header, fits.Header):
331
+ try:
332
+ header = _drop_invalid_cards(header)
333
+ except Exception:
334
+ pass
335
+
336
+ for card in header.cards:
337
+ try:
338
+ k = str(card.keyword)
339
+ v = str(card.value)
340
+ except VerifyError as e:
341
+ # Skip invalid/unparsable card
342
+ print(f"[HeaderViewer] Skipping invalid FITS card {getattr(card, 'keyword', '?')!r}: {e}")
343
+ continue
344
+ except Exception as e:
345
+ print(f"[HeaderViewer] Error reading FITS card: {e}")
346
+ continue
347
+ root.addChild(QTreeWidgetItem([k, v]))
348
+
349
+ # Plain dict fallback (e.g., XISF-style dict)
350
+ elif isinstance(header, dict):
351
+ for k, v in header.items():
352
+ try:
353
+ root.addChild(QTreeWidgetItem([str(k), str(v)]))
354
+ except Exception:
355
+ continue
356
+
357
+
358
+ def _populate_wcs(self, wcs_obj):
359
+ """Show a real astropy.wcs.WCS as header-like key/values."""
360
+ root = QTreeWidgetItem([self.tr("WCS")])
361
+ self._tree.addTopLevelItem(root)
362
+ try:
363
+ # Use relax=True so SIP/etc. are included if present.
364
+ wcs_hdr = wcs_obj.to_header(relax=True)
365
+ for k, v in wcs_hdr.items():
366
+ root.addChild(QTreeWidgetItem([str(k), str(v)]))
367
+ except Exception:
368
+ # Fallback: parse the repr into lines (better than a single blob).
369
+ for line in str(wcs_obj).splitlines():
370
+ s = line.strip()
371
+ if not s:
372
+ continue
373
+ if ":" in s:
374
+ a, b = s.split(":", 1)
375
+ root.addChild(QTreeWidgetItem([a.strip(), b.strip()]))
376
+ else:
377
+ root.addChild(QTreeWidgetItem(["", s]))
378
+
379
+
380
+ def _populate_from_xisf(self, path: str):
381
+ x = XISF(path)
382
+ file_meta: Dict[str, Any] = x.get_file_metadata()
383
+ img_meta_list = x.get_images_metadata()
384
+ img_meta: Dict[str, Any] = img_meta_list[0] if img_meta_list else {}
385
+
386
+ # File-level metadata
387
+ froot = QTreeWidgetItem([self.tr("XISF File Metadata")])
388
+ self._tree.addTopLevelItem(froot)
389
+ for k, v in file_meta.items():
390
+ vstr = v.get("value", "") if isinstance(v, dict) else v
391
+ froot.addChild(QTreeWidgetItem([str(k), str(vstr)]))
392
+
393
+ # Image-level metadata
394
+ iroot = QTreeWidgetItem([self.tr("XISF Image Metadata")])
395
+ self._tree.addTopLevelItem(iroot)
396
+
397
+ # FITS-like keywords (nested)
398
+ if "FITSKeywords" in img_meta:
399
+ fits_item = QTreeWidgetItem(["FITSKeywords"])
400
+ iroot.addChild(fits_item)
401
+ for kw, entries in img_meta["FITSKeywords"].items():
402
+ for ent in entries:
403
+ fits_item.addChild(QTreeWidgetItem([kw, str(ent.get("value", ""))]))
404
+
405
+ # XISFProperties (nested)
406
+ if "XISFProperties" in img_meta:
407
+ props_item = QTreeWidgetItem(["XISFProperties"])
408
+ iroot.addChild(props_item)
409
+ for prop_name, prop in img_meta["XISFProperties"].items():
410
+ props_item.addChild(QTreeWidgetItem([prop_name, str(prop.get("value", ""))]))
411
+
412
+ # Any remaining flat fields
413
+ for k, v in img_meta.items():
414
+ if k in ("FITSKeywords", "XISFProperties"):
415
+ continue
416
+ iroot.addChild(QTreeWidgetItem([k, str(v)]))
417
+
418
+ self._tree.expandAll()
419
+
420
+ # ---- export ----
421
+ def _save_metadata(self):
422
+ if not self._doc:
423
+ return
424
+ path, _ = QFileDialog.getSaveFileName(self, self.tr("Save Metadata"), "", self.tr("CSV (*.csv)"))
425
+ if not path:
426
+ return
427
+
428
+ # Flatten the QTreeWidget contents into key/value rows
429
+ rows = []
430
+ def walk(item: QTreeWidgetItem, prefix: str = ""):
431
+ key = item.text(0)
432
+ val = item.text(1)
433
+ full = f"{prefix}.{key}" if prefix else key
434
+ if key and val:
435
+ rows.append((full, val))
436
+ for i in range(item.childCount()):
437
+ walk(item.child(i), full)
438
+
439
+ for i in range(self._tree.topLevelItemCount()):
440
+ walk(self._tree.topLevelItem(i))
441
+
442
+ try:
443
+ with open(path, "w", newline="", encoding="utf-8") as f:
444
+ w = csv.writer(f)
445
+ w.writerow(["Key", "Value"])
446
+ w.writerows(rows)
447
+ except Exception as e:
448
+ QMessageBox.critical(self, self.tr("Save Metadata"), self.tr("Failed to save:\n{0}").format(e))
@@ -0,0 +1,88 @@
1
+ # pro/headless_utils.py
2
+ from __future__ import annotations
3
+
4
+ def unwrap_docproxy(x, max_depth: int = 8):
5
+ """
6
+ Safely unwrap live/roi/doc proxies to a real ImageDocument when possible.
7
+ - Recurses a few levels.
8
+ - Understands LiveViewDocument (_current/_base) and ROI wrappers (_parent_doc).
9
+ - Never unwraps to None unless input was None.
10
+ """
11
+ if x is None:
12
+ return None
13
+
14
+ seen = set()
15
+ y = x
16
+
17
+ for _ in range(max_depth):
18
+ if y is None or id(y) in seen:
19
+ break
20
+ seen.add(id(y))
21
+
22
+ # LiveViewDocument / similar: prefer its resolver
23
+ cur = getattr(y, "_current", None)
24
+ if callable(cur):
25
+ try:
26
+ z = cur()
27
+ if z is not None and z is not y:
28
+ y = z
29
+ continue
30
+ except Exception:
31
+ pass
32
+
33
+ # Common doc proxy fields (ordered)
34
+ for attr in (
35
+ "_base", "base",
36
+ "_parent_doc", "parent_doc",
37
+ "base_document", "_base_document",
38
+ "_target", "target",
39
+ "_doc", "doc",
40
+ "_obj", "obj",
41
+ "_proxied", "proxied",
42
+ "_wrapped", "wrapped",
43
+ ):
44
+ try:
45
+ z = getattr(y, attr, None)
46
+ except Exception:
47
+ z = None
48
+ if z is not None and z is not y:
49
+ y = z
50
+ break
51
+ else:
52
+ break
53
+
54
+ return y
55
+
56
+
57
+
58
+ def normalize_headless_main(main_or_ctx, target_doc=None):
59
+ """
60
+ Returns (main_window, doc, doc_manager)
61
+ Ensures doc + dm are fully unwrapped and ROI-aware.
62
+ """
63
+ ctx = None
64
+ main = main_or_ctx
65
+
66
+ if hasattr(main_or_ctx, "app") and hasattr(main_or_ctx, "active_document"):
67
+ ctx = main_or_ctx
68
+ main = getattr(ctx, "app", None)
69
+ if target_doc is None:
70
+ try:
71
+ # Prefer dm.get_active_document() if possible (ROI-aware, real doc type)
72
+ dm0 = getattr(main, "doc_manager", None) or getattr(main, "dm", None)
73
+ dm0 = unwrap_docproxy(dm0)
74
+ if dm0 is not None and hasattr(dm0, "get_active_document"):
75
+ target_doc = dm0.get_active_document()
76
+ else:
77
+ target_doc = ctx.active_document()
78
+ except Exception:
79
+ target_doc = None
80
+
81
+ doc = unwrap_docproxy(target_doc)
82
+
83
+ dm = None
84
+ if main is not None:
85
+ dm = getattr(main, "doc_manager", None) or getattr(main, "dm", None)
86
+
87
+
88
+ return main, doc, dm