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