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,1102 @@
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
+ if icon:
370
+ try: self.setWindowIcon(icon)
371
+ except Exception as e:
372
+ import logging
373
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
374
+
375
+ self.doc = doc
376
+ self.scene = QGraphicsScene(self)
377
+ self.view = InsertView(self.scene, self)
378
+
379
+ self.inserts: list[QGraphicsPixmapItem] = []
380
+ self.bounding_boxes: list[QGraphicsRectItem] = []
381
+ self.bounding_boxes_enabled = True
382
+ self.bounding_box_pen = QPen(QColor("red"), 2, Qt.PenStyle.DashLine)
383
+ self.text_inserts: list[OutlinedTextItem] = []
384
+ self.scene.selectionChanged.connect(self._on_selection_changed)
385
+ # Handle sync timer (keeps the handle parked on the item corner)
386
+ self._timer = QTimer(self); self._timer.timeout.connect(self._sync_handles); self._timer.start(16)
387
+
388
+ self._build_ui()
389
+ self._update_base_image()
390
+ self.resize(1000, 680)
391
+
392
+ # -------- UI ----------
393
+ def _build_ui(self):
394
+ root = QHBoxLayout(self)
395
+
396
+ # ---- LEFT COLUMN ------------------------------------------------------
397
+ col = QVBoxLayout()
398
+
399
+ # Alpha hint (always visible – simple, clear)
400
+ alpha_hint = QLabel("Tip: Transparent signatures — use “Load from File” to preserve PNG alpha. "
401
+ "Loading from View uses RGB (no alpha).")
402
+ alpha_hint.setStyleSheet("color:#e0b000;")
403
+ alpha_hint.setWordWrap(True)
404
+ col.addWidget(alpha_hint)
405
+
406
+ # Load controls
407
+ row_load = QHBoxLayout()
408
+ b_from_view = QPushButton("Load Insert from View…"); b_from_view.clicked.connect(self._load_from_view)
409
+ b_from_file = QPushButton("Load Insert from File…"); b_from_file.clicked.connect(self._load_from_file)
410
+ row_load.addWidget(b_from_view); row_load.addWidget(b_from_file)
411
+ col.addLayout(row_load)
412
+
413
+ # --- Text controls ----------------------------------------------------
414
+ txt_grp = QGroupBox("Text")
415
+ tg = QGridLayout(txt_grp)
416
+
417
+ self.btn_add_text = QPushButton("Add Text…")
418
+ self.btn_edit_text = QPushButton("Edit Selected…"); self.btn_edit_text.setEnabled(False)
419
+ self.btn_add_text.clicked.connect(self._add_text_dialog)
420
+ self.btn_edit_text.clicked.connect(self._edit_selected_text)
421
+
422
+ tg.addWidget(self.btn_add_text, 0, 0)
423
+ tg.addWidget(self.btn_edit_text, 0, 1)
424
+
425
+ self.font_box = QFontComboBox(); self.font_box.setCurrentFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont))
426
+ self.font_size = QSpinBox(); self.font_size.setRange(4, 512); self.font_size.setValue(36)
427
+ self.chk_bold = QCheckBox("Bold")
428
+ self.chk_italic = QCheckBox("Italic")
429
+
430
+ self.btn_fill = QPushButton("Fill Color…")
431
+ self.btn_outline = QPushButton("Outline Color…")
432
+ self.outline_w = QSpinBox(); self.outline_w.setRange(0, 30); self.outline_w.setValue(0)
433
+
434
+ # wire style changes
435
+ self.font_box.currentFontChanged.connect(lambda _: self._apply_text_controls_to_selected())
436
+ self.font_size.valueChanged.connect(lambda _: self._apply_text_controls_to_selected())
437
+ self.chk_bold.stateChanged.connect(lambda _: self._apply_text_controls_to_selected())
438
+ self.chk_italic.stateChanged.connect(lambda _: self._apply_text_controls_to_selected())
439
+ self.btn_fill.clicked.connect(self._pick_text_fill)
440
+ self.btn_outline.clicked.connect(self._pick_text_outline)
441
+ self.outline_w.valueChanged.connect(lambda _: self._apply_text_controls_to_selected())
442
+
443
+ tg.addWidget(QLabel("Font"), 1, 0); tg.addWidget(self.font_box, 1, 1)
444
+ tg.addWidget(QLabel("Size"), 2, 0); tg.addWidget(self.font_size, 2, 1)
445
+ tg.addWidget(self.chk_bold, 3, 0); tg.addWidget(self.chk_italic, 3, 1)
446
+ tg.addWidget(self.btn_fill, 4, 0); tg.addWidget(self.btn_outline, 4, 1)
447
+ tg.addWidget(QLabel("Outline (px)"), 5, 0); tg.addWidget(self.outline_w, 5, 1)
448
+
449
+ col.addWidget(txt_grp)
450
+
451
+
452
+ # Transform group
453
+ grp = QGroupBox("Transform")
454
+ g = QGridLayout(grp)
455
+ b_rot = QPushButton("Rotate +90°"); b_rot.clicked.connect(self._rotate_selected)
456
+ g.addWidget(b_rot, 0, 0, 1, 2)
457
+
458
+ g.addWidget(QLabel("Scale (%)"), 1, 0)
459
+ self.sl_scale = QSlider(Qt.Orientation.Horizontal); self.sl_scale.setRange(10, 400); self.sl_scale.setValue(100)
460
+ self.sl_scale.valueChanged.connect(self._scale_selected)
461
+ g.addWidget(self.sl_scale, 1, 1)
462
+
463
+ g.addWidget(QLabel("Opacity (%)"), 2, 0)
464
+ self.sl_opacity = QSlider(Qt.Orientation.Horizontal); self.sl_opacity.setRange(0, 100); self.sl_opacity.setValue(100)
465
+ self.sl_opacity.valueChanged.connect(self._opacity_selected)
466
+ g.addWidget(self.sl_opacity, 2, 1)
467
+ col.addWidget(grp)
468
+
469
+ # Bounding boxes
470
+ self.cb_draw = QCheckBox("Draw Bounding Box"); self.cb_draw.setChecked(True); self.cb_draw.stateChanged.connect(self._toggle_boxes)
471
+ col.addWidget(self.cb_draw)
472
+
473
+ grp_box = QGroupBox("Bounding Box Style")
474
+ gb = QGridLayout(grp_box)
475
+ self.b_color = QPushButton("Color…"); self.b_color.clicked.connect(self._pick_box_color)
476
+ 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)
477
+ self.cmb_style = QComboBox(); self.cmb_style.addItems(["Solid","Dash","Dot","DashDot","DashDotDot"]); self.cmb_style.currentIndexChanged.connect(self._update_box_pen)
478
+ gb.addWidget(self.b_color, 0, 0, 1, 2)
479
+ gb.addWidget(QLabel("Thickness"), 1, 0); gb.addWidget(self.sl_thick, 1, 1)
480
+ gb.addWidget(QLabel("Style"), 2, 0); gb.addWidget(self.cmb_style, 2, 1)
481
+ col.addWidget(grp_box)
482
+
483
+ # --- Snap with margins -------------------------------------------------
484
+ snap_grp = QGroupBox("Send to position")
485
+ sg = QGridLayout(snap_grp)
486
+
487
+ # margins
488
+ sg.addWidget(QLabel("Margin X (px)"), 0, 0)
489
+ self.sp_margin_x = QSpinBox(); self.sp_margin_x.setRange(0, 5000); self.sp_margin_x.setValue(20)
490
+ sg.addWidget(self.sp_margin_x, 0, 1)
491
+
492
+ sg.addWidget(QLabel("Margin Y (px)"), 0, 2)
493
+ self.sp_margin_y = QSpinBox(); self.sp_margin_y.setRange(0, 5000); self.sp_margin_y.setValue(20)
494
+ sg.addWidget(self.sp_margin_y, 0, 3)
495
+
496
+ # 3x3 snap buttons
497
+ def s(key): # helper to create buttons
498
+ btn = QPushButton(key.replace('_', ' ').title())
499
+ btn.setMinimumWidth(105)
500
+ btn.clicked.connect(lambda _, k=key: self._send_selected(k))
501
+ return btn
502
+
503
+ sg.addWidget(s("top_left"), 1, 0)
504
+ sg.addWidget(s("top_center"), 1, 1)
505
+ sg.addWidget(s("top_right"), 1, 2)
506
+ sg.addWidget(s("middle_left"), 2, 0)
507
+ sg.addWidget(s("center"), 2, 1)
508
+ sg.addWidget(s("middle_right"), 2, 2)
509
+ sg.addWidget(s("bottom_left"), 3, 0)
510
+ sg.addWidget(s("bottom_center"), 3, 1)
511
+ sg.addWidget(s("bottom_right"), 3, 2)
512
+ col.addWidget(snap_grp)
513
+
514
+ # Zoom
515
+ row_zoom = QHBoxLayout()
516
+ b_zo = QPushButton("–"); b_zo.clicked.connect(self.view.zoom_out)
517
+ b_zi = QPushButton("+"); b_zi.clicked.connect(self.view.zoom_in)
518
+ b_fit = QPushButton("Fit"); b_fit.clicked.connect(self.view.fit_to_view)
519
+ 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)
520
+ col.addLayout(row_zoom)
521
+
522
+ col.addStretch(1)
523
+
524
+ # Commit/Clear
525
+ row_commit = QHBoxLayout()
526
+ b_affix = QPushButton("Affix Inserts"); b_affix.clicked.connect(self._affix_inserts)
527
+ b_clear_sel = QPushButton("Clear Selected"); b_clear_sel.clicked.connect(self._clear_selected)
528
+ b_clear = QPushButton("Clear All"); b_clear.clicked.connect(self._clear_inserts)
529
+ row_commit.addWidget(b_affix)
530
+ row_commit.addWidget(b_clear_sel) # ← NEW
531
+ row_commit.addWidget(b_clear)
532
+ row_commit.addStretch(1)
533
+ col.addLayout(row_commit)
534
+
535
+ left = QWidget(); left.setLayout(col)
536
+ root.addWidget(left, 0)
537
+ root.addWidget(self.view, 1)
538
+
539
+ def _selected_text_items(self):
540
+ return [it for it in self.scene.selectedItems() if isinstance(it, QGraphicsTextItem)]
541
+
542
+ def _selected_pixmap_items(self):
543
+ return [it for it in self.scene.selectedItems() if isinstance(it, QGraphicsPixmapItem)]
544
+
545
+ def _add_text_item(self, text: str, font: QFont, color: QColor):
546
+ ti = OutlinedTextItem(text, font, color, outline=None, outline_w=0.0)
547
+ ti.setTextInteractionFlags(Qt.TextInteractionFlag.TextEditorInteraction)
548
+ ti.setZValue(1)
549
+ ti.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
550
+ ti.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
551
+ ti.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, True)
552
+ ti.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges, True)
553
+ ti.setTransformOriginPoint(ti.boundingRect().center())
554
+ self.scene.addItem(ti)
555
+
556
+ TransformHandle(ti, self.scene)
557
+ self.text_inserts.append(ti)
558
+ ti.setSelected(True)
559
+ return ti
560
+
561
+ def _add_text_dialog(self):
562
+ # wrapper to match your signal connect
563
+ self._on_add_text()
564
+
565
+ def _add_text_dialog(self):
566
+ # wrapper to match your signal connect
567
+ self._on_add_text()
568
+
569
+ def _edit_selected_text(self):
570
+ items = self._selected_text_items()
571
+ if not items:
572
+ return
573
+ ti = items[0]
574
+ existing = ti.toPlainText()
575
+ txt, ok = QInputDialog.getMultiLineText(self, "Edit Text", "Text:", existing)
576
+ if ok:
577
+ ti.setPlainText(txt)
578
+
579
+ def _apply_text_controls_to_selected(self):
580
+ f = self._current_qfont()
581
+ w = self.outline_w.value()
582
+ for ti in self._selected_text_items():
583
+ if isinstance(ti, OutlinedTextItem):
584
+ ti.set_font(f)
585
+ # only adjust outline width here; color comes from the outline color picker
586
+ if w <= 0:
587
+ ti.set_outline(ti._outline, 0.0)
588
+ else:
589
+ ti.set_outline(ti._outline or QColor("black"), float(w))
590
+ else:
591
+ ti.setFont(f)
592
+
593
+ def _pick_text_fill(self):
594
+ c = QColorDialog.getColor()
595
+ if not c.isValid():
596
+ return
597
+ for ti in self._selected_text_items():
598
+ if isinstance(ti, OutlinedTextItem):
599
+ ti.set_fill(c)
600
+ else:
601
+ ti.setDefaultTextColor(c)
602
+
603
+ def _pick_text_outline(self):
604
+ c = QColorDialog.getColor()
605
+ if not c.isValid():
606
+ return
607
+ w = self.outline_w.value()
608
+ for ti in self._selected_text_items():
609
+ if isinstance(ti, OutlinedTextItem):
610
+ ti.set_outline(c, float(w))
611
+
612
+ def _clear_text_selection(self, ti: QGraphicsTextItem):
613
+ cur = ti.textCursor()
614
+ if cur.hasSelection():
615
+ cur.clearSelection()
616
+ ti.setTextCursor(cur)
617
+
618
+ def _remove_item_and_accessories(self, item: QGraphicsItem):
619
+ # Remove child bounding box if present & tracked
620
+ if isinstance(item, QGraphicsPixmapItem):
621
+ # child rect we added lives as parentItem(item)
622
+ for r in list(self.bounding_boxes):
623
+ if r.parentItem() is item:
624
+ try:
625
+ self.scene.removeItem(r)
626
+ except Exception:
627
+ pass
628
+ self.bounding_boxes.remove(r)
629
+
630
+ # Remove any TransformHandle bound to this item
631
+ for it in list(self.scene.items()):
632
+ if isinstance(it, TransformHandle) and getattr(it, "parent_item", None) is item:
633
+ try:
634
+ self.scene.removeItem(it)
635
+ except Exception:
636
+ pass
637
+
638
+ # Remove from our tracking lists
639
+ if isinstance(item, QGraphicsPixmapItem) and item in self.inserts:
640
+ self.inserts.remove(item)
641
+ if isinstance(item, QGraphicsTextItem) and item in self.text_inserts:
642
+ self.text_inserts.remove(item)
643
+
644
+ # Finally remove the item itself
645
+ try:
646
+ self.scene.removeItem(item)
647
+ except Exception:
648
+ pass
649
+
650
+ def _clear_selected(self):
651
+ for it in list(self.scene.selectedItems()):
652
+ # only user inserts (pixmaps) and text inserts are removable
653
+ if (isinstance(it, QGraphicsPixmapItem) and it in self.inserts) or isinstance(it, QGraphicsTextItem):
654
+ self._remove_item_and_accessories(it)
655
+
656
+
657
+ def _on_selection_changed(self):
658
+ texts = self._selected_text_items()
659
+ self.btn_edit_text.setEnabled(bool(texts))
660
+ if texts:
661
+ ti = texts[0]
662
+ f = ti.font()
663
+ self.font_box.setCurrentFont(f)
664
+ ps = f.pointSize() if f.pointSize() > 0 else 36
665
+ self.font_size.setValue(int(ps))
666
+ self.chk_bold.setChecked(f.bold())
667
+ self.chk_italic.setChecked(f.italic())
668
+ if isinstance(ti, OutlinedTextItem):
669
+ self.outline_w.setValue(int(round(ti._outline_w)))
670
+
671
+ # ── NEW: when a text item becomes unselected, clear any in-text highlight
672
+ selected_set = set(texts)
673
+ for it in self.text_inserts:
674
+ if it not in selected_set:
675
+ self._clear_text_selection(it)
676
+
677
+
678
+ def _current_qfont(self) -> QFont:
679
+ f = self.font_box.currentFont()
680
+ f.setPointSize(self.font_size.value())
681
+ f.setBold(self.chk_bold.isChecked())
682
+ f.setItalic(self.chk_italic.isChecked())
683
+ return f
684
+
685
+ def _apply_font_to_selection(self):
686
+ f = self._current_qfont()
687
+ for ti in self._selected_text_items():
688
+ ti.setFont(f)
689
+
690
+ def _apply_color_to_selection(self, color: QColor):
691
+ for ti in self._selected_text_items():
692
+ ti.setDefaultTextColor(color)
693
+
694
+ def _on_add_text(self):
695
+ txt, ok = QInputDialog.getMultiLineText(self, "Add Text", "Enter text:")
696
+ if not ok or not txt.strip():
697
+ return
698
+ f = self._current_qfont()
699
+ c = QColor("white") # default
700
+ ti = self._add_text_item(txt, f, c)
701
+ # drop it near center
702
+ base = next((i for i in self.scene.items()
703
+ if isinstance(i, QGraphicsPixmapItem) and i.zValue() == 0), None)
704
+ if base:
705
+ center_scene = base.mapToScene(base.boundingRect().center())
706
+ ti.setPos(center_scene - ti.boundingRect().center())
707
+
708
+ def _on_text_color(self):
709
+ c = QColorDialog.getColor()
710
+ if c.isValid():
711
+ self._apply_color_to_selection(c)
712
+
713
+ def _on_font_changed(self, _):
714
+ self._apply_font_to_selection()
715
+
716
+ def _on_font_size(self, _):
717
+ self._apply_font_to_selection()
718
+
719
+ def _on_font_bold(self, _):
720
+ self._apply_font_to_selection()
721
+
722
+ def _on_font_italic(self, _):
723
+ self._apply_font_to_selection()
724
+
725
+
726
+ # -------- Scene / items ----------
727
+ def _sync_handles(self):
728
+ for it in self.scene.items():
729
+ if isinstance(it, TransformHandle):
730
+ it.update_position()
731
+
732
+ def _update_base_image(self):
733
+ self.scene.clear()
734
+ arr = np.asarray(self.doc.image, dtype=np.float32)
735
+ if arr is None: return
736
+ qimg = self._numpy_to_qimage(arr)
737
+ bg = QGraphicsPixmapItem(QPixmap.fromImage(qimg))
738
+ bg.setZValue(0)
739
+ self.scene.addItem(bg)
740
+
741
+ def _load_from_file(self):
742
+ fp, _ = QFileDialog.getOpenFileName(self, "Select Insert Image", "", "Images (*.png *.jpg *.jpeg *.tif *.tiff)")
743
+ if not fp: return
744
+ pm = QPixmap(fp)
745
+ if pm.isNull():
746
+ QMessageBox.warning(self, "Load Failed", "Could not load image.")
747
+ return
748
+ self._add_insert(pm)
749
+
750
+ def _load_from_view(self):
751
+ # list all open views via a helper the app already uses elsewhere (fallback to active only)
752
+ candidates = []
753
+ if hasattr(self.parent(), "_subwindow_docs"):
754
+ for title, d in self.parent()._subwindow_docs():
755
+ if d is self.doc: # skip self
756
+ continue
757
+ if getattr(d, "image", None) is not None:
758
+ candidates.append((title, d))
759
+ if not candidates:
760
+ QMessageBox.information(self, "Insert", "No other image windows found.")
761
+ return
762
+
763
+ names = [t for (t, _) in candidates]
764
+ choice, ok = QInputDialog.getItem(self, "Load Insert from View", "Choose:", names, 0, False)
765
+ if not ok: return
766
+ d = candidates[names.index(choice)][1]
767
+ pm = QPixmap.fromImage(self._numpy_to_qimage(np.asarray(d.image, dtype=np.float32)))
768
+ self._add_insert(pm)
769
+
770
+ def _add_insert(self, pm: QPixmap):
771
+ it = QGraphicsPixmapItem(pm)
772
+ it.setFlags(
773
+ QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
774
+ QGraphicsItem.GraphicsItemFlag.ItemIsSelectable |
775
+ QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
776
+ QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
777
+ )
778
+ it.setTransformationMode(Qt.TransformationMode.SmoothTransformation)
779
+ it.setTransformOriginPoint(it.boundingRect().center())
780
+ it.setZValue(1)
781
+ it.setOpacity(1.0)
782
+ self.scene.addItem(it)
783
+ self.inserts.append(it)
784
+ TransformHandle(it, self.scene)
785
+
786
+ if self.bounding_boxes_enabled:
787
+ rect = QGraphicsRectItem(it.boundingRect())
788
+ rect.setParentItem(it)
789
+ rect.setPen(self.bounding_box_pen)
790
+ rect.setAcceptedMouseButtons(Qt.MouseButton.NoButton)
791
+ rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIgnoresParentOpacity, True)
792
+ rect.setZValue(it.zValue() + 0.1)
793
+ self.scene.addItem(rect)
794
+ self.bounding_boxes.append(rect)
795
+
796
+
797
+ def _send_selected(self, key: str):
798
+ # pixmaps
799
+ for it in self.inserts:
800
+ if it.isSelected():
801
+ self.send_insert_to_position(it, key)
802
+ # text
803
+ for ti in self._selected_text_items():
804
+ self.send_insert_to_position(ti, key)
805
+
806
+ # -------- Commands ----------
807
+ def _rotate_selected(self):
808
+ for it in self.inserts:
809
+ if it.isSelected():
810
+ it.setRotation(it.rotation() + 90)
811
+
812
+ def _scale_selected(self, val):
813
+ s = val / 100.0
814
+ for it in self.inserts:
815
+ if it.isSelected():
816
+ it.setScale(s)
817
+ # keep the child rect matching the pixmap's local bounds
818
+ for box in self.bounding_boxes:
819
+ if box.parentItem() == it:
820
+ box.setRect(it.boundingRect())
821
+
822
+ def _opacity_selected(self, val):
823
+ o = val / 100.0
824
+ for it in (self._selected_pixmap_items() + self._selected_text_items()):
825
+ it.setOpacity(o)
826
+
827
+ def _toggle_boxes(self, state):
828
+ self.bounding_boxes_enabled = bool(state)
829
+ for r in self.bounding_boxes:
830
+ r.setVisible(self.bounding_boxes_enabled)
831
+
832
+ def _pick_box_color(self):
833
+ c = QColorDialog.getColor()
834
+ if c.isValid():
835
+ self.bounding_box_pen.setColor(c)
836
+ self._refresh_all_boxes()
837
+
838
+ def _update_box_pen(self):
839
+ style_map = {
840
+ "Solid": Qt.PenStyle.SolidLine,
841
+ "Dash": Qt.PenStyle.DashLine,
842
+ "Dot": Qt.PenStyle.DotLine,
843
+ "DashDot": Qt.PenStyle.DashDotLine,
844
+ "DashDotDot": Qt.PenStyle.DashDotDotLine
845
+ }
846
+ self.bounding_box_pen.setWidth(self.sl_thick.value())
847
+ self.bounding_box_pen.setStyle(style_map[self.cmb_style.currentText()])
848
+ self._refresh_all_boxes()
849
+
850
+ def _refresh_all_boxes(self):
851
+ for r in self.bounding_boxes:
852
+ r.setPen(self.bounding_box_pen)
853
+
854
+ # snap an insert to one of 9 standard positions inside the base image
855
+ def send_insert_to_position(self, item: QGraphicsItem, key: str):
856
+ """Snap a selected insert (pixmap or text) to one of 9 standard positions."""
857
+ base = next((i for i in self.scene.items()
858
+ if isinstance(i, QGraphicsPixmapItem) and i.zValue() == 0), None)
859
+ if not base:
860
+ return
861
+
862
+ mx = self.sp_margin_x.value()
863
+ my = self.sp_margin_y.value()
864
+
865
+ br = base.boundingRect()
866
+ # item's *local* bounding rect
867
+ ir = item.boundingRect()
868
+ size = ir.size()
869
+
870
+ table = {
871
+ "top_left": QPointF(br.left() + mx, br.top() + my),
872
+ "top_center": QPointF(br.center().x() - size.width()/2, br.top() + my),
873
+ "top_right": QPointF(br.right() - size.width() - mx, br.top() + my),
874
+ "middle_left": QPointF(br.left() + mx, br.center().y() - size.height()/2),
875
+ "center": QPointF(br.center().x() - size.width()/2, br.center().y() - size.height()/2),
876
+ "middle_right": QPointF(br.right() - size.width() - mx, br.center().y() - size.height()/2),
877
+ "bottom_left": QPointF(br.left() + mx, br.bottom() - size.height() - my),
878
+ "bottom_center": QPointF(br.center().x() - size.width()/2, br.bottom() - size.height() - my),
879
+ "bottom_right": QPointF(br.right() - size.width() - mx, br.bottom() - size.height() - my),
880
+ }
881
+ pt = table.get(key)
882
+ if pt is None:
883
+ return
884
+
885
+ # map the desired *base* point into scene coords, then move item so its local
886
+ # top-left (0,0) maps onto that scene point.
887
+ scene_pt = base.mapToScene(pt)
888
+ item.setPos(scene_pt)
889
+
890
+ def _scrub_text_highlights_for_render(self):
891
+ """
892
+ Collapse any QTextCursor selections inside QGraphicsTextItem so no
893
+ character-range highlight can be painted by Qt during scene.render().
894
+ Also disable editing and selection temporarily to be extra safe.
895
+ """
896
+ self._text_restore = [] # stash state to restore after render
897
+
898
+ for it in self.scene.items():
899
+ if isinstance(it, QGraphicsTextItem):
900
+ # Save the minimal state we need to restore
901
+ self._text_restore.append((
902
+ it,
903
+ it.textInteractionFlags(),
904
+ it.flags(),
905
+ it.hasFocus()
906
+ ))
907
+
908
+ # 1) Collapse any in-text selection (this is the blue 'N' you see)
909
+ cur = it.textCursor()
910
+ # Force a definite collapse (some cases cur.hasSelection() is False
911
+ # but an anchor remains; resetting both positions removes it):
912
+ pos = cur.position()
913
+ cur.setPosition(pos, QTextCursor.MoveMode.MoveAnchor)
914
+ cur.setPosition(pos, QTextCursor.MoveMode.KeepAnchor) # set, then collapse
915
+ cur.clearSelection()
916
+ it.setTextCursor(cur)
917
+
918
+ # 2) Fully exit editing state
919
+ it.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
920
+ it.clearFocus()
921
+
922
+ # 3) Make sure the item itself cannot be “selected” while we paint
923
+ it.setSelected(False)
924
+ it.setFlags(it.flags() & ~QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
925
+
926
+ # 4) Ensure a repaint with the new state
927
+ it.update()
928
+
929
+ def _restore_text_state_after_render(self):
930
+ if not hasattr(self, "_text_restore"):
931
+ return
932
+ for it, flags, item_flags, had_focus in self._text_restore:
933
+ it.setTextInteractionFlags(flags)
934
+ it.setFlags(item_flags)
935
+ if had_focus:
936
+ it.setFocus()
937
+ self._text_restore = []
938
+
939
+
940
+ # bake overlays into the doc
941
+ def _affix_inserts(self):
942
+ if not (self.inserts or self._selected_text_items() or any(isinstance(i, QGraphicsTextItem) for i in self.scene.items())):
943
+ QMessageBox.information(self, "Signature / Insert", "Nothing to affix.")
944
+ return
945
+
946
+ # Deselect everything to avoid selection outlines in the render
947
+ for it in self.scene.selectedItems():
948
+ it.setSelected(False)
949
+
950
+ # honor box visibility
951
+ hidden_boxes = []
952
+ if not self.bounding_boxes_enabled:
953
+ for r in self.bounding_boxes:
954
+ r.setVisible(False); hidden_boxes.append(r)
955
+
956
+ # gather background + pixmap inserts + text + (maybe) boxes
957
+ items = []
958
+ for it in self.scene.items():
959
+ if isinstance(it, QGraphicsPixmapItem) and it.zValue() == 0:
960
+ items.append(it) # background
961
+ elif isinstance(it, QGraphicsPixmapItem) and it in self.inserts:
962
+ items.append(it)
963
+ elif isinstance(it, QGraphicsTextItem):
964
+ items.append(it)
965
+ elif self.bounding_boxes_enabled and isinstance(it, QGraphicsRectItem):
966
+ items.append(it)
967
+
968
+ # compute scene bbox
969
+ bbox = QRectF()
970
+ for it in items:
971
+ bbox = bbox.united(it.sceneBoundingRect())
972
+ bbox = bbox.normalized()
973
+ x, y = int(bbox.left()), int(bbox.top())
974
+ w, h = int(bbox.right()) - x, int(bbox.bottom()) - y
975
+ if w <= 0 or h <= 0:
976
+ return
977
+
978
+ # Temporarily suppress in-text selection highlights for text items
979
+ text_states = []
980
+ for it in self.scene.items():
981
+ if isinstance(it, QGraphicsTextItem):
982
+ text_states.append((
983
+ it,
984
+ it.textInteractionFlags(),
985
+ it.textCursor(),
986
+ it.hasFocus()
987
+ ))
988
+ # clear any selection highlight and disable editing visuals
989
+ cur = it.textCursor()
990
+ if cur.hasSelection():
991
+ cur.clearSelection()
992
+ it.setTextCursor(cur)
993
+ it.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
994
+ it.clearFocus()
995
+
996
+ # temporarily hide non-items
997
+ hidden = []
998
+ for it in self.scene.items():
999
+ if it not in items:
1000
+ it.setVisible(False); hidden.append(it)
1001
+
1002
+ self._scrub_text_highlights_for_render()
1003
+
1004
+ # --- render ---
1005
+ out = QImage(w, h, QImage.Format.Format_ARGB32)
1006
+ out.fill(Qt.GlobalColor.transparent)
1007
+ p = QPainter(out)
1008
+ self.scene.render(p, target=QRectF(0, 0, w, h), source=QRectF(x, y, w, h))
1009
+ p.end()
1010
+
1011
+ self._restore_text_state_after_render()
1012
+
1013
+ # restore hidden things
1014
+ for it in hidden: it.setVisible(True)
1015
+ for r in hidden_boxes: r.setVisible(True)
1016
+
1017
+ # restore text editability / state
1018
+ for it, flags, cursor, had_focus in text_states:
1019
+ it.setTextInteractionFlags(flags)
1020
+ it.setTextCursor(cursor)
1021
+ if had_focus:
1022
+ it.setFocus()
1023
+
1024
+ # temporarily hide non-items
1025
+ hidden = []
1026
+ for it in self.scene.items():
1027
+ if it not in items:
1028
+ it.setVisible(False); hidden.append(it)
1029
+
1030
+ # render
1031
+ out = QImage(w, h, QImage.Format.Format_ARGB32)
1032
+ out.fill(Qt.GlobalColor.transparent)
1033
+ p = QPainter(out)
1034
+ self.scene.render(p, target=QRectF(0, 0, w, h), source=QRectF(x, y, w, h))
1035
+ p.end()
1036
+
1037
+ # restore
1038
+ for it in hidden: it.setVisible(True)
1039
+ for r in hidden_boxes: r.setVisible(True)
1040
+
1041
+ # drop alpha → RGB, write back to doc
1042
+ arr = self._qimage_to_numpy(out)
1043
+ if arr.shape[2] == 4:
1044
+ arr = arr[:, :, :3]
1045
+ arr = np.clip(arr, 0.0, 1.0).astype(np.float32, copy=False)
1046
+
1047
+ if hasattr(self.doc, "set_image"):
1048
+ self.doc.set_image(arr, step_name="Signature / Insert")
1049
+ elif hasattr(self.doc, "apply_numpy"):
1050
+ self.doc.apply_numpy(arr, step_name="Signature / Insert")
1051
+ else:
1052
+ self.doc.image = arr
1053
+
1054
+ # cleanup
1055
+ self._clear_inserts()
1056
+ self._update_base_image()
1057
+
1058
+
1059
+ def _clear_inserts(self):
1060
+ # remove all user pixmap inserts
1061
+ for it in list(self.inserts):
1062
+ self._remove_item_and_accessories(it)
1063
+ self.inserts.clear()
1064
+
1065
+ # remove all text inserts
1066
+ for ti in list(self.text_inserts):
1067
+ self._remove_item_and_accessories(ti)
1068
+ self.text_inserts.clear()
1069
+
1070
+ # any stray boxes that weren't parented/cleaned
1071
+ for r in list(self.bounding_boxes):
1072
+ try:
1073
+ self.scene.removeItem(r)
1074
+ except Exception:
1075
+ pass
1076
+ self.bounding_boxes.clear()
1077
+
1078
+
1079
+ # ------------------ numpy/QImage bridges ------------------
1080
+ def _numpy_to_qimage(self, a: np.ndarray) -> QImage:
1081
+ a = np.asarray(a, dtype=np.float32)
1082
+ a = np.clip(a, 0.0, 1.0)
1083
+ if a.ndim == 2:
1084
+ a = a[..., None].repeat(3, axis=2)
1085
+ if a.shape[2] == 3:
1086
+ fmt, ch = QImage.Format.Format_RGB888, 3
1087
+ elif a.shape[2] == 4:
1088
+ fmt, ch = QImage.Format.Format_RGBA8888, 4
1089
+ else:
1090
+ raise ValueError(f"Unsupported shape {a.shape}")
1091
+ u8 = (a * 255.0).astype(np.uint8)
1092
+ u8 = np.ascontiguousarray(u8)
1093
+ h, w = u8.shape[:2]
1094
+ return QImage(u8.data, w, h, w*ch, fmt).copy()
1095
+
1096
+ def _qimage_to_numpy(self, img: QImage) -> np.ndarray:
1097
+ q = img.convertToFormat(QImage.Format.Format_RGBA8888)
1098
+ w, h = q.width(), q.height()
1099
+ ptr = q.bits(); ptr.setsize(h * q.bytesPerLine())
1100
+ buf = np.frombuffer(ptr, dtype=np.uint8).reshape((h, q.bytesPerLine()))
1101
+ arr = buf[:, :w*4].reshape((h, w, 4)).astype(np.float32)/255.0
1102
+ return arr