setiastrosuitepro 1.6.7__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 (394) 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/acv_icon.png +0 -0
  24. setiastro/images/andromedatry.png +0 -0
  25. setiastro/images/andromedatry_satellited.png +0 -0
  26. setiastro/images/annotated.png +0 -0
  27. setiastro/images/aperture.png +0 -0
  28. setiastro/images/astrosuite.ico +0 -0
  29. setiastro/images/astrosuite.png +0 -0
  30. setiastro/images/astrosuitepro.icns +0 -0
  31. setiastro/images/astrosuitepro.ico +0 -0
  32. setiastro/images/astrosuitepro.png +0 -0
  33. setiastro/images/background.png +0 -0
  34. setiastro/images/background2.png +0 -0
  35. setiastro/images/benchmark.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  37. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  38. setiastro/images/blaster.png +0 -0
  39. setiastro/images/blink.png +0 -0
  40. setiastro/images/clahe.png +0 -0
  41. setiastro/images/collage.png +0 -0
  42. setiastro/images/colorwheel.png +0 -0
  43. setiastro/images/contsub.png +0 -0
  44. setiastro/images/convo.png +0 -0
  45. setiastro/images/copyslot.png +0 -0
  46. setiastro/images/cosmic.png +0 -0
  47. setiastro/images/cosmicsat.png +0 -0
  48. setiastro/images/crop1.png +0 -0
  49. setiastro/images/cropicon.png +0 -0
  50. setiastro/images/curves.png +0 -0
  51. setiastro/images/cvs.png +0 -0
  52. setiastro/images/debayer.png +0 -0
  53. setiastro/images/denoise_cnn_custom.png +0 -0
  54. setiastro/images/denoise_cnn_graph.png +0 -0
  55. setiastro/images/disk.png +0 -0
  56. setiastro/images/dse.png +0 -0
  57. setiastro/images/exoicon.png +0 -0
  58. setiastro/images/eye.png +0 -0
  59. setiastro/images/first_quarter.png +0 -0
  60. setiastro/images/fliphorizontal.png +0 -0
  61. setiastro/images/flipvertical.png +0 -0
  62. setiastro/images/font.png +0 -0
  63. setiastro/images/freqsep.png +0 -0
  64. setiastro/images/full_moon.png +0 -0
  65. setiastro/images/functionbundle.png +0 -0
  66. setiastro/images/graxpert.png +0 -0
  67. setiastro/images/green.png +0 -0
  68. setiastro/images/gridicon.png +0 -0
  69. setiastro/images/halo.png +0 -0
  70. setiastro/images/hdr.png +0 -0
  71. setiastro/images/histogram.png +0 -0
  72. setiastro/images/hubble.png +0 -0
  73. setiastro/images/imagecombine.png +0 -0
  74. setiastro/images/invert.png +0 -0
  75. setiastro/images/isophote.png +0 -0
  76. setiastro/images/isophote_demo_figure.png +0 -0
  77. setiastro/images/isophote_demo_image.png +0 -0
  78. setiastro/images/isophote_demo_model.png +0 -0
  79. setiastro/images/isophote_demo_residual.png +0 -0
  80. setiastro/images/jwstpupil.png +0 -0
  81. setiastro/images/last_quarter.png +0 -0
  82. setiastro/images/linearfit.png +0 -0
  83. setiastro/images/livestacking.png +0 -0
  84. setiastro/images/mask.png +0 -0
  85. setiastro/images/maskapply.png +0 -0
  86. setiastro/images/maskcreate.png +0 -0
  87. setiastro/images/maskremove.png +0 -0
  88. setiastro/images/morpho.png +0 -0
  89. setiastro/images/mosaic.png +0 -0
  90. setiastro/images/multiscale_decomp.png +0 -0
  91. setiastro/images/nbtorgb.png +0 -0
  92. setiastro/images/neutral.png +0 -0
  93. setiastro/images/new_moon.png +0 -0
  94. setiastro/images/nuke.png +0 -0
  95. setiastro/images/openfile.png +0 -0
  96. setiastro/images/pedestal.png +0 -0
  97. setiastro/images/pen.png +0 -0
  98. setiastro/images/pixelmath.png +0 -0
  99. setiastro/images/platesolve.png +0 -0
  100. setiastro/images/ppp.png +0 -0
  101. setiastro/images/pro.png +0 -0
  102. setiastro/images/project.png +0 -0
  103. setiastro/images/psf.png +0 -0
  104. setiastro/images/redo.png +0 -0
  105. setiastro/images/redoicon.png +0 -0
  106. setiastro/images/rescale.png +0 -0
  107. setiastro/images/rgbalign.png +0 -0
  108. setiastro/images/rgbcombo.png +0 -0
  109. setiastro/images/rgbextract.png +0 -0
  110. setiastro/images/rotate180.png +0 -0
  111. setiastro/images/rotatearbitrary.png +0 -0
  112. setiastro/images/rotateclockwise.png +0 -0
  113. setiastro/images/rotatecounterclockwise.png +0 -0
  114. setiastro/images/satellite.png +0 -0
  115. setiastro/images/script.png +0 -0
  116. setiastro/images/selectivecolor.png +0 -0
  117. setiastro/images/simbad.png +0 -0
  118. setiastro/images/slot0.png +0 -0
  119. setiastro/images/slot1.png +0 -0
  120. setiastro/images/slot2.png +0 -0
  121. setiastro/images/slot3.png +0 -0
  122. setiastro/images/slot4.png +0 -0
  123. setiastro/images/slot5.png +0 -0
  124. setiastro/images/slot6.png +0 -0
  125. setiastro/images/slot7.png +0 -0
  126. setiastro/images/slot8.png +0 -0
  127. setiastro/images/slot9.png +0 -0
  128. setiastro/images/spcc.png +0 -0
  129. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  130. setiastro/images/spinner.gif +0 -0
  131. setiastro/images/stacking.png +0 -0
  132. setiastro/images/staradd.png +0 -0
  133. setiastro/images/staralign.png +0 -0
  134. setiastro/images/starnet.png +0 -0
  135. setiastro/images/starregistration.png +0 -0
  136. setiastro/images/starspike.png +0 -0
  137. setiastro/images/starstretch.png +0 -0
  138. setiastro/images/statstretch.png +0 -0
  139. setiastro/images/supernova.png +0 -0
  140. setiastro/images/uhs.png +0 -0
  141. setiastro/images/undoicon.png +0 -0
  142. setiastro/images/upscale.png +0 -0
  143. setiastro/images/viewbundle.png +0 -0
  144. setiastro/images/waning_crescent_1.png +0 -0
  145. setiastro/images/waning_crescent_2.png +0 -0
  146. setiastro/images/waning_crescent_3.png +0 -0
  147. setiastro/images/waning_crescent_4.png +0 -0
  148. setiastro/images/waning_crescent_5.png +0 -0
  149. setiastro/images/waning_gibbous_1.png +0 -0
  150. setiastro/images/waning_gibbous_2.png +0 -0
  151. setiastro/images/waning_gibbous_3.png +0 -0
  152. setiastro/images/waning_gibbous_4.png +0 -0
  153. setiastro/images/waning_gibbous_5.png +0 -0
  154. setiastro/images/waxing_crescent_1.png +0 -0
  155. setiastro/images/waxing_crescent_2.png +0 -0
  156. setiastro/images/waxing_crescent_3.png +0 -0
  157. setiastro/images/waxing_crescent_4.png +0 -0
  158. setiastro/images/waxing_crescent_5.png +0 -0
  159. setiastro/images/waxing_gibbous_1.png +0 -0
  160. setiastro/images/waxing_gibbous_2.png +0 -0
  161. setiastro/images/waxing_gibbous_3.png +0 -0
  162. setiastro/images/waxing_gibbous_4.png +0 -0
  163. setiastro/images/waxing_gibbous_5.png +0 -0
  164. setiastro/images/whitebalance.png +0 -0
  165. setiastro/images/wimi_icon_256x256.png +0 -0
  166. setiastro/images/wimilogo.png +0 -0
  167. setiastro/images/wims.png +0 -0
  168. setiastro/images/wrench_icon.png +0 -0
  169. setiastro/images/xisfliberator.png +0 -0
  170. setiastro/qml/ResourceMonitor.qml +128 -0
  171. setiastro/saspro/__init__.py +20 -0
  172. setiastro/saspro/__main__.py +964 -0
  173. setiastro/saspro/_generated/__init__.py +7 -0
  174. setiastro/saspro/_generated/build_info.py +3 -0
  175. setiastro/saspro/abe.py +1379 -0
  176. setiastro/saspro/abe_preset.py +196 -0
  177. setiastro/saspro/aberration_ai.py +910 -0
  178. setiastro/saspro/aberration_ai_preset.py +224 -0
  179. setiastro/saspro/accel_installer.py +218 -0
  180. setiastro/saspro/accel_workers.py +30 -0
  181. setiastro/saspro/acv_exporter.py +379 -0
  182. setiastro/saspro/add_stars.py +627 -0
  183. setiastro/saspro/astrobin_exporter.py +1010 -0
  184. setiastro/saspro/astrospike.py +153 -0
  185. setiastro/saspro/astrospike_python.py +1841 -0
  186. setiastro/saspro/autostretch.py +198 -0
  187. setiastro/saspro/backgroundneutral.py +639 -0
  188. setiastro/saspro/batch_convert.py +328 -0
  189. setiastro/saspro/batch_renamer.py +522 -0
  190. setiastro/saspro/blemish_blaster.py +494 -0
  191. setiastro/saspro/blink_comparator_pro.py +3149 -0
  192. setiastro/saspro/bundles.py +61 -0
  193. setiastro/saspro/bundles_dock.py +114 -0
  194. setiastro/saspro/cheat_sheet.py +213 -0
  195. setiastro/saspro/clahe.py +371 -0
  196. setiastro/saspro/comet_stacking.py +1442 -0
  197. setiastro/saspro/common_tr.py +107 -0
  198. setiastro/saspro/config.py +38 -0
  199. setiastro/saspro/config_bootstrap.py +40 -0
  200. setiastro/saspro/config_manager.py +316 -0
  201. setiastro/saspro/continuum_subtract.py +1620 -0
  202. setiastro/saspro/convo.py +1403 -0
  203. setiastro/saspro/convo_preset.py +414 -0
  204. setiastro/saspro/copyastro.py +190 -0
  205. setiastro/saspro/cosmicclarity.py +1593 -0
  206. setiastro/saspro/cosmicclarity_preset.py +407 -0
  207. setiastro/saspro/crop_dialog_pro.py +1005 -0
  208. setiastro/saspro/crop_preset.py +189 -0
  209. setiastro/saspro/curve_editor_pro.py +2608 -0
  210. setiastro/saspro/curves_preset.py +375 -0
  211. setiastro/saspro/debayer.py +673 -0
  212. setiastro/saspro/debug_utils.py +29 -0
  213. setiastro/saspro/dnd_mime.py +35 -0
  214. setiastro/saspro/doc_manager.py +2727 -0
  215. setiastro/saspro/exoplanet_detector.py +2258 -0
  216. setiastro/saspro/file_utils.py +284 -0
  217. setiastro/saspro/fitsmodifier.py +748 -0
  218. setiastro/saspro/fix_bom.py +32 -0
  219. setiastro/saspro/free_torch_memory.py +48 -0
  220. setiastro/saspro/frequency_separation.py +1352 -0
  221. setiastro/saspro/function_bundle.py +1596 -0
  222. setiastro/saspro/generate_translations.py +3092 -0
  223. setiastro/saspro/ghs_dialog_pro.py +728 -0
  224. setiastro/saspro/ghs_preset.py +284 -0
  225. setiastro/saspro/graxpert.py +638 -0
  226. setiastro/saspro/graxpert_preset.py +287 -0
  227. setiastro/saspro/gui/__init__.py +0 -0
  228. setiastro/saspro/gui/main_window.py +8928 -0
  229. setiastro/saspro/gui/mixins/__init__.py +33 -0
  230. setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
  231. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  232. setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
  233. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  234. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  235. setiastro/saspro/gui/mixins/menu_mixin.py +391 -0
  236. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  237. setiastro/saspro/gui/mixins/toolbar_mixin.py +1824 -0
  238. setiastro/saspro/gui/mixins/update_mixin.py +323 -0
  239. setiastro/saspro/gui/mixins/view_mixin.py +477 -0
  240. setiastro/saspro/gui/statistics_dialog.py +47 -0
  241. setiastro/saspro/halobgon.py +492 -0
  242. setiastro/saspro/header_viewer.py +448 -0
  243. setiastro/saspro/headless_utils.py +88 -0
  244. setiastro/saspro/histogram.py +760 -0
  245. setiastro/saspro/history_explorer.py +941 -0
  246. setiastro/saspro/i18n.py +168 -0
  247. setiastro/saspro/image_combine.py +421 -0
  248. setiastro/saspro/image_peeker_pro.py +1608 -0
  249. setiastro/saspro/imageops/__init__.py +37 -0
  250. setiastro/saspro/imageops/mdi_snap.py +292 -0
  251. setiastro/saspro/imageops/scnr.py +36 -0
  252. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  253. setiastro/saspro/imageops/stretch.py +236 -0
  254. setiastro/saspro/isophote.py +1186 -0
  255. setiastro/saspro/layers.py +208 -0
  256. setiastro/saspro/layers_dock.py +714 -0
  257. setiastro/saspro/lazy_imports.py +193 -0
  258. setiastro/saspro/legacy/__init__.py +2 -0
  259. setiastro/saspro/legacy/image_manager.py +2360 -0
  260. setiastro/saspro/legacy/numba_utils.py +3676 -0
  261. setiastro/saspro/legacy/xisf.py +1213 -0
  262. setiastro/saspro/linear_fit.py +537 -0
  263. setiastro/saspro/live_stacking.py +1854 -0
  264. setiastro/saspro/log_bus.py +5 -0
  265. setiastro/saspro/logging_config.py +460 -0
  266. setiastro/saspro/luminancerecombine.py +510 -0
  267. setiastro/saspro/main_helpers.py +201 -0
  268. setiastro/saspro/mask_creation.py +1090 -0
  269. setiastro/saspro/masks_core.py +56 -0
  270. setiastro/saspro/mdi_widgets.py +353 -0
  271. setiastro/saspro/memory_utils.py +666 -0
  272. setiastro/saspro/metadata_patcher.py +75 -0
  273. setiastro/saspro/mfdeconv.py +3909 -0
  274. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  275. setiastro/saspro/mfdeconvcudnn.py +3312 -0
  276. setiastro/saspro/mfdeconvsport.py +2459 -0
  277. setiastro/saspro/minorbodycatalog.py +567 -0
  278. setiastro/saspro/morphology.py +411 -0
  279. setiastro/saspro/multiscale_decomp.py +1751 -0
  280. setiastro/saspro/nbtorgb_stars.py +541 -0
  281. setiastro/saspro/numba_utils.py +3145 -0
  282. setiastro/saspro/numba_warmup.py +141 -0
  283. setiastro/saspro/ops/__init__.py +9 -0
  284. setiastro/saspro/ops/command_help_dialog.py +623 -0
  285. setiastro/saspro/ops/command_runner.py +217 -0
  286. setiastro/saspro/ops/commands.py +1594 -0
  287. setiastro/saspro/ops/script_editor.py +1105 -0
  288. setiastro/saspro/ops/scripts.py +1476 -0
  289. setiastro/saspro/ops/settings.py +637 -0
  290. setiastro/saspro/parallel_utils.py +554 -0
  291. setiastro/saspro/pedestal.py +121 -0
  292. setiastro/saspro/perfect_palette_picker.py +1105 -0
  293. setiastro/saspro/pipeline.py +110 -0
  294. setiastro/saspro/pixelmath.py +1604 -0
  295. setiastro/saspro/plate_solver.py +2480 -0
  296. setiastro/saspro/project_io.py +797 -0
  297. setiastro/saspro/psf_utils.py +136 -0
  298. setiastro/saspro/psf_viewer.py +631 -0
  299. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  300. setiastro/saspro/remove_green.py +331 -0
  301. setiastro/saspro/remove_stars.py +1599 -0
  302. setiastro/saspro/remove_stars_preset.py +446 -0
  303. setiastro/saspro/resources.py +570 -0
  304. setiastro/saspro/rgb_combination.py +208 -0
  305. setiastro/saspro/rgb_extract.py +19 -0
  306. setiastro/saspro/rgbalign.py +727 -0
  307. setiastro/saspro/runtime_imports.py +7 -0
  308. setiastro/saspro/runtime_torch.py +754 -0
  309. setiastro/saspro/save_options.py +73 -0
  310. setiastro/saspro/selective_color.py +1614 -0
  311. setiastro/saspro/sfcc.py +1530 -0
  312. setiastro/saspro/shortcuts.py +3125 -0
  313. setiastro/saspro/signature_insert.py +1106 -0
  314. setiastro/saspro/stacking_suite.py +19069 -0
  315. setiastro/saspro/star_alignment.py +7383 -0
  316. setiastro/saspro/star_alignment_preset.py +329 -0
  317. setiastro/saspro/star_metrics.py +49 -0
  318. setiastro/saspro/star_spikes.py +769 -0
  319. setiastro/saspro/star_stretch.py +542 -0
  320. setiastro/saspro/stat_stretch.py +554 -0
  321. setiastro/saspro/status_log_dock.py +78 -0
  322. setiastro/saspro/subwindow.py +3523 -0
  323. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  324. setiastro/saspro/swap_manager.py +134 -0
  325. setiastro/saspro/torch_backend.py +89 -0
  326. setiastro/saspro/torch_rejection.py +434 -0
  327. setiastro/saspro/translations/all_source_strings.json +4726 -0
  328. setiastro/saspro/translations/ar_translations.py +4096 -0
  329. setiastro/saspro/translations/de_translations.py +3728 -0
  330. setiastro/saspro/translations/es_translations.py +4169 -0
  331. setiastro/saspro/translations/fr_translations.py +4090 -0
  332. setiastro/saspro/translations/hi_translations.py +3803 -0
  333. setiastro/saspro/translations/integrate_translations.py +271 -0
  334. setiastro/saspro/translations/it_translations.py +4728 -0
  335. setiastro/saspro/translations/ja_translations.py +3834 -0
  336. setiastro/saspro/translations/pt_translations.py +3847 -0
  337. setiastro/saspro/translations/ru_translations.py +3082 -0
  338. setiastro/saspro/translations/saspro_ar.qm +0 -0
  339. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  340. setiastro/saspro/translations/saspro_de.qm +0 -0
  341. setiastro/saspro/translations/saspro_de.ts +14548 -0
  342. setiastro/saspro/translations/saspro_es.qm +0 -0
  343. setiastro/saspro/translations/saspro_es.ts +16202 -0
  344. setiastro/saspro/translations/saspro_fr.qm +0 -0
  345. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  346. setiastro/saspro/translations/saspro_hi.qm +0 -0
  347. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  348. setiastro/saspro/translations/saspro_it.qm +0 -0
  349. setiastro/saspro/translations/saspro_it.ts +19046 -0
  350. setiastro/saspro/translations/saspro_ja.qm +0 -0
  351. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  352. setiastro/saspro/translations/saspro_pt.qm +0 -0
  353. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  354. setiastro/saspro/translations/saspro_ru.qm +0 -0
  355. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  356. setiastro/saspro/translations/saspro_sw.qm +0 -0
  357. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  358. setiastro/saspro/translations/saspro_uk.qm +0 -0
  359. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  360. setiastro/saspro/translations/saspro_zh.qm +0 -0
  361. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  362. setiastro/saspro/translations/sw_translations.py +3897 -0
  363. setiastro/saspro/translations/uk_translations.py +3929 -0
  364. setiastro/saspro/translations/zh_translations.py +3910 -0
  365. setiastro/saspro/versioning.py +77 -0
  366. setiastro/saspro/view_bundle.py +1558 -0
  367. setiastro/saspro/wavescale_hdr.py +648 -0
  368. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  369. setiastro/saspro/wavescalede.py +683 -0
  370. setiastro/saspro/wavescalede_preset.py +230 -0
  371. setiastro/saspro/wcs_update.py +374 -0
  372. setiastro/saspro/whitebalance.py +540 -0
  373. setiastro/saspro/widgets/__init__.py +48 -0
  374. setiastro/saspro/widgets/common_utilities.py +306 -0
  375. setiastro/saspro/widgets/graphics_views.py +122 -0
  376. setiastro/saspro/widgets/image_utils.py +518 -0
  377. setiastro/saspro/widgets/minigame/game.js +991 -0
  378. setiastro/saspro/widgets/minigame/index.html +53 -0
  379. setiastro/saspro/widgets/minigame/style.css +241 -0
  380. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  381. setiastro/saspro/widgets/resource_monitor.py +313 -0
  382. setiastro/saspro/widgets/spinboxes.py +290 -0
  383. setiastro/saspro/widgets/themed_buttons.py +13 -0
  384. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  385. setiastro/saspro/wimi.py +7367 -0
  386. setiastro/saspro/wims.py +588 -0
  387. setiastro/saspro/window_shelf.py +185 -0
  388. setiastro/saspro/xisf.py +1213 -0
  389. setiastrosuitepro-1.6.7.dist-info/METADATA +279 -0
  390. setiastrosuitepro-1.6.7.dist-info/RECORD +394 -0
  391. setiastrosuitepro-1.6.7.dist-info/WHEEL +4 -0
  392. setiastrosuitepro-1.6.7.dist-info/entry_points.txt +6 -0
  393. setiastrosuitepro-1.6.7.dist-info/licenses/LICENSE +674 -0
  394. setiastrosuitepro-1.6.7.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,1106 @@
1
+ # pro/signature_insert.py
2
+ from __future__ import annotations
3
+ import math
4
+ import numpy as np
5
+
6
+ from PyQt6.QtCore import Qt, QTimer, QRectF, QPointF
7
+ from PyQt6.QtGui import (
8
+ QImage, QPixmap, QPainter, QColor, QPen, QTransform, QIcon, QFont, QPainterPath, QFontMetricsF, QFontDatabase, QTextCursor, QTextCharFormat, QBrush
9
+ )
10
+ from PyQt6.QtWidgets import (
11
+ QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QLabel, QPushButton,
12
+ QSlider, QCheckBox, QColorDialog, QComboBox, QFileDialog, QInputDialog, QMenu,
13
+ QMessageBox, QWidget, QGraphicsView, QGraphicsScene, QGraphicsItem,QFontComboBox, QGraphicsTextItem,
14
+ QGraphicsPixmapItem, QGraphicsEllipseItem, QGraphicsRectItem, QSpinBox
15
+ )
16
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
17
+
18
+
19
+ def _np_to_qimage_rgb(a: np.ndarray) -> QImage:
20
+ a = np.asarray(a, dtype=np.float32)
21
+ a = np.clip(a, 0.0, 1.0)
22
+ if a.ndim == 2:
23
+ a = a[..., None].repeat(3, axis=2)
24
+ if a.shape[2] != 3:
25
+ a = a[:, :, :3]
26
+ u8 = (a * 255.0).astype(np.uint8)
27
+ h, w = u8.shape[:2]
28
+ return QImage(u8.data, w, h, w*3, QImage.Format.Format_RGB888).copy()
29
+
30
+ def _qimage_to_np_rgba(img: QImage) -> np.ndarray:
31
+ q = img.convertToFormat(QImage.Format.Format_RGBA8888)
32
+ w, h = q.width(), q.height()
33
+ ptr = q.bits(); ptr.setsize(h * q.bytesPerLine())
34
+ buf = np.frombuffer(ptr, dtype=np.uint8).reshape((h, q.bytesPerLine()))
35
+ arr = buf[:, :w*4].reshape((h, w, 4)).astype(np.float32) / 255.0
36
+ return arr
37
+
38
+ def _anchor_point(base_w: int, base_h: int, ins_w: int, ins_h: int,
39
+ key: str, mx: int, my: int) -> QPointF:
40
+ # compute top-left anchor by key + margins
41
+ left = 0 + mx
42
+ right = base_w - ins_w - mx
43
+ top = 0 + my
44
+ bottom = base_h - ins_h - my
45
+ center_x = (base_w - ins_w) / 2
46
+ center_y = (base_h - ins_h) / 2
47
+ table = {
48
+ "top_left": QPointF(left, top),
49
+ "top_center": QPointF(center_x, top),
50
+ "top_right": QPointF(right, top),
51
+ "middle_left": QPointF(left, center_y),
52
+ "center": QPointF(center_x, center_y),
53
+ "middle_right": QPointF(right, center_y),
54
+ "bottom_left": QPointF(left, bottom),
55
+ "bottom_center": QPointF(center_x, bottom),
56
+ "bottom_right": QPointF(right, bottom),
57
+ }
58
+ return table.get(key, QPointF(right, bottom)) # default BR
59
+
60
+ def apply_signature_preset_to_doc(doc, preset: dict) -> np.ndarray:
61
+ """
62
+ Headless apply of signature/insert using a preset.
63
+ Preset fields (all optional except file_path):
64
+ - file_path: str (PNG recommended; alpha preserved)
65
+ - position: str in {"top_left","top_center","top_right",
66
+ "middle_left","center","middle_right",
67
+ "bottom_left","bottom_center","bottom_right"}
68
+ - margin_x: int pixels (default 20)
69
+ - margin_y: int pixels (default 20)
70
+ - scale: percent (default 100)
71
+ - rotation: degrees (default 0)
72
+ - opacity: percent (default 100)
73
+ Returns: RGB float32 image in [0,1]
74
+ """
75
+ fp = str(preset.get("file_path", "")).strip()
76
+ if not fp:
77
+ raise ValueError("Preset missing 'file_path'.")
78
+
79
+ # base → RGB
80
+ base = np.asarray(getattr(doc, "image", None), dtype=np.float32)
81
+ if base is None:
82
+ raise RuntimeError("Document has no image.")
83
+ if base.ndim == 2:
84
+ base_rgb = np.repeat(base[:, :, None], 3, axis=2)
85
+ elif base.ndim == 3 and base.shape[2] == 1:
86
+ base_rgb = np.repeat(base, 3, axis=2)
87
+ else:
88
+ base_rgb = base[:, :, :3]
89
+ base_rgb = np.clip(base_rgb, 0, 1)
90
+
91
+ # canvas (ARGB32 so we can keep alpha while painting)
92
+ canvas = QImage(base_rgb.shape[1], base_rgb.shape[0], QImage.Format.Format_ARGB32)
93
+ canvas.fill(Qt.GlobalColor.transparent)
94
+ p = QPainter(canvas)
95
+ # draw base first (opaque)
96
+ p.drawImage(QPointF(0, 0), _np_to_qimage_rgb(base_rgb))
97
+
98
+ # load insert (alpha preserved)
99
+ ins_img = QImage(fp)
100
+ if ins_img.isNull():
101
+ p.end()
102
+ raise ValueError(f"Could not load insert image: {fp}")
103
+
104
+ # parameters
105
+ pos_key = str(preset.get("position", "bottom_right"))
106
+ mx = int(preset.get("margin_x", 20))
107
+ my = int(preset.get("margin_y", 20))
108
+ scale = float(preset.get("scale", 100)) / 100.0
109
+ rotation = float(preset.get("rotation", 0.0))
110
+ opacity = max(0.0, min(1.0, float(preset.get("opacity", 100)) / 100.0))
111
+
112
+ # transform: scale + rotate around center, then translate to anchor
113
+ iw, ih = ins_img.width(), ins_img.height()
114
+ aw = max(1, int(round(iw * scale)))
115
+ ah = max(1, int(round(ih * scale)))
116
+
117
+ # NOTE: we can let QPainter scale it by world transform (keeps alpha)
118
+ # Compute anchor for the post-transform bounding rect.
119
+ # For rotation, the item’s visual bbox changes; the usual UX expectation is:
120
+ # "put the visual center on the anchor then offset by half of its size".
121
+ # We do: transform about center, then translate so the *visual* top-left hits the anchor.
122
+ t = QTransform()
123
+ t.translate(aw/2, ah/2)
124
+ t.rotate(rotation)
125
+ t.scale(scale, scale) # scale first or last works since we rotate around center
126
+ t.translate(-iw/2, -ih/2)
127
+
128
+ # Find visual bbox of transformed image to compute margins correctly
129
+ transformed_rect = t.mapRect(QRectF(0, 0, iw, ih))
130
+ vis_w, vis_h = transformed_rect.width(), transformed_rect.height()
131
+
132
+ anchor = _anchor_point(base_rgb.shape[1], base_rgb.shape[0], int(round(vis_w)), int(round(vis_h)), pos_key, mx, my)
133
+
134
+ # Now shift so that the transformed visual top-left lands at anchor
135
+ t2 = QTransform(t)
136
+ t2.translate(anchor.x() - transformed_rect.left(), anchor.y() - transformed_rect.top())
137
+
138
+ p.setOpacity(opacity)
139
+ p.setWorldTransform(t2, combine=False)
140
+ p.drawImage(QPointF(0, 0), ins_img)
141
+ p.end()
142
+
143
+ # back to numpy (drop alpha → RGB)
144
+ out_rgba = _qimage_to_np_rgba(canvas)
145
+ out_rgb = out_rgba[:, :, :3]
146
+ out_rgb = np.clip(out_rgb, 0.0, 1.0).astype(np.float32, copy=False)
147
+ return out_rgb
148
+
149
+ # --------------------------- Graphics helpers ---------------------------
150
+
151
+ class TransformHandle(QGraphicsEllipseItem):
152
+ """
153
+ A small circular handle that sits on the top-right of the item's local
154
+ bounds and allows scale+rotation by dragging. Works for any QGraphicsItem
155
+ that has boundingRect()/setScale()/setRotation().
156
+ """
157
+ def __init__(self, parent_item: QGraphicsItem, scene: QGraphicsScene):
158
+ super().__init__(-5, -5, 10, 10)
159
+ self.parent_item = parent_item
160
+ self.scene = scene
161
+
162
+ self.setBrush(QColor("blue"))
163
+ self.setPen(QPen(Qt.PenStyle.SolidLine))
164
+ self.setCursor(Qt.CursorShape.SizeAllCursor)
165
+ self.setZValue(2)
166
+
167
+ self.setFlags(
168
+ QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
169
+ QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
170
+ QGraphicsItem.GraphicsItemFlag.ItemIgnoresParentOpacity |
171
+ QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
172
+ )
173
+ self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
174
+ self.setAcceptHoverEvents(True)
175
+
176
+ self.initial_distance = None
177
+ self.initial_angle = None
178
+ self.initial_scale = max(0.05, float(self.parent_item.scale()) if hasattr(self.parent_item, "scale") else 1.0)
179
+
180
+ scene.addItem(self)
181
+ self.update_position()
182
+
183
+ def update_position(self):
184
+ corner = self.parent_item.boundingRect().topRight()
185
+ scene_corner = self.parent_item.mapToScene(corner)
186
+ self.setPos(scene_corner)
187
+
188
+ def mousePressEvent(self, e):
189
+ center = self.parent_item.mapToScene(self.parent_item.boundingRect().center())
190
+ delta = self.scenePos() - center
191
+ self.initial_distance = math.hypot(delta.x(), delta.y())
192
+ self.initial_angle = math.degrees(math.atan2(delta.y(), delta.x()))
193
+ sc = getattr(self.parent_item, "scale", None)
194
+ self.initial_scale = sc() if callable(sc) else 1.0
195
+ e.accept()
196
+
197
+ def mouseMoveEvent(self, e):
198
+ center = self.parent_item.mapToScene(self.parent_item.boundingRect().center())
199
+ new_pos = self.mapToScene(e.pos())
200
+ delta = new_pos - center
201
+ dist = math.hypot(delta.x(), delta.y())
202
+ ang = math.degrees(math.atan2(delta.y(), delta.x()))
203
+
204
+ # scale
205
+ s = (dist / self.initial_distance) if self.initial_distance else 1.0
206
+ new_scale = max(0.05, float(self.initial_scale) * s)
207
+ if hasattr(self.parent_item, "setScale"):
208
+ self.parent_item.setScale(new_scale)
209
+
210
+ # rotate
211
+ if hasattr(self.parent_item, "setRotation"):
212
+ self.parent_item.setRotation(ang - self.initial_angle)
213
+
214
+ self.update_position()
215
+ e.accept()
216
+
217
+ def mouseReleaseEvent(self, e):
218
+ self.initial_distance = None
219
+ self.initial_angle = None
220
+ sc = getattr(self.parent_item, "scale", None)
221
+ self.initial_scale = sc() if callable(sc) else 1.0
222
+ e.accept()
223
+
224
+ class OutlinedTextItem(QGraphicsTextItem):
225
+ """
226
+ Text item that paints a solid fill with an optional outline.
227
+ It still supports selection/transform/opacity like other items.
228
+ """
229
+ def __init__(self, text: str, font: QFont, fill: QColor, outline: QColor | None, outline_w: float = 0.0):
230
+ super().__init__(text)
231
+ self._font = font
232
+ self._fill = QColor(fill)
233
+ self._outline = QColor(outline) if outline else None
234
+ self._outline_w = float(max(0.0, outline_w))
235
+ self.setFont(font)
236
+ self.setDefaultTextColor(self._fill)
237
+
238
+ self.setFlags(
239
+ QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
240
+ QGraphicsItem.GraphicsItemFlag.ItemIsSelectable |
241
+ QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
242
+ QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
243
+ )
244
+ self.setTransformOriginPoint(self.boundingRect().center())
245
+ self.setZValue(1)
246
+
247
+ # simple multi-line path builder
248
+ def _text_path(self) -> QPainterPath:
249
+ path = QPainterPath()
250
+ fm = QFontMetricsF(self._font)
251
+ lh = fm.lineSpacing()
252
+ y = 0.0
253
+ for i, line in enumerate(self.toPlainText().splitlines() or [""]):
254
+ # baseline at +ascent for each line
255
+ path.addText(0, y + fm.ascent(), self._font, line)
256
+ y += lh
257
+ return path
258
+
259
+ def paint(self, painter, option, widget=None):
260
+ # draw fill as normal (fast)
261
+ if not self._outline or self._outline_w <= 0.0:
262
+ return super().paint(painter, option, widget)
263
+
264
+ # with outline: draw a vector path so the stroke is crisp after scaling
265
+ painter.save()
266
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
267
+
268
+ path = self._text_path()
269
+
270
+ # center origin like the base class would—translate so (0,0) is our item’s top-left
271
+ painter.translate(self.boundingRect().topLeft())
272
+
273
+ pen = QPen(self._outline, max(0.0, self._outline_w), Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin)
274
+ painter.setPen(pen)
275
+ painter.setBrush(QBrush(self._fill))
276
+ painter.drawPath(path)
277
+
278
+ painter.restore()
279
+
280
+ # accessors used by the controls
281
+ def set_fill(self, c: QColor):
282
+ self._fill = QColor(c)
283
+ self.setDefaultTextColor(self._fill)
284
+ self.update()
285
+
286
+ def set_outline(self, c: QColor | None, w: float):
287
+ self._outline = QColor(c) if c else None
288
+ self._outline_w = float(max(0.0, w))
289
+ self.update()
290
+
291
+ def set_font(self, f: QFont):
292
+ self._font = f
293
+ self.setFont(f)
294
+ self.update()
295
+
296
+
297
+
298
+ class InsertView(QGraphicsView):
299
+ """Pannable view + Ctrl+wheel zoom, with a right-click menu on inserts."""
300
+ def __init__(self, scene: QGraphicsScene, owner: "SignatureInsertDialogPro"):
301
+ super().__init__(scene)
302
+ self.owner = owner
303
+ self.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
304
+ self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
305
+ self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
306
+ self.zoom_factor = 1.0
307
+ self.min_zoom, self.max_zoom = 0.10, 10.0
308
+
309
+ # --- zoom/pan ---
310
+ def wheelEvent(self, e):
311
+ if e.modifiers() & Qt.KeyboardModifier.ControlModifier:
312
+ step = 1.15 if e.angleDelta().y() > 0 else 1/1.15
313
+ self.set_zoom(self.zoom_factor * step)
314
+ e.accept()
315
+ return
316
+ super().wheelEvent(e)
317
+
318
+ def set_zoom(self, z):
319
+ z = max(self.min_zoom, min(self.max_zoom, z))
320
+ self.zoom_factor = z
321
+ self.setTransform(QTransform().scale(z, z))
322
+
323
+ def zoom_in(self): self.set_zoom(self.zoom_factor * 1.15)
324
+ def zoom_out(self): self.set_zoom(self.zoom_factor / 1.15)
325
+ def fit_to_view(self):
326
+ r = self.scene().itemsBoundingRect()
327
+ if r.isEmpty(): return
328
+ self.fitInView(r, Qt.AspectRatioMode.KeepAspectRatio)
329
+ self.zoom_factor = 1.0 # logical reset
330
+
331
+ # --- context menu to snap inserts ---
332
+ def contextMenuEvent(self, e):
333
+ scene_pos = self.mapToScene(e.pos())
334
+ item = self.scene().itemAt(scene_pos, self.transform())
335
+
336
+ # If user clicked the child rect, use the parent pixmap
337
+ if isinstance(item, QGraphicsRectItem) and item.parentItem() in self.owner.inserts:
338
+ item = item.parentItem()
339
+
340
+ if ((isinstance(item, QGraphicsPixmapItem) and item in self.owner.inserts) or
341
+ isinstance(item, QGraphicsTextItem)):
342
+ m = QMenu(self)
343
+ pos = {
344
+ "Top-Left":"top_left", "Top-Center":"top_center", "Top-Right":"top_right",
345
+ "Middle-Left":"middle_left","Center":"center","Middle-Right":"middle_right",
346
+ "Bottom-Left":"bottom_left","Bottom-Center":"bottom_center","Bottom-Right":"bottom_right"
347
+ }
348
+ for label, key in pos.items():
349
+ m.addAction(label, lambda k=key, it=item: self.owner.send_insert_to_position(it, k))
350
+ m.exec(e.globalPos())
351
+ return
352
+ else:
353
+ super().contextMenuEvent(e)
354
+
355
+
356
+ # --------------------------- Main dialog ---------------------------
357
+
358
+ class SignatureInsertDialogPro(QDialog):
359
+ """
360
+ Add one or more overlays (“signatures/inserts”) on top of the active doc,
361
+ transform them interactively, then bake into the doc.
362
+ """
363
+ def __init__(self, parent, doc, icon: QIcon | None = None):
364
+ super().__init__(parent)
365
+ self.setWindowTitle(self.tr("Signature / Insert"))
366
+ self.setWindowFlag(Qt.WindowType.Window, True)
367
+ self.setWindowModality(Qt.WindowModality.NonModal)
368
+ self.setModal(False)
369
+ try:
370
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
371
+ except Exception:
372
+ pass # older PyQt6 versions
373
+ if icon:
374
+ try: self.setWindowIcon(icon)
375
+ except Exception as e:
376
+ import logging
377
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
378
+
379
+ self.doc = doc
380
+ self.scene = QGraphicsScene(self)
381
+ self.view = InsertView(self.scene, self)
382
+
383
+ self.inserts: list[QGraphicsPixmapItem] = []
384
+ self.bounding_boxes: list[QGraphicsRectItem] = []
385
+ self.bounding_boxes_enabled = True
386
+ self.bounding_box_pen = QPen(QColor("red"), 2, Qt.PenStyle.DashLine)
387
+ self.text_inserts: list[OutlinedTextItem] = []
388
+ self.scene.selectionChanged.connect(self._on_selection_changed)
389
+ # Handle sync timer (keeps the handle parked on the item corner)
390
+ self._timer = QTimer(self); self._timer.timeout.connect(self._sync_handles); self._timer.start(16)
391
+
392
+ self._build_ui()
393
+ self._update_base_image()
394
+ self.resize(1000, 680)
395
+
396
+ # -------- UI ----------
397
+ def _build_ui(self):
398
+ root = QHBoxLayout(self)
399
+
400
+ # ---- LEFT COLUMN ------------------------------------------------------
401
+ col = QVBoxLayout()
402
+
403
+ # Alpha hint (always visible – simple, clear)
404
+ alpha_hint = QLabel("Tip: Transparent signatures — use “Load from File” to preserve PNG alpha. "
405
+ "Loading from View uses RGB (no alpha).")
406
+ alpha_hint.setStyleSheet("color:#e0b000;")
407
+ alpha_hint.setWordWrap(True)
408
+ col.addWidget(alpha_hint)
409
+
410
+ # Load controls
411
+ row_load = QHBoxLayout()
412
+ b_from_view = QPushButton("Load Insert from View…"); b_from_view.clicked.connect(self._load_from_view)
413
+ b_from_file = QPushButton("Load Insert from File…"); b_from_file.clicked.connect(self._load_from_file)
414
+ row_load.addWidget(b_from_view); row_load.addWidget(b_from_file)
415
+ col.addLayout(row_load)
416
+
417
+ # --- Text controls ----------------------------------------------------
418
+ txt_grp = QGroupBox("Text")
419
+ tg = QGridLayout(txt_grp)
420
+
421
+ self.btn_add_text = QPushButton("Add Text…")
422
+ self.btn_edit_text = QPushButton("Edit Selected…"); self.btn_edit_text.setEnabled(False)
423
+ self.btn_add_text.clicked.connect(self._add_text_dialog)
424
+ self.btn_edit_text.clicked.connect(self._edit_selected_text)
425
+
426
+ tg.addWidget(self.btn_add_text, 0, 0)
427
+ tg.addWidget(self.btn_edit_text, 0, 1)
428
+
429
+ self.font_box = QFontComboBox(); self.font_box.setCurrentFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont))
430
+ self.font_size = QSpinBox(); self.font_size.setRange(4, 512); self.font_size.setValue(36)
431
+ self.chk_bold = QCheckBox("Bold")
432
+ self.chk_italic = QCheckBox("Italic")
433
+
434
+ self.btn_fill = QPushButton("Fill Color…")
435
+ self.btn_outline = QPushButton("Outline Color…")
436
+ self.outline_w = QSpinBox(); self.outline_w.setRange(0, 30); self.outline_w.setValue(0)
437
+
438
+ # wire style changes
439
+ self.font_box.currentFontChanged.connect(lambda _: self._apply_text_controls_to_selected())
440
+ self.font_size.valueChanged.connect(lambda _: self._apply_text_controls_to_selected())
441
+ self.chk_bold.stateChanged.connect(lambda _: self._apply_text_controls_to_selected())
442
+ self.chk_italic.stateChanged.connect(lambda _: self._apply_text_controls_to_selected())
443
+ self.btn_fill.clicked.connect(self._pick_text_fill)
444
+ self.btn_outline.clicked.connect(self._pick_text_outline)
445
+ self.outline_w.valueChanged.connect(lambda _: self._apply_text_controls_to_selected())
446
+
447
+ tg.addWidget(QLabel("Font"), 1, 0); tg.addWidget(self.font_box, 1, 1)
448
+ tg.addWidget(QLabel("Size"), 2, 0); tg.addWidget(self.font_size, 2, 1)
449
+ tg.addWidget(self.chk_bold, 3, 0); tg.addWidget(self.chk_italic, 3, 1)
450
+ tg.addWidget(self.btn_fill, 4, 0); tg.addWidget(self.btn_outline, 4, 1)
451
+ tg.addWidget(QLabel("Outline (px)"), 5, 0); tg.addWidget(self.outline_w, 5, 1)
452
+
453
+ col.addWidget(txt_grp)
454
+
455
+
456
+ # Transform group
457
+ grp = QGroupBox("Transform")
458
+ g = QGridLayout(grp)
459
+ b_rot = QPushButton("Rotate +90°"); b_rot.clicked.connect(self._rotate_selected)
460
+ g.addWidget(b_rot, 0, 0, 1, 2)
461
+
462
+ g.addWidget(QLabel("Scale (%)"), 1, 0)
463
+ self.sl_scale = QSlider(Qt.Orientation.Horizontal); self.sl_scale.setRange(10, 400); self.sl_scale.setValue(100)
464
+ self.sl_scale.valueChanged.connect(self._scale_selected)
465
+ g.addWidget(self.sl_scale, 1, 1)
466
+
467
+ g.addWidget(QLabel("Opacity (%)"), 2, 0)
468
+ self.sl_opacity = QSlider(Qt.Orientation.Horizontal); self.sl_opacity.setRange(0, 100); self.sl_opacity.setValue(100)
469
+ self.sl_opacity.valueChanged.connect(self._opacity_selected)
470
+ g.addWidget(self.sl_opacity, 2, 1)
471
+ col.addWidget(grp)
472
+
473
+ # Bounding boxes
474
+ self.cb_draw = QCheckBox("Draw Bounding Box"); self.cb_draw.setChecked(True); self.cb_draw.stateChanged.connect(self._toggle_boxes)
475
+ col.addWidget(self.cb_draw)
476
+
477
+ grp_box = QGroupBox("Bounding Box Style")
478
+ gb = QGridLayout(grp_box)
479
+ self.b_color = QPushButton("Color…"); self.b_color.clicked.connect(self._pick_box_color)
480
+ self.sl_thick = QSlider(Qt.Orientation.Horizontal); self.sl_thick.setRange(1, 10); self.sl_thick.setValue(2); self.sl_thick.valueChanged.connect(self._update_box_pen)
481
+ self.cmb_style = QComboBox(); self.cmb_style.addItems(["Solid","Dash","Dot","DashDot","DashDotDot"]); self.cmb_style.currentIndexChanged.connect(self._update_box_pen)
482
+ gb.addWidget(self.b_color, 0, 0, 1, 2)
483
+ gb.addWidget(QLabel("Thickness"), 1, 0); gb.addWidget(self.sl_thick, 1, 1)
484
+ gb.addWidget(QLabel("Style"), 2, 0); gb.addWidget(self.cmb_style, 2, 1)
485
+ col.addWidget(grp_box)
486
+
487
+ # --- Snap with margins -------------------------------------------------
488
+ snap_grp = QGroupBox("Send to position")
489
+ sg = QGridLayout(snap_grp)
490
+
491
+ # margins
492
+ sg.addWidget(QLabel("Margin X (px)"), 0, 0)
493
+ self.sp_margin_x = QSpinBox(); self.sp_margin_x.setRange(0, 5000); self.sp_margin_x.setValue(20)
494
+ sg.addWidget(self.sp_margin_x, 0, 1)
495
+
496
+ sg.addWidget(QLabel("Margin Y (px)"), 0, 2)
497
+ self.sp_margin_y = QSpinBox(); self.sp_margin_y.setRange(0, 5000); self.sp_margin_y.setValue(20)
498
+ sg.addWidget(self.sp_margin_y, 0, 3)
499
+
500
+ # 3x3 snap buttons
501
+ def s(key): # helper to create buttons
502
+ btn = QPushButton(key.replace('_', ' ').title())
503
+ btn.setMinimumWidth(105)
504
+ btn.clicked.connect(lambda _, k=key: self._send_selected(k))
505
+ return btn
506
+
507
+ sg.addWidget(s("top_left"), 1, 0)
508
+ sg.addWidget(s("top_center"), 1, 1)
509
+ sg.addWidget(s("top_right"), 1, 2)
510
+ sg.addWidget(s("middle_left"), 2, 0)
511
+ sg.addWidget(s("center"), 2, 1)
512
+ sg.addWidget(s("middle_right"), 2, 2)
513
+ sg.addWidget(s("bottom_left"), 3, 0)
514
+ sg.addWidget(s("bottom_center"), 3, 1)
515
+ sg.addWidget(s("bottom_right"), 3, 2)
516
+ col.addWidget(snap_grp)
517
+
518
+ # Zoom
519
+ row_zoom = QHBoxLayout()
520
+ b_zo = QPushButton("–"); b_zo.clicked.connect(self.view.zoom_out)
521
+ b_zi = QPushButton("+"); b_zi.clicked.connect(self.view.zoom_in)
522
+ b_fit = QPushButton("Fit"); b_fit.clicked.connect(self.view.fit_to_view)
523
+ row_zoom.addWidget(QLabel("Zoom (Ctrl+Wheel):")); row_zoom.addWidget(b_zo); row_zoom.addWidget(b_zi); row_zoom.addWidget(b_fit); row_zoom.addStretch(1)
524
+ col.addLayout(row_zoom)
525
+
526
+ col.addStretch(1)
527
+
528
+ # Commit/Clear
529
+ row_commit = QHBoxLayout()
530
+ b_affix = QPushButton("Affix Inserts"); b_affix.clicked.connect(self._affix_inserts)
531
+ b_clear_sel = QPushButton("Clear Selected"); b_clear_sel.clicked.connect(self._clear_selected)
532
+ b_clear = QPushButton("Clear All"); b_clear.clicked.connect(self._clear_inserts)
533
+ row_commit.addWidget(b_affix)
534
+ row_commit.addWidget(b_clear_sel) # ← NEW
535
+ row_commit.addWidget(b_clear)
536
+ row_commit.addStretch(1)
537
+ col.addLayout(row_commit)
538
+
539
+ left = QWidget(); left.setLayout(col)
540
+ root.addWidget(left, 0)
541
+ root.addWidget(self.view, 1)
542
+
543
+ def _selected_text_items(self):
544
+ return [it for it in self.scene.selectedItems() if isinstance(it, QGraphicsTextItem)]
545
+
546
+ def _selected_pixmap_items(self):
547
+ return [it for it in self.scene.selectedItems() if isinstance(it, QGraphicsPixmapItem)]
548
+
549
+ def _add_text_item(self, text: str, font: QFont, color: QColor):
550
+ ti = OutlinedTextItem(text, font, color, outline=None, outline_w=0.0)
551
+ ti.setTextInteractionFlags(Qt.TextInteractionFlag.TextEditorInteraction)
552
+ ti.setZValue(1)
553
+ ti.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
554
+ ti.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
555
+ ti.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, True)
556
+ ti.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges, True)
557
+ ti.setTransformOriginPoint(ti.boundingRect().center())
558
+ self.scene.addItem(ti)
559
+
560
+ TransformHandle(ti, self.scene)
561
+ self.text_inserts.append(ti)
562
+ ti.setSelected(True)
563
+ return ti
564
+
565
+ def _add_text_dialog(self):
566
+ # wrapper to match your signal connect
567
+ self._on_add_text()
568
+
569
+ def _add_text_dialog(self):
570
+ # wrapper to match your signal connect
571
+ self._on_add_text()
572
+
573
+ def _edit_selected_text(self):
574
+ items = self._selected_text_items()
575
+ if not items:
576
+ return
577
+ ti = items[0]
578
+ existing = ti.toPlainText()
579
+ txt, ok = QInputDialog.getMultiLineText(self, "Edit Text", "Text:", existing)
580
+ if ok:
581
+ ti.setPlainText(txt)
582
+
583
+ def _apply_text_controls_to_selected(self):
584
+ f = self._current_qfont()
585
+ w = self.outline_w.value()
586
+ for ti in self._selected_text_items():
587
+ if isinstance(ti, OutlinedTextItem):
588
+ ti.set_font(f)
589
+ # only adjust outline width here; color comes from the outline color picker
590
+ if w <= 0:
591
+ ti.set_outline(ti._outline, 0.0)
592
+ else:
593
+ ti.set_outline(ti._outline or QColor("black"), float(w))
594
+ else:
595
+ ti.setFont(f)
596
+
597
+ def _pick_text_fill(self):
598
+ c = QColorDialog.getColor()
599
+ if not c.isValid():
600
+ return
601
+ for ti in self._selected_text_items():
602
+ if isinstance(ti, OutlinedTextItem):
603
+ ti.set_fill(c)
604
+ else:
605
+ ti.setDefaultTextColor(c)
606
+
607
+ def _pick_text_outline(self):
608
+ c = QColorDialog.getColor()
609
+ if not c.isValid():
610
+ return
611
+ w = self.outline_w.value()
612
+ for ti in self._selected_text_items():
613
+ if isinstance(ti, OutlinedTextItem):
614
+ ti.set_outline(c, float(w))
615
+
616
+ def _clear_text_selection(self, ti: QGraphicsTextItem):
617
+ cur = ti.textCursor()
618
+ if cur.hasSelection():
619
+ cur.clearSelection()
620
+ ti.setTextCursor(cur)
621
+
622
+ def _remove_item_and_accessories(self, item: QGraphicsItem):
623
+ # Remove child bounding box if present & tracked
624
+ if isinstance(item, QGraphicsPixmapItem):
625
+ # child rect we added lives as parentItem(item)
626
+ for r in list(self.bounding_boxes):
627
+ if r.parentItem() is item:
628
+ try:
629
+ self.scene.removeItem(r)
630
+ except Exception:
631
+ pass
632
+ self.bounding_boxes.remove(r)
633
+
634
+ # Remove any TransformHandle bound to this item
635
+ for it in list(self.scene.items()):
636
+ if isinstance(it, TransformHandle) and getattr(it, "parent_item", None) is item:
637
+ try:
638
+ self.scene.removeItem(it)
639
+ except Exception:
640
+ pass
641
+
642
+ # Remove from our tracking lists
643
+ if isinstance(item, QGraphicsPixmapItem) and item in self.inserts:
644
+ self.inserts.remove(item)
645
+ if isinstance(item, QGraphicsTextItem) and item in self.text_inserts:
646
+ self.text_inserts.remove(item)
647
+
648
+ # Finally remove the item itself
649
+ try:
650
+ self.scene.removeItem(item)
651
+ except Exception:
652
+ pass
653
+
654
+ def _clear_selected(self):
655
+ for it in list(self.scene.selectedItems()):
656
+ # only user inserts (pixmaps) and text inserts are removable
657
+ if (isinstance(it, QGraphicsPixmapItem) and it in self.inserts) or isinstance(it, QGraphicsTextItem):
658
+ self._remove_item_and_accessories(it)
659
+
660
+
661
+ def _on_selection_changed(self):
662
+ texts = self._selected_text_items()
663
+ self.btn_edit_text.setEnabled(bool(texts))
664
+ if texts:
665
+ ti = texts[0]
666
+ f = ti.font()
667
+ self.font_box.setCurrentFont(f)
668
+ ps = f.pointSize() if f.pointSize() > 0 else 36
669
+ self.font_size.setValue(int(ps))
670
+ self.chk_bold.setChecked(f.bold())
671
+ self.chk_italic.setChecked(f.italic())
672
+ if isinstance(ti, OutlinedTextItem):
673
+ self.outline_w.setValue(int(round(ti._outline_w)))
674
+
675
+ # ── NEW: when a text item becomes unselected, clear any in-text highlight
676
+ selected_set = set(texts)
677
+ for it in self.text_inserts:
678
+ if it not in selected_set:
679
+ self._clear_text_selection(it)
680
+
681
+
682
+ def _current_qfont(self) -> QFont:
683
+ f = self.font_box.currentFont()
684
+ f.setPointSize(self.font_size.value())
685
+ f.setBold(self.chk_bold.isChecked())
686
+ f.setItalic(self.chk_italic.isChecked())
687
+ return f
688
+
689
+ def _apply_font_to_selection(self):
690
+ f = self._current_qfont()
691
+ for ti in self._selected_text_items():
692
+ ti.setFont(f)
693
+
694
+ def _apply_color_to_selection(self, color: QColor):
695
+ for ti in self._selected_text_items():
696
+ ti.setDefaultTextColor(color)
697
+
698
+ def _on_add_text(self):
699
+ txt, ok = QInputDialog.getMultiLineText(self, "Add Text", "Enter text:")
700
+ if not ok or not txt.strip():
701
+ return
702
+ f = self._current_qfont()
703
+ c = QColor("white") # default
704
+ ti = self._add_text_item(txt, f, c)
705
+ # drop it near center
706
+ base = next((i for i in self.scene.items()
707
+ if isinstance(i, QGraphicsPixmapItem) and i.zValue() == 0), None)
708
+ if base:
709
+ center_scene = base.mapToScene(base.boundingRect().center())
710
+ ti.setPos(center_scene - ti.boundingRect().center())
711
+
712
+ def _on_text_color(self):
713
+ c = QColorDialog.getColor()
714
+ if c.isValid():
715
+ self._apply_color_to_selection(c)
716
+
717
+ def _on_font_changed(self, _):
718
+ self._apply_font_to_selection()
719
+
720
+ def _on_font_size(self, _):
721
+ self._apply_font_to_selection()
722
+
723
+ def _on_font_bold(self, _):
724
+ self._apply_font_to_selection()
725
+
726
+ def _on_font_italic(self, _):
727
+ self._apply_font_to_selection()
728
+
729
+
730
+ # -------- Scene / items ----------
731
+ def _sync_handles(self):
732
+ for it in self.scene.items():
733
+ if isinstance(it, TransformHandle):
734
+ it.update_position()
735
+
736
+ def _update_base_image(self):
737
+ self.scene.clear()
738
+ arr = np.asarray(self.doc.image, dtype=np.float32)
739
+ if arr is None: return
740
+ qimg = self._numpy_to_qimage(arr)
741
+ bg = QGraphicsPixmapItem(QPixmap.fromImage(qimg))
742
+ bg.setZValue(0)
743
+ self.scene.addItem(bg)
744
+
745
+ def _load_from_file(self):
746
+ fp, _ = QFileDialog.getOpenFileName(self, "Select Insert Image", "", "Images (*.png *.jpg *.jpeg *.tif *.tiff)")
747
+ if not fp: return
748
+ pm = QPixmap(fp)
749
+ if pm.isNull():
750
+ QMessageBox.warning(self, "Load Failed", "Could not load image.")
751
+ return
752
+ self._add_insert(pm)
753
+
754
+ def _load_from_view(self):
755
+ # list all open views via a helper the app already uses elsewhere (fallback to active only)
756
+ candidates = []
757
+ if hasattr(self.parent(), "_subwindow_docs"):
758
+ for title, d in self.parent()._subwindow_docs():
759
+ if d is self.doc: # skip self
760
+ continue
761
+ if getattr(d, "image", None) is not None:
762
+ candidates.append((title, d))
763
+ if not candidates:
764
+ QMessageBox.information(self, "Insert", "No other image windows found.")
765
+ return
766
+
767
+ names = [t for (t, _) in candidates]
768
+ choice, ok = QInputDialog.getItem(self, "Load Insert from View", "Choose:", names, 0, False)
769
+ if not ok: return
770
+ d = candidates[names.index(choice)][1]
771
+ pm = QPixmap.fromImage(self._numpy_to_qimage(np.asarray(d.image, dtype=np.float32)))
772
+ self._add_insert(pm)
773
+
774
+ def _add_insert(self, pm: QPixmap):
775
+ it = QGraphicsPixmapItem(pm)
776
+ it.setFlags(
777
+ QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
778
+ QGraphicsItem.GraphicsItemFlag.ItemIsSelectable |
779
+ QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
780
+ QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
781
+ )
782
+ it.setTransformationMode(Qt.TransformationMode.SmoothTransformation)
783
+ it.setTransformOriginPoint(it.boundingRect().center())
784
+ it.setZValue(1)
785
+ it.setOpacity(1.0)
786
+ self.scene.addItem(it)
787
+ self.inserts.append(it)
788
+ TransformHandle(it, self.scene)
789
+
790
+ if self.bounding_boxes_enabled:
791
+ rect = QGraphicsRectItem(it.boundingRect())
792
+ rect.setParentItem(it)
793
+ rect.setPen(self.bounding_box_pen)
794
+ rect.setAcceptedMouseButtons(Qt.MouseButton.NoButton)
795
+ rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIgnoresParentOpacity, True)
796
+ rect.setZValue(it.zValue() + 0.1)
797
+ self.scene.addItem(rect)
798
+ self.bounding_boxes.append(rect)
799
+
800
+
801
+ def _send_selected(self, key: str):
802
+ # pixmaps
803
+ for it in self.inserts:
804
+ if it.isSelected():
805
+ self.send_insert_to_position(it, key)
806
+ # text
807
+ for ti in self._selected_text_items():
808
+ self.send_insert_to_position(ti, key)
809
+
810
+ # -------- Commands ----------
811
+ def _rotate_selected(self):
812
+ for it in self.inserts:
813
+ if it.isSelected():
814
+ it.setRotation(it.rotation() + 90)
815
+
816
+ def _scale_selected(self, val):
817
+ s = val / 100.0
818
+ for it in self.inserts:
819
+ if it.isSelected():
820
+ it.setScale(s)
821
+ # keep the child rect matching the pixmap's local bounds
822
+ for box in self.bounding_boxes:
823
+ if box.parentItem() == it:
824
+ box.setRect(it.boundingRect())
825
+
826
+ def _opacity_selected(self, val):
827
+ o = val / 100.0
828
+ for it in (self._selected_pixmap_items() + self._selected_text_items()):
829
+ it.setOpacity(o)
830
+
831
+ def _toggle_boxes(self, state):
832
+ self.bounding_boxes_enabled = bool(state)
833
+ for r in self.bounding_boxes:
834
+ r.setVisible(self.bounding_boxes_enabled)
835
+
836
+ def _pick_box_color(self):
837
+ c = QColorDialog.getColor()
838
+ if c.isValid():
839
+ self.bounding_box_pen.setColor(c)
840
+ self._refresh_all_boxes()
841
+
842
+ def _update_box_pen(self):
843
+ style_map = {
844
+ "Solid": Qt.PenStyle.SolidLine,
845
+ "Dash": Qt.PenStyle.DashLine,
846
+ "Dot": Qt.PenStyle.DotLine,
847
+ "DashDot": Qt.PenStyle.DashDotLine,
848
+ "DashDotDot": Qt.PenStyle.DashDotDotLine
849
+ }
850
+ self.bounding_box_pen.setWidth(self.sl_thick.value())
851
+ self.bounding_box_pen.setStyle(style_map[self.cmb_style.currentText()])
852
+ self._refresh_all_boxes()
853
+
854
+ def _refresh_all_boxes(self):
855
+ for r in self.bounding_boxes:
856
+ r.setPen(self.bounding_box_pen)
857
+
858
+ # snap an insert to one of 9 standard positions inside the base image
859
+ def send_insert_to_position(self, item: QGraphicsItem, key: str):
860
+ """Snap a selected insert (pixmap or text) to one of 9 standard positions."""
861
+ base = next((i for i in self.scene.items()
862
+ if isinstance(i, QGraphicsPixmapItem) and i.zValue() == 0), None)
863
+ if not base:
864
+ return
865
+
866
+ mx = self.sp_margin_x.value()
867
+ my = self.sp_margin_y.value()
868
+
869
+ br = base.boundingRect()
870
+ # item's *local* bounding rect
871
+ ir = item.boundingRect()
872
+ size = ir.size()
873
+
874
+ table = {
875
+ "top_left": QPointF(br.left() + mx, br.top() + my),
876
+ "top_center": QPointF(br.center().x() - size.width()/2, br.top() + my),
877
+ "top_right": QPointF(br.right() - size.width() - mx, br.top() + my),
878
+ "middle_left": QPointF(br.left() + mx, br.center().y() - size.height()/2),
879
+ "center": QPointF(br.center().x() - size.width()/2, br.center().y() - size.height()/2),
880
+ "middle_right": QPointF(br.right() - size.width() - mx, br.center().y() - size.height()/2),
881
+ "bottom_left": QPointF(br.left() + mx, br.bottom() - size.height() - my),
882
+ "bottom_center": QPointF(br.center().x() - size.width()/2, br.bottom() - size.height() - my),
883
+ "bottom_right": QPointF(br.right() - size.width() - mx, br.bottom() - size.height() - my),
884
+ }
885
+ pt = table.get(key)
886
+ if pt is None:
887
+ return
888
+
889
+ # map the desired *base* point into scene coords, then move item so its local
890
+ # top-left (0,0) maps onto that scene point.
891
+ scene_pt = base.mapToScene(pt)
892
+ item.setPos(scene_pt)
893
+
894
+ def _scrub_text_highlights_for_render(self):
895
+ """
896
+ Collapse any QTextCursor selections inside QGraphicsTextItem so no
897
+ character-range highlight can be painted by Qt during scene.render().
898
+ Also disable editing and selection temporarily to be extra safe.
899
+ """
900
+ self._text_restore = [] # stash state to restore after render
901
+
902
+ for it in self.scene.items():
903
+ if isinstance(it, QGraphicsTextItem):
904
+ # Save the minimal state we need to restore
905
+ self._text_restore.append((
906
+ it,
907
+ it.textInteractionFlags(),
908
+ it.flags(),
909
+ it.hasFocus()
910
+ ))
911
+
912
+ # 1) Collapse any in-text selection (this is the blue 'N' you see)
913
+ cur = it.textCursor()
914
+ # Force a definite collapse (some cases cur.hasSelection() is False
915
+ # but an anchor remains; resetting both positions removes it):
916
+ pos = cur.position()
917
+ cur.setPosition(pos, QTextCursor.MoveMode.MoveAnchor)
918
+ cur.setPosition(pos, QTextCursor.MoveMode.KeepAnchor) # set, then collapse
919
+ cur.clearSelection()
920
+ it.setTextCursor(cur)
921
+
922
+ # 2) Fully exit editing state
923
+ it.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
924
+ it.clearFocus()
925
+
926
+ # 3) Make sure the item itself cannot be “selected” while we paint
927
+ it.setSelected(False)
928
+ it.setFlags(it.flags() & ~QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
929
+
930
+ # 4) Ensure a repaint with the new state
931
+ it.update()
932
+
933
+ def _restore_text_state_after_render(self):
934
+ if not hasattr(self, "_text_restore"):
935
+ return
936
+ for it, flags, item_flags, had_focus in self._text_restore:
937
+ it.setTextInteractionFlags(flags)
938
+ it.setFlags(item_flags)
939
+ if had_focus:
940
+ it.setFocus()
941
+ self._text_restore = []
942
+
943
+
944
+ # bake overlays into the doc
945
+ def _affix_inserts(self):
946
+ if not (self.inserts or self._selected_text_items() or any(isinstance(i, QGraphicsTextItem) for i in self.scene.items())):
947
+ QMessageBox.information(self, "Signature / Insert", "Nothing to affix.")
948
+ return
949
+
950
+ # Deselect everything to avoid selection outlines in the render
951
+ for it in self.scene.selectedItems():
952
+ it.setSelected(False)
953
+
954
+ # honor box visibility
955
+ hidden_boxes = []
956
+ if not self.bounding_boxes_enabled:
957
+ for r in self.bounding_boxes:
958
+ r.setVisible(False); hidden_boxes.append(r)
959
+
960
+ # gather background + pixmap inserts + text + (maybe) boxes
961
+ items = []
962
+ for it in self.scene.items():
963
+ if isinstance(it, QGraphicsPixmapItem) and it.zValue() == 0:
964
+ items.append(it) # background
965
+ elif isinstance(it, QGraphicsPixmapItem) and it in self.inserts:
966
+ items.append(it)
967
+ elif isinstance(it, QGraphicsTextItem):
968
+ items.append(it)
969
+ elif self.bounding_boxes_enabled and isinstance(it, QGraphicsRectItem):
970
+ items.append(it)
971
+
972
+ # compute scene bbox
973
+ bbox = QRectF()
974
+ for it in items:
975
+ bbox = bbox.united(it.sceneBoundingRect())
976
+ bbox = bbox.normalized()
977
+ x, y = int(bbox.left()), int(bbox.top())
978
+ w, h = int(bbox.right()) - x, int(bbox.bottom()) - y
979
+ if w <= 0 or h <= 0:
980
+ return
981
+
982
+ # Temporarily suppress in-text selection highlights for text items
983
+ text_states = []
984
+ for it in self.scene.items():
985
+ if isinstance(it, QGraphicsTextItem):
986
+ text_states.append((
987
+ it,
988
+ it.textInteractionFlags(),
989
+ it.textCursor(),
990
+ it.hasFocus()
991
+ ))
992
+ # clear any selection highlight and disable editing visuals
993
+ cur = it.textCursor()
994
+ if cur.hasSelection():
995
+ cur.clearSelection()
996
+ it.setTextCursor(cur)
997
+ it.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
998
+ it.clearFocus()
999
+
1000
+ # temporarily hide non-items
1001
+ hidden = []
1002
+ for it in self.scene.items():
1003
+ if it not in items:
1004
+ it.setVisible(False); hidden.append(it)
1005
+
1006
+ self._scrub_text_highlights_for_render()
1007
+
1008
+ # --- render ---
1009
+ out = QImage(w, h, QImage.Format.Format_ARGB32)
1010
+ out.fill(Qt.GlobalColor.transparent)
1011
+ p = QPainter(out)
1012
+ self.scene.render(p, target=QRectF(0, 0, w, h), source=QRectF(x, y, w, h))
1013
+ p.end()
1014
+
1015
+ self._restore_text_state_after_render()
1016
+
1017
+ # restore hidden things
1018
+ for it in hidden: it.setVisible(True)
1019
+ for r in hidden_boxes: r.setVisible(True)
1020
+
1021
+ # restore text editability / state
1022
+ for it, flags, cursor, had_focus in text_states:
1023
+ it.setTextInteractionFlags(flags)
1024
+ it.setTextCursor(cursor)
1025
+ if had_focus:
1026
+ it.setFocus()
1027
+
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
+ # drop alpha → RGB, write back to doc
1046
+ arr = self._qimage_to_numpy(out)
1047
+ if arr.shape[2] == 4:
1048
+ arr = arr[:, :, :3]
1049
+ arr = np.clip(arr, 0.0, 1.0).astype(np.float32, copy=False)
1050
+
1051
+ if hasattr(self.doc, "set_image"):
1052
+ self.doc.set_image(arr, step_name="Signature / Insert")
1053
+ elif hasattr(self.doc, "apply_numpy"):
1054
+ self.doc.apply_numpy(arr, step_name="Signature / Insert")
1055
+ else:
1056
+ self.doc.image = arr
1057
+
1058
+ # cleanup
1059
+ self._clear_inserts()
1060
+ self._update_base_image()
1061
+
1062
+
1063
+ def _clear_inserts(self):
1064
+ # remove all user pixmap inserts
1065
+ for it in list(self.inserts):
1066
+ self._remove_item_and_accessories(it)
1067
+ self.inserts.clear()
1068
+
1069
+ # remove all text inserts
1070
+ for ti in list(self.text_inserts):
1071
+ self._remove_item_and_accessories(ti)
1072
+ self.text_inserts.clear()
1073
+
1074
+ # any stray boxes that weren't parented/cleaned
1075
+ for r in list(self.bounding_boxes):
1076
+ try:
1077
+ self.scene.removeItem(r)
1078
+ except Exception:
1079
+ pass
1080
+ self.bounding_boxes.clear()
1081
+
1082
+
1083
+ # ------------------ numpy/QImage bridges ------------------
1084
+ def _numpy_to_qimage(self, a: np.ndarray) -> QImage:
1085
+ a = np.asarray(a, dtype=np.float32)
1086
+ a = np.clip(a, 0.0, 1.0)
1087
+ if a.ndim == 2:
1088
+ a = a[..., None].repeat(3, axis=2)
1089
+ if a.shape[2] == 3:
1090
+ fmt, ch = QImage.Format.Format_RGB888, 3
1091
+ elif a.shape[2] == 4:
1092
+ fmt, ch = QImage.Format.Format_RGBA8888, 4
1093
+ else:
1094
+ raise ValueError(f"Unsupported shape {a.shape}")
1095
+ u8 = (a * 255.0).astype(np.uint8)
1096
+ u8 = np.ascontiguousarray(u8)
1097
+ h, w = u8.shape[:2]
1098
+ return QImage(u8.data, w, h, w*ch, fmt).copy()
1099
+
1100
+ def _qimage_to_numpy(self, img: QImage) -> np.ndarray:
1101
+ q = img.convertToFormat(QImage.Format.Format_RGBA8888)
1102
+ w, h = q.width(), q.height()
1103
+ ptr = q.bits(); ptr.setsize(h * q.bytesPerLine())
1104
+ buf = np.frombuffer(ptr, dtype=np.uint8).reshape((h, q.bytesPerLine()))
1105
+ arr = buf[:, :w*4].reshape((h, w, 4)).astype(np.float32)/255.0
1106
+ return arr